├── .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 [![CI](https://github.com/qoobaa/vcard/actions/workflows/ci.yml/badge.svg)](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 | --------------------------------------------------------------------------------