├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .ruby-gemset
├── .ruby-version
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── lib
├── vcard.rb
└── vcard
│ ├── attachment.rb
│ ├── bnf.rb
│ ├── configuration.rb
│ ├── dirinfo.rb
│ ├── enumerator.rb
│ ├── errors.rb
│ ├── field.rb
│ ├── vcard.rb
│ └── version.rb
├── test
├── configuration_test.rb
├── field_test.rb
├── fixtures
│ ├── bday_decode.vcard
│ ├── bday_decode_2.vcard
│ ├── bday_decode_3.vcard
│ ├── empty_tel.vcard
│ ├── ex1.vcard
│ ├── ex2.vcard
│ ├── ex3.vcard
│ ├── ex_21.vcard
│ ├── ex_21_case0.vcard
│ ├── ex_apple1.vcard
│ ├── ex_attach.vcard
│ ├── ex_bdays.vcard
│ ├── ex_encode_1.vcard
│ ├── ex_ical_1.vcal
│ ├── gmail.vcard
│ ├── highrise.vcard
│ ├── multiple_occurences_of_type.vcard
│ ├── nickname0.vcard
│ ├── nickname1.vcard
│ ├── nickname2.vcard
│ ├── nickname3.vcard
│ ├── nickname4.vcard
│ ├── nickname5.vcard
│ ├── slash_in_field_name.vcard
│ ├── tst1.vcard
│ └── url_decode.vcard
├── test_helper.rb
└── vcard_test.rb
└── vcard.gemspec
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | host:
9 | name: ${{ matrix.os }} ${{ matrix.ruby }}
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | os:
15 | - ubuntu-latest
16 | ruby:
17 | - '3.4'
18 | - '3.3'
19 | - '3.2'
20 | - '3.1'
21 | - '3.0'
22 | - '2.7'
23 | - '2.6'
24 |
25 | steps:
26 | - uses: actions/checkout@v2
27 |
28 | - name: Set up Ruby
29 | uses: ruby/setup-ruby@v1
30 | with:
31 | ruby-version: ${{ matrix.ruby }}
32 | bundler-cache: true
33 |
34 | - run: ruby --version
35 |
36 | - run: bundle exec rake
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | .rvmrc
19 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | vcard
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.4.4
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [Unreleased](https://github.com/qoobaa/vcard/tree/HEAD)
4 |
5 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.3.0...HEAD)
6 |
7 | ## [v0.3.0](https://github.com/qoobaa/vcard/tree/v0.3.0) (2020-11-20)
8 |
9 | **Closed issues:**
10 |
11 | - New release with ruby25 and ruby26 support [\#30](https://github.com/qoobaa/vcard/issues/30)
12 |
13 | **Merged pull requests:**
14 |
15 | - Add an `nl` option to encode\(\) and remove caching [\#35](https://github.com/qoobaa/vcard/pull/35) ([knu](https://github.com/knu))
16 | - Use the default argument in encode\(\) [\#34](https://github.com/qoobaa/vcard/pull/34) ([knu](https://github.com/knu))
17 | - Do not modify the fields argument in Vcard::Vcard.create\(\) [\#33](https://github.com/qoobaa/vcard/pull/33) ([knu](https://github.com/knu))
18 | - Wrap lines exactly by a given width \(75 by default\) [\#32](https://github.com/qoobaa/vcard/pull/32) ([knu](https://github.com/knu))
19 |
20 | ## [v0.2.16](https://github.com/qoobaa/vcard/tree/v0.2.16) (2019-12-01)
21 |
22 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.15...v0.2.16)
23 |
24 | **Merged pull requests:**
25 |
26 | - Update supported ruby versions [\#31](https://github.com/qoobaa/vcard/pull/31) ([senzpo](https://github.com/senzpo))
27 |
28 | ## [v0.2.15](https://github.com/qoobaa/vcard/tree/v0.2.15) (2016-12-01)
29 |
30 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.14...v0.2.15)
31 |
32 | **Merged pull requests:**
33 |
34 | - Fix incorrect scope of Vcard.configure and Vcard.configuration methods [\#29](https://github.com/qoobaa/vcard/pull/29) ([solenko](https://github.com/solenko))
35 |
36 | ## [v0.2.14](https://github.com/qoobaa/vcard/tree/v0.2.14) (2016-11-22)
37 |
38 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.13...v0.2.14)
39 |
40 | **Closed issues:**
41 |
42 | - Method for releasing a new gem version [\#27](https://github.com/qoobaa/vcard/issues/27)
43 |
44 | **Merged pull requests:**
45 |
46 | - Allow to ignore invalid fields [\#28](https://github.com/qoobaa/vcard/pull/28) ([solenko](https://github.com/solenko))
47 |
48 | ## [v0.2.13](https://github.com/qoobaa/vcard/tree/v0.2.13) (2016-09-23)
49 |
50 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.12...v0.2.13)
51 |
52 | **Closed issues:**
53 |
54 | - vCard v2.1 ADR fields with quoted-printable encoding not decoded [\#22](https://github.com/qoobaa/vcard/issues/22)
55 | - License missing from gemspec [\#17](https://github.com/qoobaa/vcard/issues/17)
56 |
57 | **Merged pull requests:**
58 |
59 | - Fix tests [\#26](https://github.com/qoobaa/vcard/pull/26) ([brendon](https://github.com/brendon))
60 | - Fix 2.2 2.3 & Remove 1.9.2 [\#25](https://github.com/qoobaa/vcard/pull/25) ([brendon](https://github.com/brendon))
61 | - Fix travis bundler [\#24](https://github.com/qoobaa/vcard/pull/24) ([brendon](https://github.com/brendon))
62 | - Add Ruby 2.x for testing [\#23](https://github.com/qoobaa/vcard/pull/23) ([brendon](https://github.com/brendon))
63 |
64 | ## [v0.2.12](https://github.com/qoobaa/vcard/tree/v0.2.12) (2013-10-30)
65 |
66 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.11...v0.2.12)
67 |
68 | **Merged pull requests:**
69 |
70 | - Add work-around for JRuby Regexp issue [\#20](https://github.com/qoobaa/vcard/pull/20) ([dgolombek](https://github.com/dgolombek))
71 | - Change field's to accept symbols as values [\#19](https://github.com/qoobaa/vcard/pull/19) ([dgolombek](https://github.com/dgolombek))
72 |
73 | ## [v0.2.11](https://github.com/qoobaa/vcard/tree/v0.2.11) (2013-10-02)
74 |
75 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.10...v0.2.11)
76 |
77 | ## [v0.2.10](https://github.com/qoobaa/vcard/tree/v0.2.10) (2013-10-01)
78 |
79 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.9...v0.2.10)
80 |
81 | ## [v0.2.9](https://github.com/qoobaa/vcard/tree/v0.2.9) (2013-09-27)
82 |
83 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.8...v0.2.9)
84 |
85 | **Closed issues:**
86 |
87 | - Failing Tests [\#11](https://github.com/qoobaa/vcard/issues/11)
88 |
89 | **Merged pull requests:**
90 |
91 | - Fix decoding of multi-line quoted-printable fields [\#16](https://github.com/qoobaa/vcard/pull/16) ([dgolombek](https://github.com/dgolombek))
92 |
93 | ## [v0.2.8](https://github.com/qoobaa/vcard/tree/v0.2.8) (2013-06-20)
94 |
95 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.7...v0.2.8)
96 |
97 | ## [v0.2.7](https://github.com/qoobaa/vcard/tree/v0.2.7) (2013-06-20)
98 |
99 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.6...v0.2.7)
100 |
101 | **Closed issues:**
102 |
103 | - Encoding line breaks for things like Address and Notes [\#9](https://github.com/qoobaa/vcard/issues/9)
104 |
105 | **Merged pull requests:**
106 |
107 | - Add back support for multiple ROLEs [\#15](https://github.com/qoobaa/vcard/pull/15) ([brendon](https://github.com/brendon))
108 |
109 | ## [v0.2.6](https://github.com/qoobaa/vcard/tree/v0.2.6) (2013-06-19)
110 |
111 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.5...v0.2.6)
112 |
113 | **Merged pull requests:**
114 |
115 | - Eliminate carriage returns when encoding text [\#14](https://github.com/qoobaa/vcard/pull/14) ([m1foley](https://github.com/m1foley))
116 |
117 | ## [v0.2.5](https://github.com/qoobaa/vcard/tree/v0.2.5) (2013-06-19)
118 |
119 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.4...v0.2.5)
120 |
121 | **Closed issues:**
122 |
123 | - Role is missing [\#8](https://github.com/qoobaa/vcard/issues/8)
124 |
125 | **Merged pull requests:**
126 |
127 | - Newlines bug [\#13](https://github.com/qoobaa/vcard/pull/13) ([brendon](https://github.com/brendon))
128 | - ROLE can occur multiple times [\#12](https://github.com/qoobaa/vcard/pull/12) ([brendon](https://github.com/brendon))
129 |
130 | ## [v0.2.4](https://github.com/qoobaa/vcard/tree/v0.2.4) (2013-06-18)
131 |
132 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.3...v0.2.4)
133 |
134 | **Closed issues:**
135 |
136 | - Using Ruby 2.0 there is invalid multibyte escape error [\#6](https://github.com/qoobaa/vcard/issues/6)
137 |
138 | **Merged pull requests:**
139 |
140 | - Added native support for ROLE with escaping [\#10](https://github.com/qoobaa/vcard/pull/10) ([brendon](https://github.com/brendon))
141 |
142 | ## [v0.2.3](https://github.com/qoobaa/vcard/tree/v0.2.3) (2013-01-31)
143 |
144 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.2...v0.2.3)
145 |
146 | **Closed issues:**
147 |
148 | - Birthday decoding broken in 0.2.0 [\#4](https://github.com/qoobaa/vcard/issues/4)
149 |
150 | **Merged pull requests:**
151 |
152 | - Code patch to correct invalid multibyte escape [\#7](https://github.com/qoobaa/vcard/pull/7) ([seth-macpherson](https://github.com/seth-macpherson))
153 |
154 | ## [v0.2.2](https://github.com/qoobaa/vcard/tree/v0.2.2) (2012-12-17)
155 |
156 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.1...v0.2.2)
157 |
158 | **Merged pull requests:**
159 |
160 | - Regex refactor, includes date parsing fix [\#5](https://github.com/qoobaa/vcard/pull/5) ([m1foley](https://github.com/m1foley))
161 |
162 | ## [v0.2.1](https://github.com/qoobaa/vcard/tree/v0.2.1) (2012-12-16)
163 |
164 | [Full Changelog](https://github.com/qoobaa/vcard/compare/v0.2.0...v0.2.1)
165 |
166 | **Merged pull requests:**
167 |
168 | - Fix wrong scope, add Travis [\#3](https://github.com/qoobaa/vcard/pull/3) ([rngtng](https://github.com/rngtng))
169 |
170 | ## [v0.2.0](https://github.com/qoobaa/vcard/tree/v0.2.0) (2012-11-26)
171 |
172 | [Full Changelog](https://github.com/qoobaa/vcard/compare/46a2517b0f9bd073b00f233eeae6f106069ddce5...v0.2.0)
173 |
174 | **Closed issues:**
175 |
176 | - incompatible encoding regexp match \(ASCII-8BIT regexp with UTF-8 string\) [\#1](https://github.com/qoobaa/vcard/issues/1)
177 |
178 | **Merged pull requests:**
179 |
180 | - remove superfluous encoding detection, which errors with ruby-1.9 [\#2](https://github.com/qoobaa/vcard/pull/2) ([hannesm](https://github.com/hannesm))
181 |
182 |
183 |
184 |
185 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem "rake"
6 | gem 'test-unit'
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vcard [](https://github.com/qoobaa/vcard/actions/workflows/ci.yml)
2 |
3 | Vcard gem extracts Vcard support from Vpim gem.
4 |
5 | ## Installation
6 |
7 | Add this line to your application's Gemfile:
8 |
9 | gem "vcard"
10 |
11 | And then execute:
12 |
13 | $ bundle
14 |
15 | Or install it yourself as:
16 |
17 | $ gem install vcard
18 |
19 | ## Configuration
20 |
21 | You can configure how to deal with invalid lines. The gem supports three behaviours:
22 |
23 | 1. `raise_on_invalid_line = true`
24 |
25 | Vcard::InvalidEncodingError will be raised if any invalid line is found.
26 |
27 | 2. `raise_on_invalid_line = false, ignore_invalid_vcards = true`
28 |
29 | If the vcard source has an invalid line, this vcard object will be ignored.
30 | If you have only one vcard object in your source string, an empty array will be returned from `Vcard.decode`.
31 |
32 | 3. `raise_on_invalid_line = false, ignore_invalid_vcards = false`
33 |
34 | If the vcard is marked as invalid, invalid fields will be ignored, but the vcard will be present in the results of `Vcard#decode`.
35 |
36 | ```
37 | Vcard.configure do |config|
38 | config.raise_on_invalid_line = false # default true
39 | config.ignore_invalid_vcards = false # default true
40 | end
41 | ```
42 |
43 | ## Upgrade Notes
44 |
45 | We are no longer testing against Ruby 1.8.7.
46 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new(:test) do |test|
5 | test.libs << "lib" << "test"
6 | test.pattern = "test/**/*_test.rb"
7 | test.verbose = true
8 | end
9 |
10 | task :default => :test
11 |
--------------------------------------------------------------------------------
/lib/vcard.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the same terms as the ruby language itself.
5 |
6 | require "date"
7 | require "open-uri"
8 | require "stringio"
9 |
10 | require "vcard/configuration"
11 | require "vcard/attachment"
12 | require "vcard/bnf"
13 | require "vcard/dirinfo"
14 | require "vcard/enumerator"
15 | require "vcard/errors"
16 | require "vcard/field"
17 | require "vcard/vcard"
18 |
19 | module Vcard
20 | # Split on \r\n or \n to get the lines, unfold continued lines (they
21 | # start with " " or \t), and return the array of unfolded lines.
22 | #
23 | # This also supports the (invalid) encoding convention of allowing empty
24 | # lines to be inserted for readability - it does this by dropping zero-length
25 | # lines.
26 | def self.unfold(card) #:nodoc:
27 | unfolded = []
28 |
29 | prior_line = nil
30 | card.each_line do |line|
31 | line.chomp!
32 | # If it's a continuation line, add it to the last.
33 | # If it's an empty line, drop it from the input.
34 | if line =~ /^[ \t]/
35 | unfolded[-1] << line[1, line.size-1]
36 | elsif prior_line && (prior_line =~ Bnf::UNTERMINATED_QUOTED_PRINTABLE)
37 | # Strip the trailing = off prior line, then append current line
38 | unfolded[-1] = prior_line[0, prior_line.length-1] + line
39 | elsif line =~ /^$/
40 | else
41 | unfolded << line
42 | end
43 | prior_line = unfolded[-1]
44 | end
45 |
46 | unfolded
47 | end
48 |
49 | # Convert a +sep+-seperated list of values into an array of values.
50 | def self.decode_list(value, sep = ",") # :nodoc:
51 | list = []
52 |
53 | value.split(sep).each do |item|
54 | item.chomp!(sep)
55 | list << yield(item)
56 | end
57 |
58 | list
59 | end
60 |
61 | # Convert a RFC 2425 date into an array of [year, month, day].
62 | def self.decode_date(v) # :nodoc:
63 | raise ::Vcard::InvalidEncodingError, "date not valid (#{v})" unless v =~ Bnf::DATE
64 | [$1.to_i, $2.to_i, $3.to_i]
65 | end
66 |
67 | # Convert a RFC 2425 date into a Date object.
68 | def self.decode_date_to_date(v)
69 | Date.new(*decode_date(v))
70 | end
71 |
72 | # Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445
73 | # does not. I choose to encode to the subset that is valid for both.
74 |
75 | # Encode a Date object as "yyyymmdd".
76 | def self.encode_date(d) # :nodoc:
77 | "%0.4d%0.2d%0.2d" % [d.year, d.mon, d.day]
78 | end
79 |
80 | # Encode a Date object as "yyyymmdd".
81 | def self.encode_time(d) # :nodoc:
82 | "%0.4d%0.2d%0.2d" % [d.year, d.mon, d.day]
83 | end
84 |
85 | # Encode a Time or DateTime object as "yyyymmddThhmmss"
86 | def self.encode_date_time(d) # :nodoc:
87 | "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [d.year, d.mon, d.day, d.hour, d.min, d.sec]
88 | end
89 |
90 | # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
91 | def self.decode_time(v) # :nodoc:
92 | raise ::Vcard::InvalidEncodingError, "time '#{v}' not valid" unless match = Bnf::TIME.match(v)
93 | hour, min, sec, secfrac, tz = match.to_a[1..5]
94 |
95 | [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
96 | end
97 |
98 | def self.array_datetime_to_time(dtarray) #:nodoc:
99 | # We get [year, month, day, hour, min, sec, usec, tz]
100 | tz = (dtarray.pop == "Z") ? :gm : :local
101 | Time.send(tz, *dtarray)
102 | rescue ArgumentError => e
103 | raise ::Vcard::InvalidEncodingError, "#{tz} #{e} (#{dtarray.join(', ')})"
104 | end
105 |
106 | # Convert a RFC 2425 time into an array of Time objects.
107 | def self.decode_time_to_time(v) # :nodoc:
108 | array_datetime_to_time(decode_date_time(v))
109 | end
110 |
111 | # Convert a RFC 2425 date-time into an array of [year,mon,day,hour,min,sec,secfrac,timezone]
112 | def self.decode_date_time(v) # :nodoc:
113 | raise ::Vcard::InvalidEncodingError, "date-time '#{v}' not valid" unless match = Bnf::DATE_TIME.match(v)
114 | year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
115 |
116 | [
117 | # date
118 | year.to_i, month.to_i, day.to_i,
119 | # time
120 | hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
121 | ]
122 | end
123 |
124 | def self.decode_date_time_to_datetime(v) #:nodoc:
125 | year, month, day, hour, min, sec = decode_date_time(v)
126 | # TODO - DateTime understands timezones, so we could decode tz and use it.
127 | DateTime.civil(year, month, day, hour, min, sec, 0)
128 | end
129 |
130 | # decode_boolean
131 | #
132 | # float
133 | #
134 | # float_list
135 |
136 | # Convert an RFC2425 INTEGER value into an Integer
137 | def self.decode_integer(v) # :nodoc:
138 | raise ::Vcard::InvalidEncodingError, "integer not valid (#{v})" unless v =~ Bnf::INTEGER
139 | v.to_i
140 | end
141 |
142 | #
143 | # integer_list
144 |
145 | # Convert a RFC2425 date-list into an array of dates.
146 | def self.decode_date_list(v) # :nodoc:
147 | decode_list(v) do |date|
148 | date.strip!
149 | decode_date(date) if date.length > 0
150 | end.compact
151 | end
152 |
153 | # Convert a RFC 2425 time-list into an array of times.
154 | def self.decode_time_list(v) # :nodoc:
155 | decode_list(v) do |time|
156 | time.strip!
157 | decode_time(time) if time.length > 0
158 | end.compact
159 | end
160 |
161 | # Convert a RFC 2425 date-time-list into an array of date-times.
162 | def self.decode_date_time_list(v) # :nodoc:
163 | decode_list(v) do |datetime|
164 | datetime.strip!
165 | decode_date_time(datetime) if datetime.length > 0
166 | end.compact
167 | end
168 |
169 | # Convert RFC 2425 text into a String.
170 | # \\ -> \
171 | # \n -> NL
172 | # \N -> NL
173 | # \, -> ,
174 | # \; -> ;
175 | #
176 | # I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed
177 | # to escape anything but the above, everything else is ambiguous, so I'll
178 | # just support it.
179 | def self.decode_text(v) # :nodoc:
180 | # FIXME - I think this should trim leading and trailing space
181 | v.gsub(/\\(.)/) do
182 | case $1
183 | when "n", "N"
184 | "\n"
185 | else
186 | $1
187 | end
188 | end
189 | end
190 |
191 | def self.encode_text(v) #:nodoc:
192 | v.to_str.gsub(/[\\,;]/, '\\\\\0').gsub(/\r?\n/, "\\n")
193 | end
194 |
195 | # v is an Array of String, or just a single String
196 | def self.encode_text_list(v, sep = ",") #:nodoc:
197 | v.to_ary.map { |t| encode_text(t) }.join(sep)
198 | rescue
199 | encode_text(v)
200 | end
201 |
202 | # Convert a +sep+-seperated list of TEXT values into an array of values.
203 | def self.decode_text_list(value, sep = ",") # :nodoc:
204 | # Need to do in two stages, as best I can find.
205 | list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map { |v| decode_text(v.first) }
206 | list << $1 if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/)
207 | list
208 | end
209 |
210 | # param-value = paramtext / quoted-string
211 | # paramtext = *SAFE-CHAR
212 | # quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
213 | def self.encode_paramtext(value)
214 | if value =~ Bnf::ALL_SAFECHARS
215 | value
216 | else
217 | raise ::Vcard::Unencodable, "paramtext #{value.inspect}"
218 | end
219 | end
220 |
221 | def self.encode_paramvalue(value)
222 | if value =~ Bnf::ALL_SAFECHARS
223 | value
224 | elsif value =~ Bnf::ALL_QSAFECHARS
225 | %Q{"#{value}"}
226 | else
227 | raise ::Vcard::Unencodable, "param-value #{value.inspect}"
228 | end
229 | end
230 |
231 | # Unfold the lines in +card+, then return an array of one Field object per
232 | # line.
233 | def self.decode(card) #:nodoc:
234 | unfold(card).map { |line| DirectoryInfo::Field.decode(line) }
235 | end
236 |
237 |
238 | # Expand an array of fields into its syntactic entities. Each entity is a sequence
239 | # of fields where the sequences is delimited by a BEGIN/END field. Since
240 | # BEGIN/END delimited entities can be nested, we build a tree. Each entry in
241 | # the array is either a Field or an array of entries (where each entry is
242 | # either a Field, or an array of entries...).
243 | def self.expand(src) #:nodoc:
244 | # output array to expand the src to
245 | dst = []
246 | # stack used to track our nesting level, as we see begin/end we start a
247 | # new/finish the current entity, and push/pop that entity from the stack
248 | current = [dst]
249 |
250 | for f in src
251 | if f.name? "BEGIN"
252 | e = [f]
253 |
254 | current.last.push(e)
255 | current.push(e)
256 | elsif f.name? "END"
257 | current.last.push(f)
258 |
259 | unless current.last.first.value? current.last.last.value
260 | raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
261 | end
262 |
263 | current.pop
264 | else
265 | current.last.push(f)
266 | end
267 | end
268 |
269 | dst
270 | end
271 |
272 | # Split an array into an array of all the fields at the outer level, and
273 | # an array of all the inner arrays of fields. Return the array [outer,
274 | # inner].
275 | def self.outer_inner(fields) #:nodoc:
276 | # TODO - use Enumerable#partition
277 | # seperate into the outer-level fields, and the arrays of component
278 | # fields
279 | outer = []
280 | inner = []
281 | fields.each do |line|
282 | case line
283 | when Array then inner << line
284 | else outer << line
285 | end
286 | end
287 | return outer, inner
288 | end
289 |
290 | def self.configuration
291 | @configuration ||= Configuration.new
292 | end
293 |
294 | def self.configure
295 | yield configuration
296 | end
297 |
298 | end
299 |
--------------------------------------------------------------------------------
/lib/vcard/attachment.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the same terms as the ruby language itself.
5 |
6 | module Vcard
7 |
8 | # Attachments are used by both iCalendar and vCard. They are either a URI or
9 | # inline data, and their decoded value will be either a Uri or a Inline, as
10 | # appropriate.
11 | #
12 | # Besides the methods specific to their class, both kinds of object implement
13 | # a set of common methods, allowing them to be treated uniformly:
14 | # - Uri#to_io, Inline#to_io: return an IO from which the value can be read.
15 | # - Uri#to_s, Inline#to_s: return the value as a String.
16 | # - Uri#format, Inline#format: the format of the value. This is supposed to
17 | # be an "iana defined" identifier (like "image/jpeg"), but could be almost
18 | # anything (or nothing) in practice. Since the parameter is optional, it may
19 | # be "".
20 | #
21 | # The objects can also be distinguished by their class, if necessary.
22 | module Attachment
23 |
24 | # TODO - It might be possible to autodetect the format from the first few
25 | # bytes of the value, and return the appropriate MIME type when format
26 | # isn't defined.
27 | #
28 | # iCalendar and vCard put the format in different parameters, and the
29 | # default kind of value is different.
30 | def Attachment.decode(field, defkind, fmtparam) #:nodoc:
31 | format = field.pvalue(fmtparam) || ""
32 | kind = field.kind || defkind
33 | case kind
34 | when "text"
35 | Inline.new(::Vcard.decode_text(field.value), format)
36 | when "uri"
37 | Uri.new(field.value_raw, format)
38 | when "binary"
39 | Inline.new(field.value, format)
40 | else
41 | raise ::Vcard::InvalidEncodingError, "Attachment of type #{kind} is not allowed"
42 | end
43 | end
44 |
45 | # Extends a String to support some of the same methods as Uri.
46 | class Inline < String
47 | def initialize(s, format) #:nodoc:
48 | @format = format
49 | super(s)
50 | end
51 |
52 | # Return an IO object for the inline data. See +stringio+ for more
53 | # information.
54 | def to_io
55 | StringIO.new(self)
56 | end
57 |
58 | # The format of the inline data.
59 | # See Attachment.
60 | attr_reader :format
61 | end
62 |
63 | # Encapsulates a URI and implements some methods of String.
64 | class Uri
65 | def initialize(uri, format) #:nodoc:
66 | @uri = uri
67 | @format = format
68 | end
69 |
70 | # The URI value.
71 | attr_reader :uri
72 |
73 | # The format of the data referred to by the URI.
74 | # See Attachment.
75 | attr_reader :format
76 |
77 | # Return an IO object from opening the URI. See +open-uri+ for more
78 | # information.
79 | def to_io
80 | open(@uri)
81 | end
82 |
83 | # Return the String from reading the IO object to end-of-data.
84 | def to_s
85 | to_io.read(nil)
86 | end
87 |
88 | def inspect #:nodoc:
89 | s = "<#{self.class.to_s}: #{uri.inspect}>"
90 | s << ", #{@format.inspect}" if @format
91 | s
92 | end
93 | end
94 |
95 | end
96 | end
97 |
98 |
--------------------------------------------------------------------------------
/lib/vcard/bnf.rb:
--------------------------------------------------------------------------------
1 | # encoding: ascii
2 | # Copyright (C) 2008 Sam Roberts
3 |
4 | # This library is free software; you can redistribute it and/or modify
5 | # it under the same terms as the ruby language itself.
6 |
7 | module Vcard
8 | # Contains regular expressions for the EBNF of RFC 2425.
9 | module Bnf #:nodoc:
10 |
11 | # 1*(ALPHA / DIGIT / "-")
12 | # Note: "_" allowed because produced by Notes (X-LOTUS-CHILD_UID:)
13 | # Note: "/" allowed because produced by KAddressBook (X-messaging/xmpp-All:)
14 | # Note: " " allowed because produced by highrisehq.com (X-GOOGLE TALK:)
15 | NAME = /[\w\/-][ \w\/-]*/
16 |
17 | # <"> <">
18 | QSTR = /"([^"]*)"/
19 |
20 | # *
21 | PTEXT = /([^";:,]+)/
22 |
23 | # param-value = ptext / quoted-string
24 | PVALUE = /(?:#{QSTR}|#{PTEXT})/
25 |
26 | # param = name "=" param-value *("," param-value)
27 | # Note: v2.1 allows a type or encoding param-value to appear without the type=
28 | # or the encoding=. This is hideous, but we try and support it, if there
29 | # is no "=", then $2 will be "", and we will treat it as a v2.1 param.
30 | PARAM = /;(#{NAME})(=?)((?:#{PVALUE})?(?:,#{PVALUE})*)/
31 |
32 | # V3.0: contentline = [group "."] name *(";" param) ":" value
33 | # V2.1: contentline = *( group "." ) name *(";" param) ":" value
34 | # We accept the V2.1 syntax for backwards compatibility.
35 | LINE = /\A((?:#{NAME}\.)*)?(#{NAME})((?:#{PARAM})*):(.*)\z/
36 |
37 | # date = date-fullyear ["-"] date-month ["-"] date-mday
38 | # date-fullyear = 4 DIGIT
39 | # date-month = 2 DIGIT
40 | # date-mday = 2 DIGIT
41 | DATE_PARTIAL = /(\d\d\d\d)-?(\d\d)-?(\d\d)/
42 | DATE = /\A\s*#{DATE_PARTIAL}\s*\z/
43 |
44 | # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
45 | # time-hour = 2 DIGIT
46 | # time-minute = 2 DIGIT
47 | # time-second = 2 DIGIT
48 | # time-secfrac = "," 1*DIGIT
49 | # time-zone = "Z" / time-numzone
50 | # time-numzone = sign time-hour [":"] time-minute
51 | TIME_PARTIAL = /(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?/
52 | TIME = /\A\s*#{TIME_PARTIAL}\s*\z/
53 |
54 | # date-time = date "T" time
55 | DATE_TIME = /\A\s*#{DATE_PARTIAL}T#{TIME_PARTIAL}\s*\z/
56 |
57 | # integer = (["+"] / "-") 1*DIGIT
58 | INTEGER = /\A\s*[-+]?\d+\s*\z/
59 |
60 | # QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII
61 | # ; Any character except CTLs and DQUOTE
62 | # set ascii encoding so that multibyte chars can be properly escaped
63 | if RUBY_PLATFORM == "java" && RUBY_VERSION < "1.9"
64 | # JRuby in 1.8 mode doesn't respect the file encoding. See https://github.com/jruby/jruby/issues/1191
65 | QSAFECHAR = /[ \t\x21\x23-\x7e\x80-\xff]/
66 | else
67 | QSAFECHAR = Regexp.new("[ \t\x21\x23-\x7e\x80-\xff]")
68 | end
69 | ALL_QSAFECHARS = /\A#{QSAFECHAR}*\z/
70 |
71 | # SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII
72 | # ; Any character except CTLs, DQUOTE, ";", ":", ","
73 | # escape character classes then create new Regexp
74 | SAFECHAR = Regexp.new(Regexp.escape("[ \t\x21\x23-\x2b\x2d-\x39\x3c-\x7e\x80-\xff]"))
75 | ALL_SAFECHARS = /\A#{SAFECHAR}*\z/
76 |
77 | # A quoted-printable encoded string with a trailing '=', indicating that it's not terminated
78 | UNTERMINATED_QUOTED_PRINTABLE = /ENCODING=QUOTED-PRINTABLE:.*=$/
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/vcard/configuration.rb:
--------------------------------------------------------------------------------
1 | module Vcard
2 | class Configuration
3 |
4 | attr_accessor :raise_on_invalid_line
5 | alias_method :raise_on_invalid_line?, :raise_on_invalid_line
6 |
7 | attr_accessor :ignore_invalid_vcards
8 | alias_method :ignore_invalid_vcards?, :ignore_invalid_vcards
9 |
10 | def initialize
11 | set_default_values
12 | end
13 |
14 | def reset
15 | set_default_values
16 | end
17 |
18 | private
19 |
20 | def set_default_values
21 | @raise_on_invalid_line = true
22 | @ignore_invalid_vcards = true
23 | end
24 |
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/vcard/dirinfo.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the same terms as the ruby language itself.
5 |
6 | module Vcard
7 | # An RFC 2425 directory info object.
8 | #
9 | # A directory information object is a sequence of fields. The basic
10 | # structure of the object, and the way in which it is broken into fields
11 | # is common to all profiles of the directory info type.
12 | #
13 | # A vCard, for example, is a specialization of a directory info object.
14 | #
15 | # - [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
16 | #
17 | # Here's an example of encoding a simple vCard using the low-level APIs:
18 | #
19 | # card = Vcard::Vcard.create
20 | # card << Vcard::DirectoryInfo::Field.create("EMAIL", "user.name@example.com", "TYPE" => "INTERNET" )
21 | # card << Vcard::DirectoryInfo::Field.create("URL", "http://www.example.com/user" )
22 | # card << Vcard::DirectoryInfo::Field.create("FN", "User Name" )
23 | # puts card.to_s
24 | #
25 | # Don't do it like that, use Vcard::Vcard::Maker.
26 | class DirectoryInfo
27 | include Enumerable
28 |
29 | private_class_method :new
30 |
31 | # Initialize a DirectoryInfo object from +fields+. If +profile+ is
32 | # specified, check the BEGIN/END fields.
33 | def initialize(fields, profile = nil) #:nodoc:
34 | @valid = true
35 | @fields = []
36 |
37 | fields.each do |f|
38 | raise ArgumentError, "fields must be an array of DirectoryInfo::Field objects" unless f.kind_of? DirectoryInfo::Field
39 | if f.valid?
40 | @fields << f
41 | else
42 | @valid = false
43 | end
44 | end
45 |
46 | @string = nil # this is used as a flag to indicate that recoding will be necessary
47 |
48 | check_begin_end(profile) if profile
49 | end
50 |
51 | def valid?
52 | @valid
53 | end
54 |
55 | # Decode +card+ into a DirectoryInfo object.
56 | #
57 | # +card+ may either be a something that is convertible to a string using
58 | # #to_str or an Array of objects that can be joined into a string using
59 | # #join("\n"), or an IO object (which will be read to end-of-file).
60 | #
61 | # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions.
62 | #
63 | # A DirectoryInfo is mutable, you can add new fields to it, see
64 | # Vcard::DirectoryInfo::Field#create() for how to create a new Field.
65 | #
66 | # TODO: I don't believe this is ever used, maybe I can remove it.
67 | def DirectoryInfo.decode(card) #:nodoc:
68 | if card.respond_to? :to_str
69 | string = card.to_str
70 | elsif card.kind_of? Array
71 | string = card.join("\n")
72 | elsif card.kind_of? IO
73 | string = card.read(nil)
74 | else
75 | raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}"
76 | end
77 |
78 | fields = ::Vcard.decode(string)
79 |
80 | new(fields)
81 | end
82 |
83 | # Create a new DirectoryInfo object. The +fields+ are an optional array of
84 | # DirectoryInfo::Field objects to add to the new object, between the
85 | # BEGIN/END. If the +profile+ string is not nil, then it is the name of
86 | # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields
87 | # will be added.
88 | #
89 | # A DirectoryInfo is mutable, you can add new fields to it using #push(),
90 | # and see Field#create().
91 | def DirectoryInfo.create(fields = [], profile = nil)
92 |
93 | if profile
94 | p = profile.to_str
95 | fields = [
96 | Field.create("BEGIN", p),
97 | *fields,
98 | Field.create("END", p)
99 | ]
100 | end
101 |
102 | new(fields, profile)
103 | end
104 |
105 | # The first field named +name+, or nil if no
106 | # match is found.
107 | def field(name)
108 | enum_by_name(name).each { |f| return f }
109 | nil
110 | end
111 |
112 | # The value of the first field named +name+, or nil if no
113 | # match is found.
114 | def [](name)
115 | enum_by_name(name).each { |f| return f.value if f.value != ""}
116 | enum_by_name(name).each { |f| return f.value }
117 | nil
118 | end
119 |
120 | # An array of all the values of fields named +name+, converted to text
121 | # (using Field#to_text()).
122 | #
123 | # TODO - call this #texts(), as in the plural?
124 | def text(name)
125 | accum = []
126 | each do |f|
127 | if f.name? name
128 | accum << f.to_text
129 | end
130 | end
131 | accum
132 | end
133 |
134 | # Array of all the Field#group()s.
135 | def groups
136 | @fields.collect { |f| f.group } .compact.uniq
137 | end
138 |
139 | # All fields, frozen.
140 | def fields #:nodoc:
141 | @fields.dup.freeze
142 | end
143 |
144 | # Yields for each Field for which +cond+.call(field) is true. The
145 | # (default) +cond+ of nil is considered true for all fields, so
146 | # this acts like a normal #each() when called with no arguments.
147 | def each(cond = nil) # :yields: Field
148 | @fields.each do |field|
149 | if(cond == nil || cond.call(field))
150 | yield field
151 | end
152 | end
153 | self
154 | end
155 |
156 | # Returns an Enumerator for each Field for which #name?(+name+) is true.
157 | #
158 | # An Enumerator supports all the methods of Enumerable, so it allows iteration,
159 | # collection, mapping, etc.
160 | #
161 | # Examples:
162 | #
163 | # Print all the nicknames in a card:
164 | #
165 | # card.enum_by_name("NICKNAME") { |f| puts f.value }
166 | #
167 | # Print an Array of the preferred email addresses in the card:
168 | #
169 | # pref_emails = card.enum_by_name("EMAIL").select { |f| f.pref? }
170 | def enum_by_name(name)
171 | Enumerator.new(self, Proc.new { |field| field.name?(name) })
172 | end
173 |
174 | # Returns an Enumerator for each Field for which #group?(+group+) is true.
175 | #
176 | # For example, to print all the fields, sorted by group, you could do:
177 | #
178 | # card.groups.sort.each do |group|
179 | # card.enum_by_group(group).each do |field|
180 | # puts "#{group} -> #{field.name}"
181 | # end
182 | # end
183 | #
184 | # or to get an array of all the fields in group "AGROUP", you could do:
185 | #
186 | # card.enum_by_group("AGROUP").to_a
187 | def enum_by_group(group)
188 | Enumerator.new(self, Proc.new { |field| field.group?(group) })
189 | end
190 |
191 | # Returns an Enumerator for each Field for which +cond+.call(field) is true.
192 | def enum_by_cond(cond)
193 | Enumerator.new(self, cond )
194 | end
195 |
196 | # Obsoleted; force card to be reencoded from the fields.
197 | def dirty #:nodoc:
198 | end
199 |
200 | # Append +field+ to the fields. Note that it won't be literally appended
201 | # to the fields, it will be inserted before the closing END field.
202 | def push(field)
203 | dirty
204 | @fields[-1,0] = field
205 | self
206 | end
207 |
208 | alias << push
209 |
210 | # Push +field+ onto the fields, unless there is already a field
211 | # with this name.
212 | def push_unique(field)
213 | push(field) unless @fields.detect { |f| f.name? field.name }
214 | self
215 | end
216 |
217 | # Append +field+ to the end of all the fields. This isn't usually what you
218 | # want to do, usually a DirectoryInfo's first and last fields are a
219 | # BEGIN/END pair, see #push().
220 | def push_end(field)
221 | @fields << field
222 | self
223 | end
224 |
225 | # Delete +field+.
226 | #
227 | # Warning: You can't delete BEGIN: or END: fields, but other
228 | # profile-specific fields can be deleted, including mandatory ones. For
229 | # vCards in particular, in order to avoid destroying them, I suggest
230 | # creating a new Vcard, and copying over all the fields that you still
231 | # want, rather than using #delete. This is easy with Vcard::Maker#copy, see
232 | # the Vcard::Maker examples.
233 | def delete(field)
234 | case
235 | when field.name?("BEGIN"), field.name?("END")
236 | raise ArgumentError, "Cannot delete BEGIN or END fields."
237 | else
238 | @fields.delete field
239 | end
240 |
241 | self
242 | end
243 |
244 | LF= "\n"
245 |
246 | # The string encoding of the DirectoryInfo. See Field#encode for information
247 | # about the parameters.
248 | def encode(width = 75, nl: LF)
249 | @fields.collect { |f| f.encode(width, nl: nl) }.join
250 | end
251 |
252 | alias to_s encode
253 |
254 | # Check that the DirectoryInfo object is correctly delimited by a BEGIN
255 | # and END, that their profile values match, and if +profile+ is specified, that
256 | # they are the specified profile.
257 | def check_begin_end(profile=nil) #:nodoc:
258 | unless @fields.first
259 | raise "No fields to check"
260 | end
261 | unless @fields.first.name? "BEGIN"
262 | raise "Needs BEGIN, found: #{@fields.first.encode}"
263 | end
264 | unless @fields.last.name? "END"
265 | raise "Needs END, found: #{@fields.last.encode}"
266 | end
267 | unless @fields.last.value? @fields.first.value
268 | raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}"
269 | end
270 | if profile
271 | if ! @fields.first.value? profile
272 | raise "Mismatched profile"
273 | end
274 | end
275 | true
276 | end
277 | end
278 | end
279 |
280 |
--------------------------------------------------------------------------------
/lib/vcard/enumerator.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the same terms as the ruby language itself.
5 |
6 | module Vcard
7 | # This is a way for an object to have multiple ways of being enumerated via
8 | # argument to it's #each() method. An Enumerator mixes in Enumerable, so the
9 | # standard APIs such as Enumerable#map(), Enumerable#to_a(), and
10 | # Enumerable#find_all() can be used on it.
11 | #
12 | # TODO since 1.8, this is part of the standard library, I should rewrite vPim
13 | # so this can be removed.
14 | class Enumerator
15 | include Enumerable
16 |
17 | def initialize(obj, *args)
18 | @obj = obj
19 | @args = args
20 | end
21 |
22 | def each(&block)
23 | @obj.each(*@args, &block)
24 | end
25 | end
26 | end
27 |
28 |
--------------------------------------------------------------------------------
/lib/vcard/errors.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the same terms as the ruby language itself.
5 |
6 | module Vcard
7 | # Exception used to indicate that data being decoded is invalid, the message
8 | # should describe what is invalid.
9 | class InvalidEncodingError < StandardError; end
10 |
11 | # Exception used to indicate that data being decoded is unsupported, the message
12 | # should describe what is unsupported.
13 | #
14 | # If its unsupported, its likely because I didn't anticipate it being useful
15 | # to support this, and it likely it could be supported on request.
16 | class UnsupportedError < StandardError; end
17 |
18 | # Exception used to indicate that encoding failed, probably because the
19 | # object would not result in validly encoded data. The message should
20 | # describe what is unsupported.
21 | class Unencodeable < StandardError; end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/vcard/field.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify it
4 | # under the same terms as the ruby language itself.
5 |
6 | module Vcard
7 |
8 | class DirectoryInfo
9 |
10 | # A field in a directory info object.
11 | class Field
12 | # TODO
13 | # - Field should know which param values and field values are
14 | # case-insensitive, configurably, so it can down case them
15 | # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or
16 | # pvalue_iset/idel/iadd, where set sets them all, add adds if not present,
17 | # and del deletes any that are present
18 | # - I really, really, need a case-insensitive string...
19 | # - should allow nil as a field value, its not the same as "", if there is
20 | # more than one pvalue, the empty string will show up. This isn't strictly
21 | # disallowed, but its odd. Should also strip empty strings on decoding, if
22 | # I don't already.
23 | private_class_method :new
24 |
25 | def Field.create_array(fields)
26 | case fields
27 | when Hash
28 | fields.map do |name,value|
29 | DirectoryInfo::Field.create( name, value )
30 | end
31 | else
32 | fields.to_ary
33 | end
34 | end
35 |
36 | # Encode a field.
37 | def Field.encode0(group, name, params={}, value="") # :nodoc:
38 | line = ""
39 |
40 | # A reminder of the line format:
41 | # [.];=,:
42 |
43 | if group
44 | if group.class == Symbol
45 | # Explicitly allow symbols
46 | group = group.to_s
47 | end
48 | line << group.to_str << "."
49 | end
50 |
51 | line << name
52 |
53 | params.each do |pname, pvalues|
54 |
55 | unless pvalues.respond_to? :to_ary
56 | pvalues = [ pvalues ]
57 | end
58 |
59 | line << ";" << pname << "="
60 |
61 | sep = "" # set to "," after one pvalue has been appended
62 |
63 | pvalues.each do |pvalue|
64 | # check if we need to do any encoding
65 | if pname.casecmp("ENCODING") == 0 && pvalue == :b64
66 | pvalue = "B" # the RFC definition of the base64 param value
67 | value = [ value.to_str ].pack("m").gsub("\n", "")
68 | end
69 |
70 | line << sep << pvalue
71 | sep =",";
72 | end
73 | end
74 |
75 | line << ":"
76 |
77 | line << Field.value_str(value)
78 |
79 | line
80 | end
81 |
82 | def Field.value_str(value) # :nodoc:
83 | line = ""
84 | case value
85 | when Date
86 | line << ::Vcard.encode_date(value)
87 |
88 | when Time #, DateTime
89 | line << ::Vcard.encode_date_time(value)
90 |
91 | when Array
92 | line << value.map { |v| Field.value_str(v) }.join(";")
93 |
94 | when Symbol
95 | line << value.to_s
96 |
97 | else
98 | # FIXME - somewhere along here, values with special chars need escaping...
99 | line << value.to_str
100 | end
101 | line
102 | end
103 |
104 |
105 | # Decode a field.
106 | def Field.decode0(atline) # :nodoc:
107 | if !(atline =~ Bnf::LINE)
108 | raise(::Vcard::InvalidEncodingError, atline) if ::Vcard.configuration.raise_on_invalid_line?
109 | return false
110 | end
111 |
112 | atgroup = $1.upcase
113 | atname = $2.upcase
114 | paramslist = $3
115 | atvalue = $~[-1]
116 |
117 | # I've seen space that shouldn't be there, as in "BEGIN:VCARD ", so
118 | # strip it. I'm not absolutely sure this is allowed... it certainly
119 | # breaks round-trip encoding.
120 | atvalue.strip!
121 |
122 | if atgroup.length > 0
123 | atgroup.chomp!(".")
124 | else
125 | atgroup = nil
126 | end
127 |
128 | atparams = {}
129 |
130 | # Collect the params, if any.
131 | if paramslist.size > 1
132 |
133 | # v3.0 and v2.1 params
134 | paramslist.scan( Bnf::PARAM ) do
135 |
136 | # param names are case-insensitive, and multi-valued
137 | name = $1.upcase
138 | params = $3
139 |
140 | # v2.1 params have no "=" sign, figure out what kind of param it
141 | # is (either its a known encoding, or we treat it as a "TYPE"
142 | # param).
143 |
144 | if $2 == ""
145 | params = $1
146 | case $1
147 | when /quoted-printable/i
148 | name = "ENCODING"
149 |
150 | when /base64/i
151 | name = "ENCODING"
152 |
153 | else
154 | name = "TYPE"
155 | end
156 | end
157 |
158 | # TODO - In ruby1.8 I can give an initial value to the atparams
159 | # hash values instead of this.
160 | unless atparams.key? name
161 | atparams[name] = []
162 | end
163 |
164 | params.scan( Bnf::PVALUE ) do
165 | atparams[name] << ($1 || $2)
166 | end
167 | end
168 | end
169 |
170 | [ true, atgroup, atname, atparams, atvalue ]
171 | end
172 |
173 | def initialize(line) # :nodoc:
174 | @line = line.to_str
175 | @valid, @group, @name, @params, @value = Field.decode0(@line)
176 |
177 | if valid?
178 | @params.each do |pname,pvalues|
179 | pvalues.freeze
180 | end
181 | else
182 | @group = @name = ''
183 | end
184 | self
185 | end
186 |
187 | def valid?
188 | @valid
189 | end
190 |
191 | # Create a field by decoding +line+, a String which must already be
192 | # unfolded. Decoded fields are frozen, but see #copy().
193 | def Field.decode(line)
194 | new(line).freeze
195 | end
196 |
197 | # Create a field with name +name+ (a String), value +value+ (see below),
198 | # and optional parameters, +params+. +params+ is a hash of the parameter
199 | # name (a String) to either a single string or symbol, or an array of
200 | # strings and symbols (parameters can be multi-valued).
201 | #
202 | # If "ENCODING" => :b64 is specified as a parameter, the value will be
203 | # base-64 encoded. If it's already base-64 encoded, then use String
204 | # values ("ENCODING" => "B"), and no further encoding will be done by
205 | # this routine.
206 | #
207 | # Currently handled value types are:
208 | # - Time, encoded as a date-time value
209 | # - Date, encoded as a date value
210 | # - String, encoded directly
211 | # - Array of String, concatentated with ";" between them.
212 | #
213 | # TODO - need a way to encode String values as TEXT, at least optionally,
214 | # so as to escape special chars, etc.
215 | def Field.create(name, value="", params={})
216 | line = Field.encode0(nil, name, params, value)
217 |
218 | begin
219 | new(line)
220 | rescue ::Vcard::InvalidEncodingError => e
221 | raise ArgumentError, e.to_s
222 | end
223 | end
224 |
225 | # Create a copy of Field. If the original Field was frozen, this one
226 | # won't be.
227 | def copy
228 | Marshal.load(Marshal.dump(self))
229 | end
230 |
231 | LF = "\n"
232 |
233 | # The String encoding of the Field. The String will be wrapped
234 | # to a maximum line width of +width+, where +0+ means no
235 | # wrapping, and omitting it is to accept the default wrapping
236 | # (75, recommended by RFC2425).
237 | #
238 | # The +nl+ parameter specifies the line delimiter, which is
239 | # defaulted to LF ("\n") for historical reasons. Relevant RFC's
240 | # all say it should be CRLF, so it is highly recommended that
241 | # you specify "\r\n" if you care about maximizing
242 | # interoperability and interchangeability.
243 | #
244 | # Note: AddressBook.app 3.0.3 neither understands to unwrap lines when it
245 | # imports vCards (it treats them as raw new-line characters), nor wraps
246 | # long lines on export. This is mostly a cosmetic problem, but wrapping
247 | # can be disabled by setting width to +0+, if desired.
248 | #
249 | # FIXME - breaks round-trip encoding, need to change this to not wrap
250 | # fields that are already wrapped.
251 | def encode(width = 75, nl: LF)
252 | l = @line.rstrip
253 | if width.zero?
254 | l + nl
255 | elsif width <= 1
256 | raise ArgumentError, "#{width} is too narrow"
257 | else
258 | # Wrap to width
259 | l.scan(/\A.{,#{width}}|.{1,#{width - 1}}/).join("#{nl} ") + nl
260 | end
261 | end
262 |
263 | alias to_s encode
264 |
265 | # The name.
266 | def name
267 | @name
268 | end
269 |
270 | # The group, if present, or nil if not present.
271 | def group
272 | @group
273 | end
274 |
275 | # An Array of all the param names.
276 | def pnames
277 | @params.keys
278 | end
279 |
280 | # FIXME - remove my own uses of #params
281 | alias params pnames # :nodoc:
282 |
283 | # The first value of the param +name+, nil if there is no such param,
284 | # the param has no value, or the first param value is zero-length.
285 | def pvalue(name)
286 | v = pvalues( name )
287 | if v
288 | v = v.first
289 | end
290 | if v
291 | v = nil unless v.length > 0
292 | end
293 | v
294 | end
295 |
296 | # The Array of all values of the param +name+, nil if there is no such
297 | # param, [] if the param has no values. If the Field isn't frozen, the
298 | # Array is mutable.
299 | def pvalues(name)
300 | @params[name.upcase]
301 | end
302 |
303 | # FIXME - remove my own uses of #param
304 | alias param pvalues # :nodoc:
305 |
306 | alias [] pvalues
307 |
308 | # Yield once for each param, +name+ is the parameter name, +value+ is an
309 | # array of the parameter values.
310 | def each_param(&block) #:yield: name, value
311 | if @params
312 | @params.each(&block)
313 | end
314 | end
315 |
316 | # The decoded value.
317 | #
318 | # The encoding specified by the #encoding, if any, is stripped.
319 | #
320 | # Note: Both the RFC 2425 encoding param ("b", meaning base-64) and the
321 | # vCard 2.1 encoding params ("base64", "quoted-printable", "8bit", and
322 | # "7bit") are supported.
323 | #
324 | # FIXME:
325 | # - should use the VALUE parameter
326 | # - should also take a default value type, so it can be converted
327 | # if VALUE parameter is not present.
328 | def value
329 | case encoding
330 | when nil, "8BIT", "7BIT" then @value
331 |
332 | # Hack - if the base64 lines started with 2 SPC chars, which is invalid,
333 | # there will be extra spaces in @value. Since no SPC chars show up in
334 | # b64 encodings, they can be safely stripped out before unpacking.
335 | when "B", "BASE64" then @value.gsub(" ", "").unpack("m*").first
336 |
337 | when "QUOTED-PRINTABLE" then @value.unpack("M*").first
338 |
339 | else
340 | raise ::Vcard::InvalidEncodingError, "unrecognized encoding (#{encoding})"
341 | end
342 | end
343 |
344 | # Is the #name of this Field +name+? Names are case insensitive.
345 | def name?(name)
346 | @name.to_s.casecmp(name) == 0
347 | end
348 |
349 | # Is the #group of this field +group+? Group names are case insensitive.
350 | # A +group+ of nil matches if the field has no group.
351 | def group?(group)
352 | @group.casecmp(group) == 0
353 | end
354 |
355 | # Is the value of this field of type +kind+? RFC2425 allows the type of
356 | # a fields value to be encoded in the VALUE parameter. Don't rely on its
357 | # presence, they aren't required, and usually aren't bothered with. In
358 | # cases where the kind of value might vary (an iCalendar DTSTART can be
359 | # either a date or a date-time, for example), you are more likely to see
360 | # the kind of value specified explicitly.
361 | #
362 | # The value types defined by RFC 2425 are:
363 | # - uri:
364 | # - text:
365 | # - date: a list of 1 or more dates
366 | # - time: a list of 1 or more times
367 | # - date-time: a list of 1 or more date-times
368 | # - integer:
369 | # - boolean:
370 | # - float:
371 | def kind?(kind)
372 | self.kind.casecmp(kind) == 0
373 | end
374 |
375 | # Is one of the values of the TYPE parameter of this field +type+? The
376 | # type parameter values are case insensitive. False if there is no TYPE
377 | # parameter.
378 | #
379 | # TYPE parameters are used for general categories, such as
380 | # distinguishing between an email address used at home or at work.
381 | def type?(type)
382 | type = type.to_str
383 |
384 | types = param("TYPE")
385 |
386 | if types
387 | types = types.detect { |t| t.casecmp(type) == 0 }
388 | end
389 | end
390 |
391 | # Is this field marked as preferred? A vCard field is preferred if
392 | # #type?("PREF"). This method is not necessarily meaningful for
393 | # non-vCard profiles.
394 | def pref?
395 | type? "PREF"
396 | end
397 |
398 | # Set whether a field is marked as preferred. See #pref?
399 | def pref=(ispref)
400 | if ispref
401 | pvalue_iadd("TYPE", "PREF")
402 | else
403 | pvalue_idel("TYPE", "PREF")
404 | end
405 | end
406 |
407 | # Is the value of this field +value+? The check is case insensitive.
408 | # FIXME - it shouldn't be insensitive, make a #casevalue? method.
409 | def value?(value)
410 | @value.casecmp(value) == 0
411 | end
412 |
413 | # The value of the ENCODING parameter, if present, or nil if not
414 | # present.
415 | def encoding
416 | e = param("ENCODING")
417 |
418 | if e
419 | if e.length > 1
420 | raise ::Vcard::InvalidEncodingError, "multi-valued param 'ENCODING' (#{e})"
421 | end
422 | e = e.first.upcase
423 | end
424 | e
425 | end
426 |
427 | # The type of the value, as specified by the VALUE parameter, nil if
428 | # unspecified.
429 | def kind
430 | v = param("VALUE")
431 | if v
432 | if v.size > 1
433 | raise ::Vcard::InvalidEncodingError, "multi-valued param 'VALUE' (#{values})"
434 | end
435 | v = v.first.downcase
436 | end
437 | v
438 | end
439 |
440 | # The value as an array of Time objects (all times and dates in
441 | # RFC2425 are lists, even where it might not make sense, such as a
442 | # birthday). The time will be UTC if marked as so (with a timezone of
443 | # "Z"), and in localtime otherwise.
444 | #
445 | # TODO - support timezone offsets
446 | #
447 | # TODO - if year is before 1970, this won't work... but some people
448 | # are generating calendars saying Canada Day started in 1753!
449 | # That's just wrong! So, what to do? I add a message
450 | # saying what the year is that breaks, so they at least know that
451 | # its ridiculous! I think I need my own DateTime variant.
452 | def to_time
453 | ::Vcard.decode_date_time_list(value).collect do |d|
454 | # We get [ year, month, day, hour, min, sec, usec, tz ]
455 | begin
456 | if(d.pop == "Z")
457 | Time.gm(*d)
458 | else
459 | Time.local(*d)
460 | end
461 | rescue ArgumentError => e
462 | raise ::Vcard::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
463 | end
464 | end
465 | rescue ::Vcard::InvalidEncodingError
466 | ::Vcard.decode_date_list(value).collect do |d|
467 | # We get [ year, month, day ]
468 | begin
469 | Time.gm(*d)
470 | rescue ArgumentError => e
471 | raise ::Vcard::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
472 | end
473 | end
474 | end
475 |
476 | # The value as an array of Date objects (all times and dates in
477 | # RFC2425 are lists, even where it might not make sense, such as a
478 | # birthday).
479 | #
480 | # The field value may be a list of either DATE or DATE-TIME values,
481 | # decoding is tried first as a DATE-TIME, then as a DATE, if neither
482 | # works an InvalidEncodingError will be raised.
483 | def to_date
484 | ::Vcard.decode_date_time_list(value).collect do |d|
485 | # We get [ year, month, day, hour, min, sec, usec, tz ]
486 | Date.new(d[0], d[1], d[2])
487 | end
488 | rescue ::Vcard::InvalidEncodingError
489 | ::Vcard.decode_date_list(value).collect do |d|
490 | # We get [ year, month, day ]
491 | Date.new(*d)
492 | end
493 | end
494 |
495 | # The value as text. Text can have escaped newlines, commas, and escape
496 | # characters, this method will strip them, if present.
497 | #
498 | # In theory, #value could also do this, but it would need to know that
499 | # the value is of type "TEXT", and often for text values the "VALUE"
500 | # parameter is not present, so knowledge of the expected type of the
501 | # field is required from the decoder.
502 | def to_text
503 | ::Vcard.decode_text(value)
504 | end
505 |
506 | # The undecoded value, see +value+.
507 | def value_raw
508 | @value
509 | end
510 |
511 | # TODO def pretty_print() ...
512 |
513 | # Set the group of this field to +group+.
514 | def group=(group)
515 | mutate(group, @name, @params, @value)
516 | group
517 | end
518 |
519 | # Set the value of this field to +value+. Valid values are as in
520 | # Field.create().
521 | def value=(value)
522 | mutate(@group, @name, @params, value)
523 | value
524 | end
525 |
526 | # Convert +value+ to text, then assign.
527 | #
528 | # TODO - unimplemented
529 | def text=(text)
530 | end
531 |
532 | # Set a the param +pname+'s value to +pvalue+, replacing any value it
533 | # currently has. See Field.create() for a description of +pvalue+.
534 | #
535 | # Example:
536 | # if field["TYPE"]
537 | # field["TYPE"] << "HOME"
538 | # else
539 | # field["TYPE"] = [ "HOME" ]
540 | # end
541 | #
542 | # TODO - this could be an alias to #pvalue_set
543 | def []=(pname,pvalue)
544 | unless pvalue.respond_to?(:to_ary)
545 | pvalue = [ pvalue ]
546 | end
547 |
548 | h = @params.dup
549 |
550 | h[pname.upcase] = pvalue
551 |
552 | mutate(@group, @name, h, @value)
553 | pvalue
554 | end
555 |
556 | # Add +pvalue+ to the param +pname+'s value. The values are treated as a
557 | # set so duplicate values won't occur, and String values are case
558 | # insensitive. See Field.create() for a description of +pvalue+.
559 | def pvalue_iadd(pname, pvalue)
560 | pname = pname.upcase
561 |
562 | # Get a uniq set, where strings are compared case-insensitively.
563 | values = [ pvalue, @params[pname] ].flatten.compact
564 | values = values.collect do |v|
565 | if v.respond_to? :to_str
566 | v = v.to_str.upcase
567 | end
568 | v
569 | end
570 | values.uniq!
571 |
572 | h = @params.dup
573 |
574 | h[pname] = values
575 |
576 | mutate(@group, @name, h, @value)
577 | values
578 | end
579 |
580 | # Delete +pvalue+ from the param +pname+'s value. The values are treated
581 | # as a set so duplicate values won't occur, and String values are case
582 | # insensitive. +pvalue+ must be a single String or Symbol.
583 | def pvalue_idel(pname, pvalue)
584 | pname = pname.upcase
585 | if pvalue.respond_to? :to_str
586 | pvalue = pvalue.to_str.downcase
587 | end
588 |
589 | # Get a uniq set, where strings are compared case-insensitively.
590 | values = [ nil, @params[pname] ].flatten.compact
591 | values = values.collect do |v|
592 | if v.respond_to? :to_str
593 | v = v.to_str.downcase
594 | end
595 | v
596 | end
597 | values.uniq!
598 | values.delete pvalue
599 |
600 | h = @params.dup
601 |
602 | h[pname] = values
603 |
604 | mutate(@group, @name, h, @value)
605 | values
606 | end
607 |
608 | # FIXME - should change this so it doesn't assign to @line here, so @line
609 | # is used to preserve original encoding. That way, #encode can only wrap
610 | # new fields, not old fields.
611 | def mutate(g, n, p, v) #:nodoc:
612 | line = Field.encode0(g, n, p, v)
613 | @valid, @group, @name, @params, @value = Field.decode0(line)
614 | @line = line
615 | self
616 | rescue ::Vcard::InvalidEncodingError => e
617 | raise ArgumentError, e.to_s
618 | end
619 |
620 | private :mutate
621 | end
622 | end
623 | end
624 |
625 |
--------------------------------------------------------------------------------
/lib/vcard/vcard.rb:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2008 Sam Roberts
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the same terms as the ruby language itself.
5 |
6 | module Vcard
7 | # A vCard, a specialization of a directory info object.
8 | #
9 | # The vCard format is specified by:
10 | # - RFC2426[http://www.ietf.org/rfc/rfc2426.txt]: vCard MIME Directory Profile (vCard 3.0)
11 | # - RFC2425[http://www.ietf.org/rfc/rfc2425.txt]: A MIME Content-Type for Directory Information
12 | #
13 | # This implements vCard 3.0, but it is also capable of working with vCard 2.1
14 | # if used with care.
15 | #
16 | # All line values can be accessed with Vcard#value, Vcard#values, or even by
17 | # iterating through Vcard#lines. Line types that don't have specific support
18 | # and non-standard line types ("X-MY-SPECIAL", for example) will be returned
19 | # as a String, with any base64 or quoted-printable encoding removed.
20 | #
21 | # Specific support exists to return more useful values for the standard vCard
22 | # types, where appropriate.
23 | #
24 | # The wrapper functions (#birthday, #nicknames, #emails, etc.) exist
25 | # partially as an API convenience, and partially as a place to document
26 | # the values returned for the more complex types, like PHOTO and EMAIL.
27 | #
28 | # For types that do not sensibly occur multiple times (like BDAY or GEO),
29 | # sometimes a wrapper exists only to return a single line, using #value.
30 | # However, if you find the need, you can still call #values to get all the
31 | # lines, and both the singular and plural forms will eventually be
32 | # implemented.
33 | #
34 | # For more information see:
35 | # - RFC2426[http://www.ietf.org/rfc/rfc2426.txt]: vCard MIME Directory Profile (vCard 3.0)
36 | # - RFC2425[http://www.ietf.org/rfc/rfc2425.txt]: A MIME Content-Type for Directory Information
37 | # - vCard2.1[http://www.imc.org/pdi/pdiproddev.html]: vCard 2.1 Specifications
38 | #
39 | # vCards are usually transmitted in files with .vcf
40 | # extensions.
41 | #
42 | # = Examples
43 | #
44 | # - link:ex_mkvcard.txt: example of creating a vCard
45 | # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
46 | # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
47 | # - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards
48 | # - link:ex_get_vcard_photo.txt: pull photo data from a vCard
49 | # - link:ab-query.txt: query the OS X Address Book to find vCards
50 | # - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful
51 | # with Mutt (see link:README.mutt for details)
52 | # - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a
53 | # (small but) complete application contributed by Dane G. Avilla, thanks!
54 | # - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards
55 | # - link:vcf-dump.txt: utility for dumping contents of .vcf files
56 | class Vcard < DirectoryInfo
57 |
58 | # Represents the value of an ADR field.
59 | #
60 | # #location, #preferred, and #delivery indicate information about how the
61 | # address is to be used, the other attributes are parts of the address.
62 | #
63 | # Using values other than those defined for #location or #delivery is
64 | # unlikely to be portable, or even conformant.
65 | #
66 | # All attributes are optional. #location and #delivery can be set to arrays
67 | # of strings.
68 | class Address
69 | # post office box (String)
70 | attr_accessor :pobox
71 | # seldom used, its not clear what it is for (String)
72 | attr_accessor :extended
73 | # street address (String)
74 | attr_accessor :street
75 | # usually the city (String)
76 | attr_accessor :locality
77 | # usually the province or state (String)
78 | attr_accessor :region
79 | # postal code (String)
80 | attr_accessor :postalcode
81 | # country name (String)
82 | attr_accessor :country
83 | # home, work (Array of String): the location referred to by the address
84 | attr_accessor :location
85 | # true, false (boolean): where this is the preferred address (for this location)
86 | attr_accessor :preferred
87 | # postal, parcel, dom (domestic), intl (international) (Array of String): delivery
88 | # type of this address
89 | attr_accessor :delivery
90 |
91 | # nonstandard types, their meaning is undefined (Array of String). These
92 | # might be found during decoding, but shouldn't be set during encoding.
93 | attr_reader :nonstandard
94 |
95 | # Used to simplify some long and tedious code. These symbols are in the
96 | # order required for the ADR field structured TEXT value, the order
97 | # cannot be changed.
98 | @@adr_parts = [
99 | :@pobox,
100 | :@extended,
101 | :@street,
102 | :@locality,
103 | :@region,
104 | :@postalcode,
105 | :@country,
106 | ]
107 |
108 | # TODO
109 | # - #location?
110 | # - #delivery?
111 | def initialize #:nodoc:
112 | # TODO - Add #label to support LABEL. Try to find LABEL
113 | # in either same group, or with sam params.
114 | @@adr_parts.each do |part|
115 | instance_variable_set(part, "")
116 | end
117 |
118 | @location = []
119 | @preferred = false
120 | @delivery = []
121 | @nonstandard = []
122 | end
123 |
124 | def encode #:nodoc:
125 | parts = @@adr_parts.map do |part|
126 | instance_variable_get(part)
127 | end
128 |
129 | value = ::Vcard.encode_text_list(parts, ";")
130 |
131 | params = [ @location, @delivery, @nonstandard ]
132 | params << "pref" if @preferred
133 | params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
134 |
135 | paramshash = {}
136 |
137 | paramshash["TYPE"] = params if params.first
138 |
139 | ::Vcard::DirectoryInfo::Field.create( "ADR", value, paramshash)
140 | end
141 |
142 | def Address.decode(card, field) #:nodoc:
143 | adr = new
144 |
145 | parts = ::Vcard.decode_text_list(field.value_raw, ";")
146 |
147 | @@adr_parts.each_with_index do |part,i|
148 | adr.instance_variable_set(part, parts[i] || "")
149 | end
150 |
151 | params = field.pvalues("TYPE")
152 |
153 | if params
154 | params.each do |p|
155 | p.downcase!
156 | case p
157 | when "home", "work"
158 | adr.location << p
159 | when "postal", "parcel", "dom", "intl"
160 | adr.delivery << p
161 | when "pref"
162 | adr.preferred = true
163 | else
164 | adr.nonstandard << p
165 | end
166 | end
167 | # Strip duplicates
168 | [ adr.location, adr.delivery, adr.nonstandard ].each do |a|
169 | a.uniq!
170 | end
171 | end
172 |
173 | adr
174 | end
175 | end
176 |
177 | # Represents the value of an EMAIL field.
178 | class Email < String
179 | # true, false (boolean): whether this is the preferred email address
180 | attr_accessor :preferred
181 | # internet, x400 (String): the email address format, rarely specified
182 | # since the default is "internet"
183 | attr_accessor :format
184 | # home, work (Array of String): the location referred to by the address. The
185 | # inclusion of location parameters in a vCard seems to be non-conformant,
186 | # strictly speaking, but also seems to be widespread.
187 | attr_accessor :location
188 | # nonstandard types, their meaning is undefined (Array of String). These
189 | # might be found during decoding, but shouldn't be set during encoding.
190 | attr_reader :nonstandard
191 |
192 | def initialize(email="") #:nodoc:
193 | @preferred = false
194 | @format = "internet"
195 | @location = []
196 | @nonstandard = []
197 | super(email)
198 | end
199 |
200 | def inspect #:nodoc:
201 | s = "#<#{self.class.to_s}: #{to_str.inspect}"
202 | s << ", pref" if preferred
203 | s << ", #{format}" if format != "internet"
204 | s << ", " << @location.join(", ") if @location.first
205 | s << ", #{@nonstandard.join(", ")}" if @nonstandard.first
206 | s
207 | end
208 |
209 | def encode #:nodoc:
210 | value = to_str.strip
211 |
212 | if value.length < 1
213 | raise ::Vcard::InvalidEncodingError, "EMAIL must have a value"
214 | end
215 |
216 | params = [ @location, @nonstandard ]
217 | params << @format if @format != "internet"
218 | params << "pref" if @preferred
219 |
220 | params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
221 |
222 | paramshash = {}
223 |
224 | paramshash["TYPE"] = params if params.first
225 |
226 | ::Vcard::DirectoryInfo::Field.create("EMAIL", value, paramshash)
227 | end
228 |
229 | def Email.decode(field) #:nodoc:
230 | value = field.to_text.strip
231 |
232 | if value.length < 1
233 | raise ::Vcard::InvalidEncodingError, "EMAIL must have a value"
234 | end
235 |
236 | eml = Email.new(value)
237 |
238 | params = field.pvalues("TYPE")
239 |
240 | if params
241 | params.each do |p|
242 | p.downcase!
243 | case p
244 | when "home", "work"
245 | eml.location << p
246 | when "pref"
247 | eml.preferred = true
248 | when "x400", "internet"
249 | eml.format = p
250 | else
251 | eml.nonstandard << p
252 | end
253 | end
254 | # Strip duplicates
255 | [ eml.location, eml.nonstandard ].each do |a|
256 | a.uniq!
257 | end
258 | end
259 |
260 | eml
261 | end
262 | end
263 |
264 | # Represents the value of a TEL field.
265 | #
266 | # The value is supposed to be a "X.500 Telephone Number" according to RFC
267 | # 2426, but that standard is not freely available. Otherwise, anything that
268 | # looks like a phone number should be OK.
269 | class Telephone < String
270 | # true, false (boolean): whether this is the preferred email address
271 | attr_accessor :preferred
272 | # home, work, cell, car, pager (Array of String): the location
273 | # of the device
274 | attr_accessor :location
275 | # voice, fax, video, msg, bbs, modem, isdn, pcs (Array of String): the
276 | # capabilities of the device
277 | attr_accessor :capability
278 | # nonstandard types, their meaning is undefined (Array of String). These
279 | # might be found during decoding, but shouldn't be set during encoding.
280 | attr_reader :nonstandard
281 |
282 | def initialize(telephone="") #:nodoc:
283 | @preferred = false
284 | @location = []
285 | @capability = []
286 | @nonstandard = []
287 | super(telephone)
288 | end
289 |
290 | def inspect #:nodoc:
291 | s = "#<#{self.class.to_s}: #{to_str.inspect}"
292 | s << ", pref" if preferred
293 | s << ", " << @location.join(", ") if @location.first
294 | s << ", " << @capability.join(", ") if @capability.first
295 | s << ", #{@nonstandard.join(", ")}" if @nonstandard.first
296 | s
297 | end
298 |
299 | def encode #:nodoc:
300 | value = to_str.strip
301 |
302 | if value.length < 1
303 | raise ::Vcard::InvalidEncodingError, "TEL must have a value"
304 | end
305 |
306 | params = [ @location, @capability, @nonstandard ]
307 | params << "pref" if @preferred
308 |
309 | params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
310 |
311 | paramshash = {}
312 |
313 | paramshash["TYPE"] = params if params.first
314 |
315 | ::Vcard::DirectoryInfo::Field.create( "TEL", value, paramshash)
316 | end
317 |
318 | def Telephone.decode(field) #:nodoc:
319 | value = field.to_text.strip
320 |
321 | if value.length < 1
322 | raise ::Vcard::InvalidEncodingError, "TEL must have a value"
323 | end
324 |
325 | tel = Telephone.new(value)
326 |
327 | params = field.pvalues("TYPE")
328 |
329 | if params
330 | params.each do |p|
331 | p.downcase!
332 | case p
333 | when "home", "work", "cell", "car", "pager"
334 | tel.location << p
335 | when "voice", "fax", "video", "msg", "bbs", "modem", "isdn", "pcs"
336 | tel.capability << p
337 | when "pref"
338 | tel.preferred = true
339 | else
340 | tel.nonstandard << p
341 | end
342 | end
343 | # Strip duplicates
344 | [ tel.location, tel.capability, tel.nonstandard ].each do |a|
345 | a.uniq!
346 | end
347 | end
348 |
349 | tel
350 | end
351 | end
352 |
353 | # The name from a vCard, including all the components of the N: and FN:
354 | # fields.
355 | class Name
356 | # family name, from N
357 | attr_accessor :family
358 | # given name, from N
359 | attr_accessor :given
360 | # additional names, from N
361 | attr_accessor :additional
362 | # such as "Ms." or "Dr.", from N
363 | attr_accessor :prefix
364 | # such as "BFA", from N
365 | attr_accessor :suffix
366 | # full name, the FN field. FN is a formatted version of the N field,
367 | # intended to be in a form more aligned with the cultural conventions of
368 | # the vCard owner than +formatted+ is.
369 | attr_accessor :fullname
370 | # all the components of N formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}"
371 | attr_reader :formatted
372 |
373 | # Override the attr reader to make it dynamic
374 | remove_method :formatted
375 | def formatted #:nodoc:
376 | f = [ @prefix, @given, @additional, @family ].map{|i| i == "" ? nil : i.strip}.compact.join(" ")
377 | if @suffix != ""
378 | f << ", " << @suffix
379 | end
380 | f
381 | end
382 |
383 | def initialize(n="", fn="") #:nodoc:
384 | n = ::Vcard.decode_text_list(n, ";") do |item|
385 | item.strip
386 | end
387 |
388 | @family = n[0] || ""
389 | @given = n[1] || ""
390 | @additional = n[2] || ""
391 | @prefix = n[3] || ""
392 | @suffix = n[4] || ""
393 |
394 | # FIXME - make calls to #fullname fail if fn is nil
395 | @fullname = (fn || "").strip
396 | end
397 |
398 | def encode #:nodoc:
399 | ::Vcard::DirectoryInfo::Field.create("N", ::Vcard.encode_text_list([ @family, @given, @additional, @prefix, @suffix ].map{|n| n.strip}, ";"))
400 | end
401 |
402 | def encode_fn #:nodoc:
403 | fn = @fullname.strip
404 | if @fullname.length == 0
405 | fn = formatted
406 | end
407 | ::Vcard::DirectoryInfo::Field.create("FN", fn)
408 | end
409 | end
410 |
411 | def decode_invisible(field) #:nodoc:
412 | nil
413 | end
414 |
415 | def decode_default(field) #:nodoc:
416 | Line.new( field.group, field.name, field.value )
417 | end
418 |
419 | def decode_version(field) #:nodoc:
420 | Line.new( field.group, field.name, (field.value.to_f * 10).to_i )
421 | end
422 |
423 | def decode_text(field) #:nodoc:
424 | Line.new( field.group, field.name, ::Vcard.decode_text(field.value_raw) )
425 | end
426 |
427 | def decode_n(field) #:nodoc:
428 | Line.new( field.group, field.name, Name.new(field.value, self["FN"]).freeze )
429 | end
430 |
431 | def decode_date_or_datetime(field) #:nodoc:
432 | date = nil
433 | begin
434 | date = ::Vcard.decode_date_to_date(field.value_raw)
435 | rescue ::Vcard::InvalidEncodingError
436 | date = ::Vcard.decode_date_time_to_datetime(field.value_raw)
437 | end
438 | Line.new( field.group, field.name, date )
439 | end
440 |
441 | def decode_bday(field) #:nodoc:
442 | decode_date_or_datetime(field)
443 | rescue ::Vcard::InvalidEncodingError
444 | # Hack around BDAY dates hat are correct in the month and day, but have
445 | # some kind of garbage in the year.
446 | if field.value =~ /^\s*(\d+)-(\d+)-(\d+)\s*$/
447 | y = $1.to_i
448 | y = Time.now.year if y < 1900
449 | m = $2.to_i
450 | d = $3.to_i
451 | Line.new( field.group, field.name, Date.new(y, m, d) )
452 | else
453 | raise
454 | end
455 | end
456 |
457 | def decode_geo(field) #:nodoc:
458 | geo = ::Vcard.decode_list(field.value_raw, ";") do |item| item.to_f end
459 | Line.new( field.group, field.name, geo )
460 | end
461 |
462 | def decode_address(field) #:nodoc:
463 | Line.new( field.group, field.name, Address.decode(self, field) )
464 | end
465 |
466 | def decode_email(field) #:nodoc:
467 | Line.new( field.group, field.name, Email.decode(field) )
468 | end
469 |
470 | def decode_telephone(field) #:nodoc:
471 | Line.new( field.group, field.name, Telephone.decode(field) )
472 | end
473 |
474 | def decode_list_of_text(field) #:nodoc:
475 | Line.new(field.group, field.name, ::Vcard.decode_text_list(field.value_raw).select{|t| t.length > 0}.uniq)
476 | end
477 |
478 | def decode_structured_text(field) #:nodoc:
479 | Line.new( field.group, field.name, ::Vcard.decode_text_list(field.value_raw, ";") )
480 | end
481 |
482 | def decode_uri(field) #:nodoc:
483 | Line.new( field.group, field.name, Attachment::Uri.new(field.value, nil) )
484 | end
485 |
486 | def decode_agent(field) #:nodoc:
487 | case field.kind
488 | when "text"
489 | decode_text(field)
490 | when "uri"
491 | decode_uri(field)
492 | when "vcard", nil
493 | Line.new( field.group, field.name, ::Vcard.decode(::Vcard.decode_text(field.value_raw)).first )
494 | else
495 | raise ::Vcard::InvalidEncodingError, "AGENT type #{field.kind} is not allowed"
496 | end
497 | end
498 |
499 | def decode_attachment(field) #:nodoc:
500 | Line.new( field.group, field.name, Attachment.decode(field, "binary", "TYPE") )
501 | end
502 |
503 | @@decode = {
504 | "BEGIN" => :decode_invisible, # Don't return delimiter
505 | "END" => :decode_invisible, # Don't return delimiter
506 | "FN" => :decode_invisible, # Returned as part of N.
507 |
508 | "ADR" => :decode_address,
509 | "AGENT" => :decode_agent,
510 | "BDAY" => :decode_bday,
511 | "CATEGORIES" => :decode_list_of_text,
512 | "EMAIL" => :decode_email,
513 | "GEO" => :decode_geo,
514 | "KEY" => :decode_attachment,
515 | "LOGO" => :decode_attachment,
516 | "MAILER" => :decode_text,
517 | "N" => :decode_n,
518 | "NAME" => :decode_text,
519 | "NICKNAME" => :decode_list_of_text,
520 | "NOTE" => :decode_text,
521 | "ORG" => :decode_structured_text,
522 | "PHOTO" => :decode_attachment,
523 | "PRODID" => :decode_text,
524 | "PROFILE" => :decode_text,
525 | "REV" => :decode_date_or_datetime,
526 | "ROLE" => :decode_text,
527 | "SOUND" => :decode_attachment,
528 | "SOURCE" => :decode_text,
529 | "TEL" => :decode_telephone,
530 | "TITLE" => :decode_text,
531 | "UID" => :decode_text,
532 | "URL" => :decode_uri,
533 | "VERSION" => :decode_version
534 | }
535 |
536 | @@decode.default = :decode_default
537 |
538 | # Cache of decoded lines/fields, so we don't have to decode a field more than once.
539 | attr_reader :cache #:nodoc:
540 |
541 | # An entry in a vCard. The #value object's type varies with the kind of
542 | # line (the #name), and on how the line was encoded. The objects returned
543 | # for a specific kind of line are often extended so that they support a
544 | # common set of methods. The goal is to allow all types of objects for a
545 | # kind of line to be treated with some uniformity, but still allow specific
546 | # handling for the various value types if desired.
547 | #
548 | # See the specific methods for details.
549 | class Line
550 | attr_reader :group
551 | attr_reader :name
552 | attr_reader :value
553 |
554 | def initialize(group, name, value) #:nodoc:
555 | @group, @name, @value = (group||""), name.to_str, value
556 | end
557 |
558 | def self.decode(decode, card, field) #:nodoc:
559 | card.cache[field] || (card.cache[field] = card.send(decode[field.name], field))
560 | end
561 | end
562 |
563 | #@lines = {} FIXME - dead code
564 |
565 | # Return line for a field
566 | def f2l(field) #:nodoc:
567 | Line.decode(@@decode, self, field)
568 | rescue ::Vcard::InvalidEncodingError
569 | # Skip invalidly encoded fields.
570 | end
571 |
572 | # With no block, returns an Array of Line. If +name+ is specified, the
573 | # Array will only contain the +Line+s with that +name+. The Array may be
574 | # empty.
575 | #
576 | # If a block is given, each Line will be yielded instead of being returned
577 | # in an Array.
578 | def lines(name=nil) #:yield: Line
579 | # FIXME - this would be much easier if #lines was #each, and there was a
580 | # different #lines that returned an Enumerator that used #each
581 | unless block_given?
582 | map do |f|
583 | if( !name || f.name?(name) )
584 | f2l(f)
585 | else
586 | nil
587 | end
588 | end.compact
589 | else
590 | each do |f|
591 | if( !name || f.name?(name) )
592 | line = f2l(f)
593 | if line
594 | yield line
595 | end
596 | end
597 | end
598 | self
599 | end
600 | end
601 |
602 | private_class_method :new
603 |
604 | def initialize(fields, profile) #:nodoc:
605 | @cache = {}
606 | super(fields, profile)
607 | end
608 |
609 | # Create a vCard 3.0 object with the minimum required fields, plus any
610 | # +fields+ you want in the card (they can also be added later).
611 | def self.create(fields = [])
612 | super([Field.create("VERSION", "3.0"), *fields], "VCARD")
613 | end
614 |
615 | # Decode a collection of vCards into an array of Vcard objects.
616 | #
617 | # +card+ can be either a String or an IO object.
618 | #
619 | # Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard),
620 | # multiple vCards can be concatenated into a single directory info object.
621 | # They may or may not be related. For example, AddressBook.app (the OS X
622 | # contact manager) will export multiple selected cards in this format.
623 | #
624 | # Input data will be converted from unicode if it is detected. The heuristic
625 | # is based on the first bytes in the string:
626 | # - 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped
627 | # - 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string
628 | # is converted to UTF-8
629 | # - 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string
630 | # is converted to UTF-8
631 | # - 0x00 "B" or 0x00 "b": UTF-16 (big-endian), the string is converted to UTF-8
632 | # - "B" 0x00 or "b" 0x00: UTF-16 (little-endian), the string is converted to UTF-8
633 | #
634 | # If you know that you have only one vCard, then you can decode that
635 | # single vCard by doing something like:
636 | #
637 | # vcard = Vcard.decode(card_data).first
638 | #
639 | # Note: Should the import encoding be remembered, so that it can be reencoded in
640 | # the same format?
641 | def self.decode(card)
642 | if card.respond_to? :to_str
643 | string = card.to_str
644 | elsif card.respond_to? :read
645 | string = card.read(nil)
646 | else
647 | raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
648 | end
649 |
650 | entities = ::Vcard.expand(::Vcard.decode(string))
651 |
652 | # Since all vCards must have a begin/end, the top-level should consist
653 | # entirely of entities/arrays, even if its a single vCard.
654 | if entities.detect { |e| ! e.kind_of? Array }
655 | raise "Not a valid vCard"
656 | end
657 |
658 | vcards = []
659 |
660 | for e in entities
661 | vcard = new(e.flatten, "VCARD")
662 | vcards.push(vcard) if vcard.valid? || !::Vcard.configuration.ignore_invalid_vcards?
663 | end
664 |
665 | vcards
666 | end
667 |
668 | # The value of the field named +name+, optionally limited to fields of
669 | # type +type+. If no match is found, nil is returned, if multiple matches
670 | # are found, the first match to have one of its type values be "PREF"
671 | # (preferred) is returned, otherwise the first match is returned.
672 | #
673 | # FIXME - this will become an alias for #value.
674 | def [](name, type=nil)
675 | fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
676 |
677 | valued = fields.select { |f| f.value != "" }
678 | if valued.first
679 | fields = valued
680 | end
681 |
682 | # limit to preferred, if possible
683 | pref = fields.select { |f| f.pref? }
684 |
685 | if pref.first
686 | fields = pref
687 | end
688 |
689 | fields.first ? fields.first.value : nil
690 | end
691 |
692 | # Return the Line#value for a specific +name+, and optionally for a
693 | # specific +type+.
694 | #
695 | # If no line with the +name+ (and, optionally, +type+) exists, nil is
696 | # returned.
697 | #
698 | # If multiple lines exist, the order of preference is:
699 | # - lines with values over lines without
700 | # - lines with a type of "pref" over lines without
701 | # If multiple lines are equally preferred, then the first line will be
702 | # returned.
703 | #
704 | # This is most useful when looking for a line that can not occur multiple
705 | # times, or when the line can occur multiple times, and you want to pick
706 | # the first preferred line of a specific type. See #values if you need to
707 | # access all the lines.
708 | #
709 | # Note that the +type+ field parameter is used for different purposes by
710 | # the various kinds of vCard lines, but for the addressing lines (ADR,
711 | # LABEL, TEL, EMAIL) it is has a reasonably consistent usage. Each
712 | # addressing line can occur multiple times, and a +type+ of "pref"
713 | # indicates that a particular line is the preferred line. Other +type+
714 | # values tend to indicate some information about the location ("home",
715 | # "work", ...) or some detail about the address ("cell", "fax", "voice",
716 | # ...). See the methods for the specific types of line for information
717 | # about supported types and their meaning.
718 | def value(name, type = nil)
719 | fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
720 |
721 | valued = fields.select { |f| f.value != "" }
722 | if valued.first
723 | fields = valued
724 | end
725 |
726 | pref = fields.select { |f| f.pref? }
727 |
728 | if pref.first
729 | fields = pref
730 | end
731 |
732 | if fields.first
733 | line = begin
734 | Line.decode(@@decode, self, fields.first)
735 | rescue ::Vcard::InvalidEncodingError
736 | end
737 |
738 | if line
739 | return line.value
740 | end
741 | end
742 |
743 | nil
744 | end
745 |
746 | # A variant of #lines that only iterates over specific Line names. Since
747 | # the name is known, only the Line#value is returned or yielded.
748 | def values(name)
749 | unless block_given?
750 | lines(name).map { |line| line.value }
751 | else
752 | lines(name) { |line| yield line.value }
753 | end
754 | end
755 |
756 | # The first ADR value of type +type+, a Address. Any of the location or
757 | # delivery attributes of Address can be used as +type+. A wrapper around
758 | # #value("ADR", +type+).
759 | def address(type=nil)
760 | value("ADR", type)
761 | end
762 |
763 | # The ADR values, an array of Address. If a block is given, the values are
764 | # yielded. A wrapper around #values("ADR").
765 | def addresses #:yield:address
766 | values("ADR")
767 | end
768 |
769 | # The AGENT values. Each AGENT value is either a String, a Uri, or a Vcard.
770 | # If a block is given, the values are yielded. A wrapper around
771 | # #values("AGENT").
772 | def agents #:yield:agent
773 | values("AGENT")
774 | end
775 |
776 | # The BDAY value as either a Date or a DateTime, or nil if there is none.
777 | #
778 | # If the BDAY value is invalidly formatted, a feeble heuristic is applied
779 | # to find the month and year, and return a Date in the current year.
780 | def birthday
781 | value("BDAY")
782 | end
783 |
784 | # The CATEGORIES values, an array of String. A wrapper around
785 | # #value("CATEGORIES").
786 | def categories
787 | value("CATEGORIES")
788 | end
789 |
790 | # The first EMAIL value of type +type+, a Email. Any of the location
791 | # attributes of Email can be used as +type+. A wrapper around
792 | # #value("EMAIL", +type+).
793 | def email(type=nil)
794 | value("EMAIL", type)
795 | end
796 |
797 | # The EMAIL values, an array of Email. If a block is given, the values are
798 | # yielded. A wrapper around #values("EMAIL").
799 | def emails #:yield:email
800 | values("EMAIL")
801 | end
802 |
803 | # The GEO value, an Array of two Floats, +[ latitude, longitude]+. North
804 | # of the equator is positive latitude, east of the meridian is positive
805 | # longitude. See RFC2445 for more info, there are lots of special cases
806 | # and RFC2445"s description is more complete thant RFC2426.
807 | def geo
808 | value("GEO")
809 | end
810 |
811 | # Return an Array of KEY Line#value, or yield each Line#value if a block
812 | # is given. A wrapper around #values("KEY").
813 | #
814 | # KEY is a public key or authentication certificate associated with the
815 | # object that the vCard represents. It is not commonly used, but could
816 | # contain a X.509 or PGP certificate.
817 | #
818 | # See Attachment for a description of the value.
819 | def keys(&proc) #:yield: Line.value
820 | values("KEY", &proc)
821 | end
822 |
823 | # Return an Array of LOGO Line#value, or yield each Line#value if a block
824 | # is given. A wrapper around #values("LOGO").
825 | #
826 | # LOGO is a graphic image of a logo associated with the object the vCard
827 | # represents. Its not common, but would probably be equivalent to the logo
828 | # on a printed card.
829 | #
830 | # See Attachment for a description of the value.
831 | def logos(&proc) #:yield: Line.value
832 | values("LOGO", &proc)
833 | end
834 |
835 | ## MAILER
836 |
837 | # The N and FN as a Name object.
838 | #
839 | # N is required for a vCards, this raises InvalidEncodingError if
840 | # there is no N so it cannot return nil.
841 | def name
842 | value("N") || raise(::Vcard::InvalidEncodingError, "Missing mandatory N field")
843 | end
844 |
845 | # The first NICKNAME value, nil if there are none.
846 | def nickname
847 | v = value("NICKNAME")
848 | v = v.first if v
849 | v
850 | end
851 |
852 | # The NICKNAME values, an array of String. The array may be empty.
853 | def nicknames
854 | values("NICKNAME").flatten.uniq
855 | end
856 |
857 | # The NOTE value, a String. A wrapper around #value("NOTE").
858 | def note
859 | value("NOTE")
860 | end
861 |
862 | # The ORG value, an Array of String. The first string is the organization,
863 | # subsequent strings are departments within the organization. A wrapper
864 | # around #value("ORG").
865 | def org
866 | value("ORG")
867 | end
868 |
869 | # Return an Array of PHOTO Line#value, or yield each Line#value if a block
870 | # is given. A wrapper around #values("PHOTO").
871 | #
872 | # PHOTO is an image or photograph information that annotates some aspect of
873 | # the object the vCard represents. Commonly there is one PHOTO, and it is a
874 | # photo of the person identified by the vCard.
875 | #
876 | # See Attachment for a description of the value.
877 | def photos(&proc) #:yield: Line.value
878 | values("PHOTO", &proc)
879 | end
880 |
881 | ## PRODID
882 |
883 | ## PROFILE
884 |
885 | ## REV
886 |
887 | ## ROLE
888 |
889 | # Return an Array of SOUND Line#value, or yield each Line#value if a block
890 | # is given. A wrapper around #values("SOUND").
891 | #
892 | # SOUND is digital sound content information that annotates some aspect of
893 | # the vCard. By default this type is used to specify the proper
894 | # pronunciation of the name associated with the vCard. It is not commonly
895 | # used. Also, note that there is no mechanism available to specify that the
896 | # SOUND is being used for anything other than the default.
897 | #
898 | # See Attachment for a description of the value.
899 | def sounds(&proc) #:yield: Line.value
900 | values("SOUND", &proc)
901 | end
902 |
903 | ## SOURCE
904 |
905 | # The first TEL value of type +type+, a Telephone. Any of the location or
906 | # capability attributes of Telephone can be used as +type+. A wrapper around
907 | # #value("TEL", +type+).
908 | def telephone(type=nil)
909 | value("TEL", type)
910 | end
911 |
912 | # The TEL values, an array of Telephone. If a block is given, the values are
913 | # yielded. A wrapper around #values("TEL").
914 | def telephones #:yield:tel
915 | values("TEL")
916 | end
917 |
918 | # The TITLE value, a text string specifying the job title, functional
919 | # position, or function of the object the card represents. A wrapper around
920 | # #value("TITLE").
921 | def title
922 | value("TITLE")
923 | end
924 |
925 | ## UID
926 |
927 | # The URL value, a Attachment::Uri. A wrapper around #value("URL").
928 | def url
929 | value("URL")
930 | end
931 |
932 | # The URL values, an Attachment::Uri. A wrapper around #values("URL").
933 | def urls
934 | values("URL")
935 | end
936 |
937 | # The VERSION multiplied by 10 as an Integer. For example, a VERSION:2.1
938 | # vCard would have a version of 21, and a VERSION:3.0 vCard would have a
939 | # version of 30.
940 | #
941 | # VERSION is required for a vCard, this raises InvalidEncodingError if
942 | # there is no VERSION so it cannot return nil.
943 | def version
944 | v = value("VERSION")
945 | unless v
946 | raise ::Vcard::InvalidEncodingError, "Invalid vCard - it has no version field!"
947 | end
948 | v
949 | end
950 |
951 | def role
952 | value("ROLE")
953 | end
954 |
955 | # Make changes to a vCard.
956 | #
957 | # Yields a Vcard::Vcard::Maker that can be used to modify this vCard.
958 | def make #:yield: maker
959 | ::Vcard::Vcard::Maker.make2(self) do |maker|
960 | yield maker
961 | end
962 | end
963 |
964 | # Delete +line+ if block yields true.
965 | def delete_if #:nodoc: :yield: line
966 | # Do in two steps to not mess up progress through the enumerator.
967 | rm = []
968 |
969 | each do |f|
970 | line = f2l(f)
971 | if line && yield(line)
972 | rm << f
973 |
974 | # Hack - because we treat N and FN as one field
975 | if f.name? "N"
976 | rm << field("FN")
977 | end
978 | end
979 | end
980 |
981 | rm.each do |f|
982 | @fields.delete( f )
983 | @cache.delete( f )
984 | end
985 |
986 | end
987 |
988 | # A class to make and make changes to vCards.
989 | #
990 | # It can be used to create completely new vCards using Vcard#make2.
991 | #
992 | # Its is also yielded from Vcard::Vcard#make, in which case it allows a kind
993 | # of transactional approach to changing vCards, so their values can be
994 | # validated after any changes have been made.
995 | #
996 | # Examples:
997 | # - link:ex_mkvcard.txt: example of creating a vCard
998 | # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
999 | # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
1000 | # - link:ex_mkyourown.txt: example of adding support for new fields to Vcard::Maker
1001 | class Maker
1002 | # Make a vCard.
1003 | #
1004 | # Yields +maker+, a Vcard::Vcard::Maker which allows fields to be added to
1005 | # +card+, and returns +card+, a Vcard::Vcard.
1006 | #
1007 | # If +card+ is nil or not provided a new Vcard::Vcard is created and the
1008 | # fields are added to it.
1009 | #
1010 | # Defaults:
1011 | # - vCards must have both an N and an FN field, #make2 will fail if there
1012 | # is no N field in the +card+ when your block is finished adding fields.
1013 | # - If there is an N field, but no FN field, FN will be set from the
1014 | # information in N, see Vcard::Name#preformatted for more information.
1015 | # - vCards must have a VERSION field. If one does not exist when your block is
1016 | # is finished it will be set to 3.0.
1017 | def self.make2(card = ::Vcard::Vcard.create, &block) # :yields: maker
1018 | new(nil, card).make(&block)
1019 | end
1020 |
1021 | # Deprecated, use #make2.
1022 | #
1023 | # If set, the FN field will be set to +full_name+. Otherwise, FN will
1024 | # be set from the values in #name.
1025 | def self.make(full_name = nil, &block) # :yields: maker
1026 | new(full_name, ::Vcard::Vcard.create).make(&block)
1027 | end
1028 |
1029 | def make # :nodoc:
1030 | yield self
1031 | unless @card["N"]
1032 | raise ::Vcard::Unencodeable, "N field is mandatory"
1033 | end
1034 | fn = @card.field("FN")
1035 | if fn && fn.value.strip.length == 0
1036 | @card.delete(fn)
1037 | fn = nil
1038 | end
1039 | unless fn
1040 | @card << ::Vcard::DirectoryInfo::Field.create("FN", ::Vcard::Vcard::Name.new(@card["N"], "").formatted)
1041 | end
1042 | unless @card["VERSION"]
1043 | @card << ::Vcard::DirectoryInfo::Field.create("VERSION", "3.0")
1044 | end
1045 | @card
1046 | end
1047 |
1048 | private
1049 |
1050 | def initialize(full_name, card) # :nodoc:
1051 | @card = card || ::Vcard::Vcard::create
1052 | if full_name
1053 | @card << ::Vcard::DirectoryInfo::Field.create("FN", full_name.strip )
1054 | end
1055 | end
1056 |
1057 | public
1058 |
1059 | # Deprecated, see #name.
1060 | #
1061 | # Use
1062 | # maker.name do |n| n.fullname = "foo" end
1063 | # to set just fullname, or set the other fields to set fullname and the
1064 | # name.
1065 | def fullname=(fullname) #:nodoc: bacwards compat
1066 | if @card.field("FN")
1067 | raise ::Vcard::InvalidEncodingError, "Not allowed to add more than one FN field to a vCard."
1068 | end
1069 | @card << ::Vcard::DirectoryInfo::Field.create( "FN", fullname );
1070 | end
1071 |
1072 | # Set the name fields, N and FN.
1073 | #
1074 | # Attributes of +name+ are:
1075 | # - family: family name
1076 | # - given: given name
1077 | # - additional: additional names
1078 | # - prefix: such as "Ms." or "Dr."
1079 | # - suffix: such as "BFA", or "Sensei"
1080 | #
1081 | # +name+ is a Vcard::Name.
1082 | #
1083 | # All attributes are optional, though have all names be zero-length
1084 | # strings isn't really in the spirit of things. FN's value will be set
1085 | # to Vcard::Name#formatted if Vcard::Name#fullname isn't given a specific
1086 | # value.
1087 | #
1088 | # Warning: This is the only mandatory field.
1089 | def name #:yield:name
1090 | x = begin
1091 | @card.name.dup
1092 | rescue
1093 | ::Vcard::Vcard::Name.new
1094 | end
1095 |
1096 | yield x
1097 |
1098 | x.fullname.strip!
1099 |
1100 | delete_if do |line|
1101 | line.name == "N"
1102 | end
1103 |
1104 | @card << x.encode
1105 | @card << x.encode_fn
1106 |
1107 | self
1108 | end
1109 |
1110 | alias :add_name :name #:nodoc: backwards compatibility
1111 |
1112 | # Add an address field, ADR. +address+ is a Vcard::Vcard::Address.
1113 | def add_addr # :yield: address
1114 | x = ::Vcard::Vcard::Address.new
1115 | yield x
1116 | @card << x.encode
1117 | self
1118 | end
1119 |
1120 | # Add a telephone field, TEL. +tel+ is a Vcard::Vcard::Telephone.
1121 | #
1122 | # The block is optional, its only necessary if you want to specify
1123 | # the optional attributes.
1124 | def add_tel(number) # :yield: tel
1125 | x = ::Vcard::Vcard::Telephone.new(number)
1126 | if block_given?
1127 | yield x
1128 | end
1129 | @card << x.encode
1130 | self
1131 | end
1132 |
1133 | # Add an email field, EMAIL. +email+ is a Vcard::Vcard::Email.
1134 | #
1135 | # The block is optional, its only necessary if you want to specify
1136 | # the optional attributes.
1137 | def add_email(email) # :yield: email
1138 | x = ::Vcard::Vcard::Email.new(email)
1139 | if block_given?
1140 | yield x
1141 | end
1142 | @card << x.encode
1143 | self
1144 | end
1145 |
1146 | # Set the nickname field, NICKNAME.
1147 | #
1148 | # It can be set to a single String or an Array of String.
1149 | def nickname=(nickname)
1150 | delete_if { |l| l.name == "NICKNAME" }
1151 |
1152 | @card << ::Vcard::DirectoryInfo::Field.create( "NICKNAME", nickname );
1153 | end
1154 |
1155 | # Add a birthday field, BDAY.
1156 | #
1157 | # +birthday+ must be a time or date object.
1158 | #
1159 | # Warning: It may confuse both humans and software if you add multiple
1160 | # birthdays.
1161 | def birthday=(birthday)
1162 | if !birthday.respond_to? :month
1163 | raise ArgumentError, "birthday must be a date or time object."
1164 | end
1165 | delete_if { |l| l.name == "BDAY" }
1166 | @card << ::Vcard::DirectoryInfo::Field.create( "BDAY", birthday );
1167 | end
1168 |
1169 | # Add a note field, NOTE. The +note+ String can contain newlines, they
1170 | # will be escaped.
1171 | def add_note(note)
1172 | @card << ::Vcard::DirectoryInfo::Field.create( "NOTE", ::Vcard.encode_text(note) );
1173 | end
1174 |
1175 | # Add an instant-messaging/point of presence address field, IMPP. The address
1176 | # is a URL, with the syntax depending on the protocol.
1177 | #
1178 | # Attributes of IMPP are:
1179 | # - preferred: true - set if this is the preferred address
1180 | # - location: home, work, mobile - location of address
1181 | # - purpose: personal,business - purpose of communications
1182 | #
1183 | # All attributes are optional, and so is the block.
1184 | #
1185 | # The URL syntaxes for the messaging schemes is fairly complicated, so I
1186 | # don't try and build the URLs here, maybe in the future. This forces
1187 | # the user to know the URL for their own address, hopefully not too much
1188 | # of a burden.
1189 | #
1190 | # IMPP is defined in draft-jennings-impp-vcard-04.txt. It refers to the
1191 | # URI scheme of a number of messaging protocols, but doesn't give
1192 | # references to all of them:
1193 | # - "xmpp" indicates to use XMPP, draft-saintandre-xmpp-uri-06.txt
1194 | # - "irc" or "ircs" indicates to use IRC, draft-butcher-irc-url-04.txt
1195 | # - "sip" indicates to use SIP/SIMPLE, RFC 3261
1196 | # - "im" or "pres" indicates to use a CPIM or CPP gateway, RFC 3860 and RFC 3859
1197 | # - "ymsgr" indicates to use yahoo
1198 | # - "msn" might indicate to use Microsoft messenger
1199 | # - "aim" indicates to use AOL
1200 | #
1201 | def add_impp(url) # :yield: impp
1202 | params = {}
1203 |
1204 | if block_given?
1205 | x = Struct.new( :location, :preferred, :purpose ).new
1206 |
1207 | yield x
1208 |
1209 | x[:preferred] = "PREF" if x[:preferred]
1210 |
1211 | types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1212 |
1213 | params["TYPE"] = types if types.first
1214 | end
1215 |
1216 | @card << ::Vcard::DirectoryInfo::Field.create( "IMPP", url, params)
1217 | self
1218 | end
1219 |
1220 | # Add an X-AIM account name where +xaim+ is an AIM screen name.
1221 | #
1222 | # I don't know if this is conventional, or supported by anything other
1223 | # than AddressBook.app, but an example is:
1224 | # X-AIM;type=HOME;type=pref:exampleaccount
1225 | #
1226 | # Attributes of X-AIM are:
1227 | # - preferred: true - set if this is the preferred address
1228 | # - location: home, work, mobile - location of address
1229 | #
1230 | # All attributes are optional, and so is the block.
1231 | def add_x_aim(xaim) # :yield: xaim
1232 | params = {}
1233 |
1234 | if block_given?
1235 | x = Struct.new( :location, :preferred ).new
1236 |
1237 | yield x
1238 |
1239 | x[:preferred] = "PREF" if x[:preferred]
1240 |
1241 | types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1242 |
1243 | params["TYPE"] = types if types.first
1244 | end
1245 |
1246 | @card << ::Vcard::DirectoryInfo::Field.create( "X-AIM", xaim, params)
1247 | self
1248 | end
1249 |
1250 |
1251 | # Add a photo field, PHOTO.
1252 | #
1253 | # Attributes of PHOTO are:
1254 | # - image: set to image data to include inline
1255 | # - link: set to the URL of the image data
1256 | # - type: string identifying the image type, supposed to be an "IANA registered image format",
1257 | # or a non-registered image format (usually these start with an x-)
1258 | #
1259 | # An error will be raised if neither image or link is set, or if both image
1260 | # and link is set.
1261 | #
1262 | # Setting type is optional for a link image, because either the URL, the
1263 | # image file extension, or a HTTP Content-Type may specify the type. If
1264 | # it's not a link, setting type is mandatory, though it can be set to an
1265 | # empty string, ''
, if the type is unknown.
1266 | #
1267 | # TODO - I'm not sure about this API. I'm thinking maybe it should be
1268 | # #add_photo(image, type), and that I should detect when the image is a
1269 | # URL, and make type mandatory if it wasn't a URL.
1270 | def add_photo # :yield: photo
1271 | x = Struct.new(:image, :link, :type).new
1272 | yield x
1273 | if x[:image] && x[:link]
1274 | raise ::Vcard::InvalidEncodingError, "Image is not allowed to be both inline and a link."
1275 | end
1276 |
1277 | value = x[:image] || x[:link]
1278 |
1279 | if !value
1280 | raise ::Vcard::InvalidEncodingError, "A image link or inline data must be provided."
1281 | end
1282 |
1283 | params = {}
1284 |
1285 | # Don't set type to the empty string.
1286 | params["TYPE"] = x[:type] if( x[:type] && x[:type].length > 0 )
1287 |
1288 | if x[:link]
1289 | params["VALUE"] = "URI"
1290 | else # it's inline, base-64 encode it
1291 | params["ENCODING"] = :b64
1292 | if !x[:type]
1293 | raise ::Vcard::InvalidEncodingError, "Inline image data must have it's type set."
1294 | end
1295 | end
1296 |
1297 | @card << ::Vcard::DirectoryInfo::Field.create( "PHOTO", value, params )
1298 | self
1299 | end
1300 |
1301 | # Set the title field, TITLE.
1302 | #
1303 | # It can be set to a single String.
1304 | def title=(title)
1305 | delete_if { |l| l.name == "TITLE" }
1306 |
1307 | @card << ::Vcard::DirectoryInfo::Field.create( "TITLE", title );
1308 | end
1309 |
1310 | # Set the org field, ORG.
1311 | #
1312 | # It can be set to a single String or an Array of String.
1313 | def org=(org)
1314 | delete_if { |l| l.name == "ORG" }
1315 |
1316 | @card << ::Vcard::DirectoryInfo::Field.create( "ORG", org );
1317 | end
1318 |
1319 | # Add a role field, ROLE.
1320 | #
1321 | # It can be set to a single String.
1322 | def add_role(role)
1323 | @card << ::Vcard::DirectoryInfo::Field.create("ROLE", ::Vcard.encode_text(role));
1324 | end
1325 |
1326 | # Add a URL field, URL.
1327 | def add_url(url)
1328 | @card << ::Vcard::DirectoryInfo::Field.create( "URL", url.to_str );
1329 | end
1330 |
1331 | # Add a Field, +field+.
1332 | def add_field(field)
1333 | fieldname = field.name.upcase
1334 | case
1335 | when [ "BEGIN", "END" ].include?(fieldname)
1336 | raise ::Vcard::InvalidEncodingError, "Not allowed to manually add #{field.name} to a vCard."
1337 |
1338 | when [ "VERSION", "N", "FN" ].include?(fieldname)
1339 | if @card.field(fieldname)
1340 | raise ::Vcard::InvalidEncodingError, "Not allowed to add more than one #{fieldname} to a vCard."
1341 | end
1342 | @card << field
1343 |
1344 | else
1345 | @card << field
1346 | end
1347 | end
1348 |
1349 | # Copy the fields from +card+ into self using #add_field. If a block is
1350 | # provided, each Field from +card+ is yielded. The block should return a
1351 | # Field to add, or nil. The Field doesn't have to be the one yielded,
1352 | # allowing the field to be copied and modified (see Field#copy) before adding, or
1353 | # not added at all if the block yields nil.
1354 | #
1355 | # The vCard fields BEGIN and END aren't copied, and VERSION, N, and FN are copied
1356 | # only if the card doesn't have them already.
1357 | def copy(card) # :yields: Field
1358 | card.each do |field|
1359 | fieldname = field.name.upcase
1360 | case
1361 | when [ "BEGIN", "END" ].include?(fieldname)
1362 | # Never copy these
1363 |
1364 | when [ "VERSION", "N", "FN" ].include?(fieldname) && @card.field(fieldname)
1365 | # Copy these only if they don't already exist.
1366 |
1367 | else
1368 | if block_given?
1369 | field = yield field
1370 | end
1371 |
1372 | if field
1373 | add_field(field)
1374 | end
1375 | end
1376 | end
1377 | end
1378 |
1379 | # Delete +line+ if block yields true.
1380 | def delete_if #:yield: line
1381 | @card.delete_if do |line|
1382 | yield line
1383 | end
1384 | rescue NoMethodError
1385 | # FIXME - this is a hideous hack, allowing a DirectoryInfo to
1386 | # be passed instead of a Vcard, and for it to almost work. Yuck.
1387 | end
1388 |
1389 | end
1390 | end
1391 | end
1392 |
1393 |
--------------------------------------------------------------------------------
/lib/vcard/version.rb:
--------------------------------------------------------------------------------
1 | module Vcard
2 | VERSION = "0.4.0"
3 | end
4 |
--------------------------------------------------------------------------------
/test/configuration_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ConfigurationTest < Test::Unit::TestCase
4 | def test_should_be_an_instance_of_configuration
5 | assert Vcard.configuration.is_a?(::Vcard::Configuration)
6 | end
7 |
8 | def test_have_default_values
9 | Vcard.configuration.reset
10 | assert_equal(Vcard.configuration.raise_on_invalid_line, true)
11 | assert_equal(Vcard.configuration.ignore_invalid_vcards, true)
12 | end
13 |
14 | def test_allow_configuration_with_block
15 | Vcard.configuration.reset
16 | Vcard.configure do |config|
17 | config.raise_on_invalid_line = false
18 | config.ignore_invalid_vcards = false
19 | end
20 | assert_equal(Vcard.configuration.raise_on_invalid_line, false)
21 | assert_equal(Vcard.configuration.ignore_invalid_vcards, false)
22 | end
23 | end
--------------------------------------------------------------------------------
/test/field_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class FieldTest < Test::Unit::TestCase
4 |
5 | Field = Vcard::DirectoryInfo::Field
6 |
7 | def test_encode_decode_text()
8 | enc_in = "+\\\\+\\n+\\N+\\,+\\;+\\a+\\b+\\c+"
9 | dec = Vcard.decode_text(enc_in)
10 | #puts("<#{enc_in}> => <#{dec}>")
11 | assert_equal("+\\+\n+\n+,+;+a+b+c+", dec)
12 | enc_out = Vcard.encode_text(dec)
13 | should_be = "+\\\\+\\n+\\n+\\,+\\;+a+b+c+"
14 | # Note a, b, and c are allowed to be escaped, but shouldn't be and
15 | # aren't in output
16 | #puts("<#{dec}> => <#{enc_out}>")
17 | assert_equal(should_be, enc_out)
18 |
19 | end
20 |
21 | def test_field4
22 | line = "t;e=a,b: 4 "
23 | part = Field.decode0(line)
24 | assert_equal("4", part[ 4 ])
25 | end
26 |
27 | def test_field3
28 | line = "t;e=a,b:4"
29 | part = Field.decode0(line)
30 | assert_equal("4", part[ 4 ])
31 | assert_equal( {"E" => [ "a","b" ] }, part[ 3 ])
32 | end
33 |
34 | def test_field2
35 | line = "tel;type=work,voice,msg:+1 313 747-4454"
36 | part = Field.decode0(line)
37 | assert_equal("+1 313 747-4454", part[ 4 ])
38 | assert_equal( {"TYPE" => [ "work","voice","msg" ] }, part[ 3 ])
39 | end
40 |
41 | def test_field1
42 | line = 'ORGANIZER;CN="xxxx, xxxx [SC100:370:EXCH]":MAILTO:xxxx@americasm01.nt.com'
43 | parts = Field.decode0(line)
44 |
45 | assert_equal(nil, parts[1])
46 | assert_equal("ORGANIZER", parts[2])
47 | assert_equal({ "CN" => [ "xxxx, xxxx [SC100:370:EXCH]" ] }, parts[3])
48 | assert_equal("MAILTO:xxxx@americasm01.nt.com", parts[4])
49 | end
50 |
51 | =begin this can not be done :-(
52 | def test_case_equiv
53 | line = 'ORGANIZER;CN="xxxx, xxxx [SC100:370:EXCH]":MAILTO:xxxx@americasm01.nt.com'
54 | field = Field.decode(line)
55 | assert_equal(true, field.name?('organIZER'))
56 | assert_equal(true, field === 'organIZER')
57 |
58 | b = nil
59 | case field
60 | when 'organIZER'
61 | b = true
62 | end
63 |
64 | assert_equal(true, b)
65 | end
66 | =end
67 |
68 | def test_field0
69 | assert_equal("name:", line = Field.encode0(nil, "name"))
70 | assert_equal([ true, nil, "NAME", {}, ""], Field.decode0(line))
71 |
72 | assert_equal("name:value", line = Field.encode0(nil, "name", {}, "value"))
73 | assert_equal([ true, nil, "NAME", {}, "value"], Field.decode0(line))
74 |
75 | assert_equal("name;encoding=B:dmFsdWU=", line = Field.encode0(nil, "name", { "encoding"=>:b64 }, "value"))
76 | assert_equal([ true, nil, "NAME", { "ENCODING"=>["B"]}, ["value"].pack("m").chomp ], Field.decode0(line))
77 |
78 | line = Field.encode0("group", "name", {}, "value")
79 | assert_equal "group.name:value", line
80 | assert_equal [ true, "GROUP", "NAME", {}, "value"], Field.decode0(line)
81 | end
82 |
83 | def test_invalid_fields_wih_raise_error
84 | Vcard::configuration.raise_on_invalid_line = true
85 | [
86 | "g.:",
87 | ":v",
88 | ].each do |line|
89 | assert_raises(::Vcard::InvalidEncodingError) { Field.decode0(line) }
90 | end
91 | end
92 |
93 | def test_invalid_fields_wihout_raise_error
94 | Vcard.configuration.raise_on_invalid_line = false
95 | [
96 | "g.:",
97 | ":v",
98 | ].each do |line|
99 | assert_nothing_raised { Field.decode0(line) }
100 | end
101 | end
102 |
103 | def test_date_encode
104 | assert_equal("DTSTART:20040101\n", Field.create("DTSTART", Date.new(2004, 1, 1) ).to_s)
105 | assert_equal("DTSTART:20040101\n", Field.create("DTSTART", [Date.new(2004, 1, 1)]).to_s)
106 | end
107 |
108 | def test_field_modify
109 | f = Field.create("name")
110 |
111 |
112 | assert_equal("", f.value)
113 | f.value = ""
114 | assert_equal("", f.value)
115 | f.value = "z"
116 | assert_equal("z", f.value)
117 |
118 | f.group = "z.b"
119 | assert_equal("Z.B", f.group)
120 | assert_equal("z.b.NAME:z\n", f.encode)
121 |
122 | f.value = :group
123 | assert_equal("Z.B.NAME:group\n", f.encode)
124 | f.value = "z"
125 |
126 | assert_equal("Z.B", f.group)
127 |
128 | assert_equal("Z.B.NAME:z\n", f.encode)
129 |
130 | f.group = :group
131 | assert_equal("group.NAME:z\n", f.encode)
132 | f.group = "z.b"
133 |
134 | assert_equal("z.b.NAME:z\n", f.encode)
135 | assert_equal("Z.B", f.group)
136 |
137 | f["p0"] = "hi julie"
138 |
139 | assert_equal("Z.B.NAME;P0=hi julie:z\n", f.encode)
140 | assert_equal(["hi julie"], f.param("p0"))
141 | assert_equal(["hi julie"], f["p0"])
142 | assert_equal("NAME", f.name)
143 | assert_equal("Z.B", f.group)
144 |
145 | # FAIL assert_raises(ArgumentError) { f.group = "z.b:" }
146 |
147 | assert_equal("Z.B", f.group)
148 |
149 | f.value = "some text"
150 |
151 | assert_equal("some text", f.value)
152 | assert_equal("some text", f.value_raw)
153 |
154 | f["encoding"] = :b64
155 |
156 | assert_equal("some text", f.value)
157 | assert_equal([ "some text" ].pack("m*").chomp, f.value_raw)
158 | end
159 |
160 | def test_field_wrapping
161 | assert_equal("0:x\n", Vcard::DirectoryInfo::Field.create("0", "x" * 1).encode(4))
162 | assert_equal("0:xx\n", Vcard::DirectoryInfo::Field.create("0", "x" * 2).encode(4))
163 | assert_equal("0:xx\n x\n", Vcard::DirectoryInfo::Field.create("0", "x" * 3).encode(4))
164 | assert_equal("0:xx\n xx\n", Vcard::DirectoryInfo::Field.create("0", "x" * 4).encode(4))
165 | assert_equal("0:xx\n xxx\n x\n", Vcard::DirectoryInfo::Field.create("0", "x" * 6).encode(4))
166 | assert_equal("0:xx\n xxx\n xx\n", Vcard::DirectoryInfo::Field.create("0", "x" * 7).encode(4))
167 | assert_equal("0:xxxxxxx\n", Vcard::DirectoryInfo::Field.create("0", "x" * 7).encode(0))
168 | assert_equal("0:xxxxxxx\n", Vcard::DirectoryInfo::Field.create("0", "x" * 7).encode())
169 | assert_equal("0:#{"x" * 73}\n #{"x" * 74}\n #{"x" * 53}\n", Vcard::DirectoryInfo::Field.create("0", "x" * 200).encode())
170 | end
171 |
172 | def test_nl
173 | assert_equal("test:#{"value" * 14}\r\n #{"value" * 6}\r\n", Vcard::DirectoryInfo::Field.create("test", "value" * 20).encode(nl: "\r\n"))
174 | end
175 | end
176 |
--------------------------------------------------------------------------------
/test/fixtures/bday_decode.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | BDAY:1970-07-14
3 | END:VCARD
4 |
--------------------------------------------------------------------------------
/test/fixtures/bday_decode_2.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | BDAY:1970-07-14
3 | BDAY:70-7-14
4 | BDAY:1970-07-15T03:45:12
5 | BDAY:1970-07-15T03:45:12Z
6 | END:VCARD
7 |
--------------------------------------------------------------------------------
/test/fixtures/bday_decode_3.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:3.0
3 | N:;Given Name;;;
4 | FN:Given Name
5 | BDAY:19801025
6 | END:VCARD
7 |
--------------------------------------------------------------------------------
/test/fixtures/empty_tel.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | TEL;HOME;FAX:
3 | END:VCARD
4 |
--------------------------------------------------------------------------------
/test/fixtures/ex1.vcard:
--------------------------------------------------------------------------------
1 | cn:
2 | cn:Babs Jensen
3 | cn:Barbara J Jensen
4 | sn:Jensen
5 | email:babs@umich.edu
6 | phone:+1 313 747-4454
7 | x-id:1234567890
8 |
--------------------------------------------------------------------------------
/test/fixtures/ex2.vcard:
--------------------------------------------------------------------------------
1 | begin:VCARD
2 | source:ldap://cn=bjorn%20Jensen, o=university%20of%20Michigan, c=US
3 | name:Bjorn Jensen
4 | fn:Bj=F8rn Jensen
5 | n:Jensen;Bj=F8rn
6 | email;type=internet:bjorn@umich.edu
7 | tel;type=work,voice,msg:+1 313 747-4454
8 | key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK
9 | role:Office Manager\;Something Else
10 | end:VCARD
11 |
--------------------------------------------------------------------------------
/test/fixtures/ex3.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE
3 | name:Meister Berger
4 | fn:Meister Berger
5 | n:Berger;Meister
6 | bday;value=date:1963-09-21
7 | o:Universit=E6t G=F6rlitz
8 | title:Mayor
9 | title;language=de;value=text:Burgermeister
10 | note:The Mayor of the great city of
11 | Goerlitz in the great country of Germany.
12 | email;internet:mb@goerlitz.de
13 | home.tel;type=fax,voice,msg:+49 3581 123456
14 | home.label:Hufenshlagel 1234\n
15 | 02828 Goerlitz\n
16 | Deutschland
17 | key;type=X509;encoding=b:MIICajCCAdOgAwIBAgICBEUwDQYJKoZIhvcNAQEEBQ
18 | AwdzELMAkGA1UEBhMCVVMxLDAqBgNVBAoTI05ldHNjYXBlIENvbW11bmljYXRpb25zI
19 | ENvcnBvcmF0aW9uMRwwGgYDVQQLExNJbmZvcm1hdGlvbiBTeXN0ZW1zMRwwGgYDVQQD
20 | ExNyb290Y2EubmV0c2NhcGUuY29tMB4XDTk3MDYwNjE5NDc1OVoXDTk3MTIwMzE5NDc
21 | 1OVowgYkxCzAJBgNVBAYTAlVTMSYwJAYDVQQKEx1OZXRzY2FwZSBDb21tdW5pY2F0aW
22 | 9ucyBDb3JwLjEYMBYGA1UEAxMPVGltb3RoeSBBIEhvd2VzMSEwHwYJKoZIhvcNAQkBF
23 | hJob3dlc0BuZXRzY2FwZS5jb20xFTATBgoJkiaJk/IsZAEBEwVob3dlczBcMA0GCSqG
24 | SIb3DQEBAQUAA0sAMEgCQQC0JZf6wkg8pLMXHHCUvMfL5H6zjSk4vTTXZpYyrdN2dXc
25 | oX49LKiOmgeJSzoiFKHtLOIboyludF90CgqcxtwKnAgMBAAGjNjA0MBEGCWCGSAGG+E
26 | IBAQQEAwIAoDAfBgNVHSMEGDAWgBT84FToB/GV3jr3mcau+hUMbsQukjANBgkqhkiG9
27 | w0BAQQFAAOBgQBexv7o7mi3PLXadkmNP9LcIPmx93HGp0Kgyx1jIVMyNgsemeAwBM+M
28 | SlhMfcpbTrONwNjZYW8vJDSoi//yrZlVt9bJbs7MNYZVsyF1unsqaln4/vy6Uawfg8V
29 | UMk1U7jt8LYpo4YULU7UZHPYVUaSgVttImOHZIKi4hlPXBOhcUQ==
30 | end:vcard
31 |
--------------------------------------------------------------------------------
/test/fixtures/ex_21.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:2.1
3 | X-EVOLUTION-FILE-AS:AAA Our Fax
4 | FN:AAA Our Fax
5 | N:AAA Our Fax
6 | ADR;WORK;PREF:
7 | LABEL;WORK;PREF:
8 | TEL;WORK;FAX:925 833-7660
9 | TEL;HOME;FAX:925 833-7660
10 | TEL;VOICE:1
11 | TEL;FAX:2
12 | EMAIL;INTERNET:e@c
13 | TITLE:
14 | NOTE:
15 | UID:pas-id-3F93E22900000001
16 | END:VCARD
17 |
--------------------------------------------------------------------------------
/test/fixtures/ex_21_case0.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:2.1
3 | N:Middle Family;Ny_full
4 | TEL;PREF;HOME;VOICE:0123456789
5 | TEL;FAX:0123456789
6 | TEL;CELL;VOICE:0123456789
7 | TEL;HOME;VOICE:0123456789
8 | TEL;WORK;VOICE:0123456789
9 | EMAIL:email@email.com
10 | EMAIL:work@work.com
11 | URL:www.email.com
12 | URL:www.work.com
13 | LABEL;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:Box 1234=0AWorkv=E4gen =
14 | 2=0AWorkv=E4gen 1=0AUme=E5=0AV=E4sterbotten=0A12345=0AS
15 | END:VCARD
16 |
--------------------------------------------------------------------------------
/test/fixtures/ex_apple1.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:3.0
3 | N:Roberts;Sam;;;
4 | FN:Roberts Sam
5 | EMAIL;type=HOME;type=pref:sroberts@uniserve.com
6 | TEL;type=WORK;type=pref:905-501-3781
7 | TEL;type=FAX:905-907-4230
8 | TEL;type=HOME:416 535 5341
9 | ADR;type=HOME;type=pref:;;376 Westmoreland Ave.;Toronto;ON;M6H 3
10 | A6;Canada
11 | NOTE:CATEGORIES: Amis/Famille
12 | BDAY;value=date:1970-07-14
13 | END:VCARD
14 |
--------------------------------------------------------------------------------
/test/fixtures/ex_attach.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:3.0
3 | N:Middle Family;Ny_full
4 | PHOTO:val\nue
5 | PHOTO;encoding=8bit:val\nue
6 | PHOTO;encoding=8bit:val\nue
7 | PHOTO;encoding=8bit;type=atype:val\nue
8 | PHOTO;value=binary;encoding=8bit:val\nue
9 | PHOTO;value=binary;encoding=8bit:val\nue
10 | PHOTO;value=binary;encoding=8bit;type=atype:val\nue
11 | PHOTO;value=text;encoding=8bit:val\nue
12 | PHOTO;value=text;encoding=8bit:val\nue
13 | PHOTO;value=text;encoding=8bit;type=atype:val\nue
14 | PHOTO;value=uri:my://
15 | PHOTO;value=uri;type=atype:my://
16 | END:VCARD
17 |
--------------------------------------------------------------------------------
/test/fixtures/ex_bdays.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | BDAY;value=date:206-12-15
3 | END:VCARD
4 | BEGIN:VCARD
5 | BDAY;value=date:2003-12-09
6 | END:VCARD
7 | BEGIN:VCARD
8 | END:VCARD
9 |
--------------------------------------------------------------------------------
/test/fixtures/ex_encode_1.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:3.0
3 | N:Roberts;Sam;;;
4 | FN:Roberts Sam
5 | EMAIL;type=HOME;type=pref:sroberts@uniserve.com
6 | TEL;type=HOME:416 535 5341
7 | ADR;type=HOME;type=pref:;;376 Westmoreland Ave.;Toronto;ON;M6H 3A6;Canada
8 | NOTE:CATEGORIES: Amis/Famille
9 | BDAY;value=date:1970-07-14
10 | END:VCARD
11 |
--------------------------------------------------------------------------------
/test/fixtures/ex_ical_1.vcal:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | CALSCALE:GREGORIAN
3 | X-WR-TIMEZONE;VALUE=TEXT:Canada/Eastern
4 | METHOD:PUBLISH
5 | PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
6 | X-WR-RELCALID;VALUE=TEXT:18E75B8C-5722-11D7-AB0B-000393AD088C
7 | X-WR-CALNAME;VALUE=TEXT:Events
8 | VERSION:2.0
9 | BEGIN:VEVENT
10 | SEQUENCE:14
11 | UID:18E74C28-5722-11D7-AB0B-000393AD088C
12 | DTSTAMP:20030301T171521Z
13 | SUMMARY:Bob Log III
14 | DTSTART;TZID=Canada/Eastern:20030328T200000
15 | DTEND;TZID=Canada/Eastern:20030328T230000
16 | DESCRIPTION:Healey's\n\nLook up exact time.\n
17 | BEGIN:VALARM
18 | TRIGGER;VALUE=DURATION:-P2D
19 | ACTION:DISPLAY
20 | DESCRIPTION:Event reminder
21 | END:VALARM
22 | BEGIN:VALARM
23 | ATTENDEE:mailto:sroberts@uniserve.com
24 | TRIGGER;VALUE=DURATION:-P1D
25 | ACTION:EMAIL
26 | SUMMARY:Alarm notification
27 | DESCRIPTION:This is an event reminder
28 | END:VALARM
29 | END:VEVENT
30 | BEGIN:VEVENT
31 | SEQUENCE:1
32 | DTSTAMP:20030312T043534Z
33 | SUMMARY:Small Potatoes 10\nFriday\, March 14th\, 8:00 p.m.\n361 Danforth
34 | Avenue (at Hampton -- Chester subway)\nInfo:_ (416) 480-2802 or (416)
35 | 323-1715\n
36 | UID:18E750A8-5722-11D7-AB0B-000393AD088C
37 | DTSTART;TZID=Canada/Eastern:20030315T000000
38 | DURATION:PT1H
39 | BEGIN:VALARM
40 | ATTENDEE:mailto:sroberts@uniserve.com
41 | TRIGGER;VALUE=DURATION:-P1D
42 | ACTION:EMAIL
43 | SUMMARY:Alarm notification
44 | DESCRIPTION:This is an event reminder
45 | END:VALARM
46 | END:VEVENT
47 | END:VCALENDAR
48 |
--------------------------------------------------------------------------------
/test/fixtures/gmail.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:3.0
3 | FN:Stepcase TestUser
4 | N:TestUser;Stepcase;;;
5 | EMAIL;TYPE=INTERNET:testuser@stepcase.com
6 | X-GTALK:gtalk.step
7 | X-AIM:aim.step
8 | X-YAHOO:yahoo.step
9 | X-MSN:msn.step
10 | X-ICQ:icq.step
11 | X-JABBER:jabber.step
12 | TEL;TYPE=FAX:44444444
13 | TEL;TYPE=PAGER:66666666
14 | TEL;TYPE=HOME:22222222
15 | TEL;TYPE=CELL:11111111
16 | TEL;TYPE=FAX:55555555
17 | TEL;TYPE=WORK:33333333
18 | LABEL;TYPE=HOME;ENCODING=QUOTED-PRINTABLE:123 Home, Home Street=0D=0A=
19 | Kowloon, N/A=0D=0A=
20 | Hong Kong
21 | LABEL;TYPE=HOME;ENCODING=QUOTED-PRINTABLE:321 Office, Work Road=0D=0A=
22 | Tsuen Wan NT=0D=0A=
23 | Hong Kong
24 | TITLE:CTO
25 | ORG:Stepcase.com
26 | NOTE:Stepcase test user is a robot.
27 | END:VCARD
28 |
--------------------------------------------------------------------------------
/test/fixtures/highrise.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | VERSION:3.0
3 | REV:20080409T095515Z
4 | X-YAHOO;TYPE=HOME:yahoo.john
5 | X-GOOGLE TALK;TYPE=WORK:gtalk.john
6 | X-SAMETIME;TYPE=WORK:sametime.john
7 | X-SKYPE;TYPE=WORK:skype.john
8 | X-MSN;TYPE=WORK:msn.john
9 | X-JABBER;TYPE=WORK:jabber.john
10 | N:Doe;John;;;
11 | ADR;TYPE=WORK:;;456 Grandview Building\, Wide Street;San Diego;CA;90204;
12 | United States
13 | ADR;TYPE=HOME:;;123 Sweet Home\, Narrow Street;New York;NY;91102;United
14 | States
15 | URL;TYPE=OTHER:http\://www.homepage.com
16 | URL;TYPE=HOME:http\://www.home.com
17 | URL;TYPE=WORK:http\://www.work.com
18 | URL;TYPE=OTHER:http\://www.other.com
19 | URL;TYPE=OTHER:http\://www.custom.com
20 | ORG:John Doe & Partners Limited;;
21 | TEL;TYPE=WORK:11111111
22 | TEL;TYPE=CELL:22222222
23 | TEL;TYPE=HOME:33333333
24 | TEL;TYPE=OTHER:44444444
25 | TEL;TYPE=FAX:55555555
26 | TEL;TYPE=FAX:66666666
27 | TEL;TYPE=PAGER:77777777
28 | TEL;TYPE=OTHER:88888888
29 | TEL;TYPE=OTHER:99999999
30 | UID:cc548e11-569e-3bf5-a9aa-722de4571f4a
31 | X-ICQ;TYPE=HOME:icq.john
32 | EMAIL;TYPE=WORK,INTERNET:john.doe@work.com
33 | EMAIL;TYPE=HOME,INTERNET:john.doe@home.com
34 | EMAIL;TYPE=OTHER,INTERNET:john.doe@other.com
35 | EMAIL;TYPE=OTHER,INTERNET:john.doe@custom.com
36 | TITLE:Sales Manager
37 | X-OTHER;TYPE=WORK:other.john
38 | X-AIM;TYPE=WORK:aim.john
39 | X-QQ;TYPE=WORK:qq.john
40 | FN:John Doe
41 | END:VCARD
42 |
--------------------------------------------------------------------------------
/test/fixtures/multiple_occurences_of_type.vcard:
--------------------------------------------------------------------------------
1 | begin:VCARD
2 | version:2.1
3 | v;x1=a;x2=,a;x3=a,;x4=a,,a;x5=,a,:
4 | source:ldap://cn=bjorn%20Jensen, o=university%20of%20Michigan, c=US
5 | fn:Bj=F8rn
6 | Jensen
7 | other.name:Jensen;Bj=F8rn
8 | some.other.value:1.2.3
9 | some.other.value:some.other
10 | some.other.value:some.other.value
11 | v;p-1=;p-2=,,;p-3=a;p-4=a b,"v;p-1=;p-2=,,;p-3=a;p-4=a":v-value
12 | email;type=internet:
13 | bjorn@umich.edu
14 | tel;type=work,voice,msg:+1 313 747-4454
15 | tel:+...
16 | key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK
17 | end:vcard
18 |
--------------------------------------------------------------------------------
/test/fixtures/nickname0.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | end:vcard
3 |
--------------------------------------------------------------------------------
/test/fixtures/nickname1.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | nickname:
3 | end:vcard
4 |
--------------------------------------------------------------------------------
/test/fixtures/nickname2.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | nickname:
3 | end:vcard
4 |
--------------------------------------------------------------------------------
/test/fixtures/nickname3.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | nickname: Big Joey
3 | end:vcard
4 |
--------------------------------------------------------------------------------
/test/fixtures/nickname4.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | nickname:
3 | nickname: Big Joey
4 | end:vcard
5 |
--------------------------------------------------------------------------------
/test/fixtures/nickname5.vcard:
--------------------------------------------------------------------------------
1 | begin:vcard
2 | nickname:
3 | nickname: Big Joey
4 | nickname:Bob
5 | end:vcard
6 |
--------------------------------------------------------------------------------
/test/fixtures/slash_in_field_name.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | X-messaging/xmpp-All:some@jabber.id
3 | END:VCARD
4 |
--------------------------------------------------------------------------------
/test/fixtures/tst1.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:vCard
2 | DESCRIPTION:Healey's\n\nLook up exact time.\n
3 | email;type=work:work@example.com
4 | email;type=internet,home;type=pref:home@example.com
5 | fax;type=foo,pref;bar:fax
6 | name:firstname
7 | name:secondname
8 | time;value=time:
9 | END:vCARD
10 |
--------------------------------------------------------------------------------
/test/fixtures/url_decode.vcard:
--------------------------------------------------------------------------------
1 | BEGIN:VCARD
2 | URL:www.email.com
3 | URL:www.work.com
4 | END:VCARD
5 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require "test/unit"
2 | require "vcard"
3 |
4 | include Vcard
5 |
6 | class Test::Unit::TestCase
7 | # Test equivalence where whitespace is compressed.
8 |
9 | def assert_equal_nospace(expected, got)
10 | expected = expected.gsub(/\s+/, "")
11 | got = expected.gsub(/\s+/, "")
12 | assert_equal(expected, got)
13 | end
14 |
15 | def utf_name_test(c)
16 | card = Vcard::Vcard.decode(c).first
17 | assert_equal("name", card.name.family)
18 | rescue => exception
19 | exception.message << " #{c.inspect}"
20 | raise
21 | end
22 |
23 | def be(s)
24 | s.unpack("U*").pack("n*")
25 | end
26 |
27 | def le(s)
28 | s.unpack("U*").pack("v*")
29 | end
30 |
31 | def vcard(name)
32 | open("test/fixtures/#{name}.vcard").read
33 | end
34 |
35 | def vcal(name)
36 | open("test/fixtures/#{name}.vcal").read
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/vcard_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class VcardTest < Test::Unit::TestCase
4 |
5 | # RFC2425 - 8.1. Example 1
6 | # Note that this is NOT a valid vCard, it lacks BEGIN/END.
7 | def test_ex1
8 | card = nil
9 | assert_nothing_thrown { card = Vcard::DirectoryInfo.decode(vcard(:ex1)) }
10 | assert_equal_nospace(vcard(:ex1), card.to_s)
11 |
12 | assert_equal("Babs Jensen", card["cn"])
13 | assert_equal("Jensen", card["sn"])
14 |
15 | assert_equal("babs@umich.edu", card[ "email" ])
16 |
17 | assert_equal("+1 313 747-4454", card[ "PhOnE" ])
18 | assert_equal("1234567890", card[ "x-id" ])
19 | assert_equal([], card.groups)
20 | end
21 |
22 | # RFC2425 - 8.2. Example 2
23 | def test_ex2
24 | card = nil
25 | assert_nothing_thrown { card = Vcard::Vcard.decode(vcard(:ex2)).first }
26 | assert_equal(vcard(:ex2), card.encode(0))
27 | assert_raises(::Vcard::InvalidEncodingError) { card.version }
28 |
29 | assert_equal("Bj=F8rn Jensen", card.name.fullname)
30 | assert_equal("Jensen", card.name.family)
31 | assert_equal("Bj=F8rn", card.name.given)
32 | assert_equal("", card.name.prefix)
33 | assert_equal('Office Manager;Something Else', card.role)
34 |
35 | assert_equal("Bj=F8rn Jensen", card[ "fn" ])
36 | assert_equal("+1 313 747-4454", card[ "tEL" ])
37 |
38 | assert_equal(nil, card[ "not-a-field" ])
39 | assert_equal([], card.groups)
40 |
41 | assert_equal(nil, card.enum_by_name("n").entries[0].param("encoding"))
42 |
43 | assert_equal(["internet"], card.enum_by_name("Email").entries.first.param("Type"))
44 | assert_equal(nil, card.enum_by_name("Email").entries[0].param("foo"))
45 |
46 | assert_equal(["B"], card.enum_by_name("kEy").to_a.first.param("encoding"))
47 | assert_equal("B", card.enum_by_name("kEy").entries[0].encoding)
48 |
49 | assert_equal(["work", "voice", "msg"], card.enum_by_name("tel").entries[0].param("Type"))
50 |
51 | assert_equal([card.fields[6]], card.enum_by_name("tel").entries)
52 |
53 | assert_equal([card.fields[6]], card.enum_by_name("tel").to_a)
54 |
55 | assert_equal(nil, card.enum_by_name("tel").entries.first.encoding)
56 |
57 | assert_equal("B", card.enum_by_name("key").entries.first.encoding)
58 |
59 | assert_equal("dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK", card.enum_by_name("key").entries.first.value_raw)
60 |
61 | assert_equal("this could be \nmy certificate\n", card.enum_by_name("key").entries.first.value)
62 |
63 | card.lines
64 | end
65 |
66 | # This is my vCard exported from OS X's AddressBook.app.
67 | def test_ex_apple1
68 | card = nil
69 | assert_nothing_thrown { card = Vcard::Vcard.decode(vcard(:ex_apple1)).first }
70 |
71 | assert_equal("Roberts Sam", card.name.fullname)
72 | assert_equal("Roberts", card.name.family)
73 | assert_equal("Sam", card.name.given)
74 | assert_equal("", card.name.prefix)
75 | assert_equal("", card.name.suffix)
76 |
77 | assert_equal(vcard(:ex_apple1), card.to_s(64))
78 |
79 | assert_equal("3.0", card[ "version" ])
80 | assert_equal(30, card.version)
81 |
82 | assert_equal("sroberts@uniserve.com", card[ "email" ])
83 | assert_equal(["HOME", "pref"], card.enum_by_name("email").entries.first.param("type"))
84 | assert_equal(nil, card.enum_by_name("email").entries.first.group)
85 |
86 | assert_equal(["WORK","pref"], card.enum_by_name("tel").entries[0].param("type"))
87 | assert_equal(["FAX"], card.enum_by_name("tel").entries[1].param("type"))
88 | assert_equal(["HOME"], card.enum_by_name("tel").entries[2].param("type"))
89 |
90 | assert_equal(nil, card.enum_by_name("bday").entries[0].param("type"))
91 | assert_equal(["date"], card.enum_by_name("bday").entries[0].param("value"))
92 |
93 | assert_equal( 1970, card.enum_by_name("bday").entries[0].to_time[0].year)
94 | assert_equal( 7, card.enum_by_name("bday").entries[0].to_time[0].month)
95 | assert_equal( 14, card.enum_by_name("bday").entries[0].to_time[0].day)
96 |
97 | assert_equal("CATEGORIES: Amis/Famille", card[ "note" ])
98 | end
99 |
100 | def test_nl
101 | lf_card = crlf_card = nil
102 | assert_nothing_thrown {
103 | lf_card = Vcard::Vcard.decode(vcard(:ex3)).first
104 | crlf_encoded = lf_card.encode(nl: "\r\n")
105 | lf_encoded = lf_card.encode
106 | assert_equal(crlf_encoded.split(/\r\n/), lf_encoded.split(/\n/))
107 | crlf_card = Vcard::Vcard.decode(crlf_encoded).first
108 | }
109 |
110 | assert_equal(lf_card["note"], crlf_card["note"])
111 | assert_equal(lf_card["home.label"], crlf_card["home.label"])
112 | end
113 |
114 | def test_nickname
115 | assert_equal(nil, Vcard::Vcard.decode(vcard(:nickname0)).first.nickname)
116 | assert_equal(nil, Vcard::Vcard.decode(vcard(:nickname1)).first.nickname)
117 | assert_equal(nil, Vcard::Vcard.decode(vcard(:nickname2)).first.nickname)
118 | assert_equal('Big Joey', Vcard::Vcard.decode(vcard(:nickname3)).first.nickname)
119 | assert_equal('Big Joey', Vcard::Vcard.decode(vcard(:nickname4)).first['nickname'])
120 | assert_equal(['Big Joey', 'Bob'], Vcard::Vcard.decode(vcard(:nickname5)).first.nicknames)
121 | end
122 |
123 |
124 | # Test data for Vcard.expand
125 | def test_expand
126 | ex_expand =<<'EOF'
127 | BEGIN:a
128 | a1:
129 | BEGIN:b
130 | BEGIN:c
131 | c1:
132 | c2:
133 | END:c
134 | V1:
135 | V2:
136 | END:b
137 | a2:
138 | END:a
139 | EOF
140 | src = Vcard.decode(ex_expand)
141 | dst = Vcard.expand(src)
142 |
143 | assert_equal('a', dst[0][0].value)
144 | assert_equal('A1', dst[0][1].name)
145 | assert_equal('b', dst[0][2][0].value)
146 | assert_equal('c', dst[0][2][1][0].value)
147 | assert_equal('C1', dst[0][2][1][1].name)
148 | assert_equal('C2', dst[0][2][1][2].name)
149 | assert_equal('c', dst[0][2][1][3].value)
150 | end
151 |
152 | # An iCalendar for Vcard.expand
153 | def test_ical_1
154 | src = nil
155 | dst = nil
156 | assert_nothing_thrown do
157 | src = Vcard.decode(vcal(:ex_ical_1))
158 | dst = Vcard.expand(src)
159 | end
160 | end
161 |
162 | # Constructed data.
163 | def _test_cons # FIXME
164 | card = nil
165 | assert_nothing_thrown { card = Vcard::Vcard.decode(vcard(:tst1)).first }
166 | assert_equal(vcard(:tst1), card.to_s)
167 | assert_equal('Healey\'s\n\nLook up exact time.\n', card[ "description" ])
168 |
169 | # Test the [] API
170 | assert_equal(nil, card[ "not-a-field" ])
171 |
172 | assert_equal('firstname', card[ "name" ])
173 |
174 | assert_equal('home@example.com', card[ "email" ])
175 | assert_equal('home@example.com', card[ "email", "pref" ])
176 | assert_equal('home@example.com', card[ "email", "internet" ])
177 | assert_equal('work@example.com', card[ "email", "work" ])
178 |
179 |
180 | # Test the merging of vCard 2.1 type fields.
181 | assert_equal('fax', card[ "fax" ])
182 | assert_equal('fax', card[ "fax", 'bar' ])
183 | end
184 |
185 | def test_bad
186 | Vcard::configuration.raise_on_invalid_line = true
187 | assert_raises(::Vcard::InvalidEncodingError) do
188 | Vcard::Vcard.decode("BEGIN:VCARD\nVERSION:3.0\nKEYencoding=b:this could be \nmy certificate\n\nEND:VCARD\n")
189 | end
190 | end
191 |
192 | def test_not_raise_error_if_configured_to_ignore
193 | Vcard::configuration.raise_on_invalid_line = false
194 | Vcard::configuration.ignore_invalid_vcards = false
195 | assert_nothing_raised do
196 | Vcard::Vcard.decode("BEGIN:VCARD\nVERSION:3.0\nKEYencoding=b:this could be \nmy certificate\n\nEND:VCARD\n")
197 | end
198 | end
199 |
200 | def test_ignore_vcards_with_invalid_fields
201 | Vcard::configuration.raise_on_invalid_line = false
202 | Vcard::configuration.ignore_invalid_vcards = true
203 | src = <<'EOF'
204 | BEGIN:VCARD
205 | VERSION:3.0
206 | KEYencoding=b:this could be
207 | my certificate
208 | EMAIL:valid@field.value
209 | END:VCARD
210 | BEGIN:VCARD
211 | VERSION:3.0
212 | EMAIL:valid@field.value
213 | END:VCARD
214 | EOF
215 |
216 | cards = Vcard::Vcard.decode(src)
217 | assert_equal 1, cards.size
218 | end
219 |
220 | def test_ignore_only_invalid_fields
221 | Vcard::configuration.raise_on_invalid_line = false
222 | Vcard::configuration.ignore_invalid_vcards = false
223 | email = 'test@example.com'
224 | cards = Vcard::Vcard.decode("BEGIN:VCARD\nVERSION:3.0\nKEYencoding=b:this could be \nmy certificate\nEMAIL:#{email}\n\nEND:VCARD\n")
225 | assert_equal email, cards.first.email
226 | # [BEGIN, VERSION, EMAIL, END].size == 4
227 | assert_equal 4, cards.first.fields.size
228 | end
229 |
230 | def test_create
231 | card = Vcard::Vcard.create
232 | key = Vcard::DirectoryInfo.decode("key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK\n")['key']
233 | card << Vcard::DirectoryInfo::Field.create('key', key, 'encoding' => :b64)
234 | assert_equal(key, card['key'])
235 |
236 | field = Vcard::DirectoryInfo::Field.create('key', 'value')
237 | card = assert_nothing_raised {
238 | Vcard::Vcard.create([field].freeze)
239 | }
240 | assert_equal('value', card['key'])
241 | end
242 |
243 | def test_decode_date
244 | assert_equal [2002, 4, 22], Vcard.decode_date(" 20020422 ")
245 | assert_equal [2002, 4, 22], Vcard.decode_date(" 2002-04-22 ")
246 | assert_equal [2002, 4, 22], Vcard.decode_date(" 2002-04-22 \n")
247 | end
248 |
249 | def test_decode_date_list
250 | assert_equal [[2002, 4, 22]], Vcard.decode_date_list(" 2002-04-22 ")
251 | assert_equal [[2002, 4, 22],[2002, 4, 22]], Vcard.decode_date_list(" 2002-04-22, 2002-04-22,")
252 | assert_equal [[2002, 4, 22],[2002, 4, 22]], Vcard.decode_date_list(" 2002-04-22,,, , ,2002-04-22, , \n")
253 | assert_equal [], Vcard.decode_date_list(" , , ")
254 | end
255 |
256 | def test_decode_time
257 | assert_equal [4, 53, 22, 0, nil], Vcard.decode_time(" 04:53:22 \n")
258 | assert_equal [4, 53, 22, 0.10, nil], Vcard.decode_time(" 04:53:22.10 \n")
259 | assert_equal [4, 53, 22, 0.10, "Z"], Vcard.decode_time(" 04:53:22.10Z \n")
260 | assert_equal [4, 53, 22, 0, "Z"], Vcard.decode_time(" 045322Z \n")
261 | assert_equal [4, 53, 22, 0, "+0530"], Vcard.decode_time(" 04:5322+0530 \n")
262 | assert_equal [4, 53, 22, 0.10, "Z"], Vcard.decode_time(" 045322.10Z \n")
263 | end
264 |
265 | def test_decode_date_time
266 | assert_equal [2002, 4, 22, 4, 53, 22, 0, nil], Vcard.decode_date_time("20020422T04:53:22 \n")
267 | assert_equal [2002, 4, 22, 4, 53, 22, 0.10, nil], Vcard.decode_date_time(" 2002-04-22T04:53:22.10 \n")
268 | assert_equal [2002, 4, 22, 4, 53, 22, 0.10, "Z"], Vcard.decode_date_time(" 20020422T04:53:22.10Z \n")
269 | assert_equal [2002, 4, 22, 4, 53, 22, 0, "Z"], Vcard.decode_date_time(" 20020422T045322Z \n")
270 | assert_equal [2002, 4, 22, 4, 53, 22, 0, "+0530"], Vcard.decode_date_time(" 20020422T04:5322+0530 \n")
271 | assert_equal [2002, 4, 22, 4, 53, 22, 0.10, "Z"], Vcard.decode_date_time(" 20020422T045322.10Z \n")
272 | assert_equal [2003, 3, 25, 3, 20, 35, 0, "Z"], Vcard.decode_date_time("20030325T032035Z")
273 | end
274 |
275 | def test_decode_text
276 | assert_equal "aa,\n\n,\\,\\a;;b", Vcard.decode_text('aa,\\n\\n,\\\\\,\\\\a\;\;b')
277 | end
278 |
279 | def test_decode_text_list
280 | assert_equal ['', "1\n2,3", "bbb", '', "zz", ''], Vcard.decode_text_list(',1\\n2\\,3,bbb,,zz,')
281 | end
282 |
283 | def test_create_1
284 | card = Vcard::Vcard.create
285 |
286 | card << DirectoryInfo::Field.create('n', 'Roberts;Sam;;;')
287 | card << DirectoryInfo::Field.create('fn', 'Roberts Sam')
288 | card << DirectoryInfo::Field.create('email', 'sroberts@uniserve.com', 'type' => ['home', 'pref'])
289 | card << DirectoryInfo::Field.create('tel', '416 535 5341', 'type' => 'home')
290 | # TODO - allow the value to be an array, in which case it will be
291 | # concatentated with ';'
292 | card << DirectoryInfo::Field.create('adr', ';;376 Westmoreland Ave.;Toronto;ON;M6H 3A6;Canada', 'type' => ['home', 'pref'])
293 | # TODO - allow the date to be a Date, and for value to be set correctly
294 | card << DirectoryInfo::Field.create('bday', Date.new(1970, 7, 14), 'value' => 'date')
295 | end
296 |
297 | def test_birthday
298 | cards = Vcard::Vcard.decode(vcard(:ex_bdays))
299 |
300 | expected = [
301 | Date.new(Time.now.year, 12, 15),
302 | Date.new(2003, 12, 9),
303 | nil
304 | ]
305 |
306 | expected.each_with_index { | d, i| assert_equal(d, cards[i].birthday) }
307 | end
308 |
309 | def test_attach
310 | card = Vcard::Vcard.decode(vcard(:ex_attach)).first
311 | card.lines # FIXME - assert values are as expected
312 | end
313 |
314 | def test_v21_modification
315 | card0 = Vcard::Vcard.decode(vcard(:ex_21)).first
316 | card1 = Vcard::Vcard::Maker.make2(card0) do |maker|
317 | maker.nickname = 'nickname'
318 | end
319 | card2 = Vcard::Vcard.decode(card1.encode).first
320 |
321 | assert_equal(card0.version, card1.version)
322 | assert_equal(card0.version, card2.version)
323 | end
324 |
325 | def test_v21_versioned_copy
326 | card0 = Vcard::Vcard.decode(vcard(:ex_21)).first
327 | card1 = Vcard::Vcard::Maker.make2(Vcard::DirectoryInfo.create([], 'VCARD')) do |maker|
328 | maker.copy card0
329 | end
330 | card2 = Vcard::Vcard.decode(card1.encode).first
331 |
332 | assert_equal(card0.version, card2.version)
333 | end
334 |
335 | def test_v21_strip_version
336 | card0 = Vcard::Vcard.decode(vcard(:ex_21)).first
337 |
338 | card0.delete card0.field('VERSION')
339 | card0.delete card0.field('TEL')
340 | card0.delete card0.field('TEL')
341 | card0.delete card0.field('TEL')
342 | card0.delete card0.field('TEL')
343 |
344 | assert_raises(ArgumentError) do
345 | card0.delete card0.field('END')
346 | end
347 | assert_raises(ArgumentError) do
348 | card0.delete card0.field('BEGIN')
349 | end
350 |
351 | card1 = Vcard::Vcard::Maker.make2(Vcard::DirectoryInfo.create([], 'VCARD')) do |maker|
352 | maker.copy card0
353 | end
354 | card2 = Vcard::Vcard.decode(card1.encode).first
355 |
356 | assert_equal(30, card2.version)
357 | assert_equal(nil, card2.field('TEL'))
358 | end
359 |
360 |
361 | def test_v21_case0
362 | Vcard::Vcard.decode(vcard(:ex_21_case0)).first
363 | end
364 |
365 | def test_modify_name
366 | card = Vcard::Vcard.decode("begin:vcard\nend:vcard\n").first
367 |
368 | assert_raises(::Vcard::Unencodeable) do
369 | Vcard::Vcard::Maker.make2(card) {}
370 | end
371 |
372 | card.make do |m|
373 | m.name {}
374 | end
375 |
376 | assert_equal('', card.name.given)
377 | assert_equal('', card.name.fullname)
378 |
379 | assert_raises do
380 | card.name.given = 'given'
381 | end
382 |
383 | card.make do |m|
384 | m.name do |n|
385 | n.given = 'given'
386 | end
387 | end
388 |
389 | assert_equal('given', card.name.given)
390 | assert_equal('given', card.name.fullname)
391 | assert_equal('' , card.name.family)
392 |
393 | card.make do |m|
394 | m.name do |n|
395 | n.family = n.given
396 | n.prefix = ' Ser '
397 | n.fullname = 'well given'
398 | end
399 | end
400 |
401 | assert_equal('given', card.name.given)
402 | assert_equal('given', card.name.family)
403 | assert_equal('Ser given given', card.name.formatted)
404 | assert_equal('well given', card.name.fullname)
405 | end
406 |
407 | def test_add_note
408 | note = "hi\' \ \"\",,;; \n \n field"
409 |
410 | card = Vcard::Vcard::Maker.make2 do |m|
411 | m.add_note(note)
412 | m.name {}
413 | end
414 |
415 | assert_equal(note, card.note)
416 | end
417 |
418 | def test_empty_tel
419 | card = Vcard::Vcard.decode(vcard(:empty_tel)).first
420 | assert_equal(card.telephone, nil)
421 | assert_equal(card.telephone('HOME'), nil)
422 | assert_equal([], card.telephones)
423 | end
424 |
425 | def test_slash_in_field_name
426 | card = Vcard::Vcard.decode(vcard(:slash_in_field_name)).first
427 | assert_equal(card.value("X-messaging/xmpp-All"), "some@jabber.id")
428 | assert_equal(card["X-messaging/xmpp-All"], "some@jabber.id")
429 | end
430 |
431 | def test_url_decode
432 | card = Vcard::Vcard.decode(vcard(:url_decode)).first
433 | assert_equal("www.email.com", card.url.uri)
434 | assert_equal("www.email.com", card.url.uri.to_s)
435 | assert_equal("www.email.com", card.urls.first.uri)
436 | assert_equal("www.work.com", card.urls.last.uri)
437 | end
438 |
439 | def test_bday_decode
440 | card = Vcard::Vcard.decode(vcard(:bday_decode)).first
441 |
442 | assert_equal(Date.new(1970, 7, 14), card.birthday)
443 | assert_equal(1, card.values("bday").size)
444 |
445 | # Nobody should have multiple bdays, I hope, but its allowed syntactically,
446 | # so test it, along with some variant forms of BDAY
447 | end
448 |
449 | def test_bday_decode_2
450 | card = Vcard::Vcard.decode(vcard(:bday_decode_2)).first
451 | assert_equal(Date.new(1970, 7, 14), card.birthday)
452 | assert_equal(4, card.values("bday").size)
453 | assert_equal(Date.new(1970, 7, 14), card.values("bday").first)
454 | assert_equal(Date.new(Time.now.year, 7, 14), card.values("bday")[1])
455 | assert_equal(DateTime.new(1970, 7, 15, 3, 45, 12).to_s, card.values("bday")[2].to_s)
456 | assert_equal(DateTime.new(1970, 7, 15, 3, 45, 12).to_s, card.values("bday").last.to_s)
457 | end
458 |
459 | def test_bday_decode_3
460 | card = Vcard::Vcard.decode(vcard(:bday_decode_3)).first
461 |
462 | assert_equal(Date.new(1980, 10, 25), card.birthday)
463 | end
464 |
465 | # Broken output from Highrise. Report to support@highrisehq.com
466 | def test_highrises_invalid_google_talk_field
467 | c = vcard(:highrise)
468 | card = Vcard::Vcard.decode(c).first
469 | assert_equal("Doe", card.name.family)
470 | assert_equal("456 Grandview Building, Wide Street", card.address('work').street)
471 | assert_equal("123 Sweet Home, Narrow Street", card.address('home').street)
472 | assert_equal("John Doe & Partners Limited", card.org.first)
473 | assert_equal("gtalk.john", card.value("x-google talk"))
474 | assert_equal("http\\://www.homepage.com", card.url.uri)
475 | end
476 |
477 | def test_gmail_vcard_export
478 | c = vcard(:gmail)
479 | card = Vcard::Vcard.decode(c).first
480 | assert_equal("123 Home, Home Street\r\nKowloon, N/A\r\nHong Kong", card.value("label"))
481 | end
482 |
483 | def test_title
484 | title = "She Who Must Be Obeyed"
485 | card = Vcard::Vcard::Maker.make2 do |m|
486 | m.name do |n|
487 | n.given = "Hilda"
488 | n.family = "Rumpole"
489 | end
490 | m.title = title
491 | end
492 | assert_equal(title, card.title)
493 | card = Vcard::Vcard.decode(card.encode).first
494 | assert_equal(title, card.title)
495 | end
496 |
497 | def _test_org(*org)
498 | card = Vcard::Vcard::Maker.make2 do |m|
499 | m.name do |n|
500 | n.given = "Hilda"
501 | n.family = "Rumpole"
502 | end
503 | m.org = org
504 | end
505 | assert_equal(org, card.org)
506 | card = Vcard::Vcard.decode(card.encode).first
507 | assert_equal(org, card.org)
508 | end
509 |
510 | def test_org_single
511 | _test_org("Megamix Corp.")
512 | end
513 |
514 | def test_org_multiple
515 | _test_org("Megamix Corp.", "Marketing")
516 | end
517 |
518 | def test_role
519 | card = Vcard::Vcard::Maker.make2 do |m|
520 | m.name do |n|
521 | n.given = "John"
522 | n.family = "Woe"
523 | end
524 | m.add_role "Office Manager\r\n;Something Else"
525 | end
526 | assert_equal "Office Manager\n;Something Else", card.role
527 | assert_match(/Office Manager\\n\\;Something Else/, card.to_s)
528 | card = Vcard::Vcard.decode(card.encode).first
529 | assert_equal "Office Manager\n;Something Else", card.role
530 | end
531 |
532 | def test_note
533 | card = Vcard::Vcard::Maker.make2 do |m|
534 | m.name do |n|
535 | n.given = "John"
536 | n.family = "Woe"
537 | end
538 | m.add_note "line1\r\n;line2"
539 | end
540 | assert_equal "line1\n;line2", card.note
541 | assert_match(/line1\\n\\;line2/, card.to_s)
542 | card = Vcard::Vcard.decode(card.encode).first
543 | assert_equal "line1\n;line2", card.note
544 | end
545 | end
546 |
--------------------------------------------------------------------------------
/vcard.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'vcard/version'
5 |
6 | Gem::Specification.new do |gem|
7 | gem.name = "vcard"
8 | gem.version = Vcard::VERSION
9 | gem.authors = ["Kuba Kuźma"]
10 | gem.email = ["kuba@jah.pl"]
11 | gem.description = %q{Vcard extracted from Vpim}
12 | gem.summary = %q{Vcard extracted from Vpim}
13 | gem.homepage = "http://github.com/qoobaa/vcard"
14 | gem.licenses = "GPL"
15 | gem.files = `git ls-files`.split($/)
16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18 | gem.require_paths = ["lib"]
19 | end
20 |
--------------------------------------------------------------------------------