jwhitmire-contacts 1.0.17

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ test/accounts.yml
data/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2006, Lucas Carlson, MOG
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ Neither the name of the Lucas Carlson nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10
+
data/README ADDED
@@ -0,0 +1,59 @@
1
+ == Welcome to Contacts
2
+
3
+ Contacts is a universal interface to grab contact list information from various providers including Hotmail, Gmail and Yahoo.
4
+
5
+ == Download
6
+
7
+ * gem install contacts
8
+ * http://rubyforge.org/projects/contacts
9
+ * svn co svn://rubyforge.org/var/svn/contacts
10
+
11
+ == Background
12
+
13
+ For a long time, the only way to get a list of contacts from your free online email accounts was with proprietary PHP scripts that would cost you $50. The act of grabbing that list is a simple matter of screen scrapping and this library gives you all the functionality you need. Thanks to the generosity of the highly popular Rails website MOG (http://mog.com) for allowing this library to be released open-source to the world. It is easy to extend this library to add new free email providers, so please contact the author if you would like to help.
14
+
15
+ == Usage
16
+
17
+ Contacts::Hotmail.new(login, password).contacts # => [["name", "foo@bar.com"], ["another name", "bow@wow.com"]]
18
+ Contacts::Yahoo.new(login, password).contacts
19
+ Contacts::Gmail.new(login, password).contacts
20
+
21
+ Contacts.new(:gmail, login, password).contacts
22
+ Contacts.new(:hotmail, login, password).contacts
23
+ Contacts.new(:yahoo, login, password).contacts
24
+
25
+ Contacts.guess(login, password).contacts
26
+
27
+ Notice there are three ways to use this library so that you can limit the use as much as you would like in your particular application. The Contacts.guess method will automatically concatenate all the address book contacts from each of the successful logins in the case that a username password works across multiple services.
28
+
29
+ == Examples
30
+
31
+ See the examples/ directory.
32
+
33
+ == Authors
34
+
35
+ * Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
36
+
37
+ == Contributors
38
+
39
+ * Britt Selvitelle from Twitter (mailto:anotherbritt@gmail.com) - http://twitter.com
40
+ * Tony Targonski from GigPark (mailto:tony@gigpark.com) - http://gigpark.com
41
+ * Waheed Barghouthi from Watwet (mailto:waheed.barghouthi@gmail.com) - http://watwet.com
42
+
43
+ == Forked by jwhitmire
44
+
45
+ The purpose of my fork is to use the simple structure of the existing contacts gem and expand it to support the new OAuth based authentication APIs provided by email carriers. It would also be nice to have this support all Portable Contact providers (great initiative led by Plaxo). It is also my intention to expand the test coverage and not require real accounts with the providers in order to test the behavior, though some level of connection test is still useful in case they change their API.
46
+
47
+ My Todo List:
48
+
49
+ * Support Gmail Contacts API
50
+ * Support Plaxo Portable Contacts
51
+ * Support Yahoo API
52
+ * Support Microsoft Live API
53
+
54
+ * Add support for Facebook?
55
+ * Add support for LinkedIn?
56
+ * Add support for Twitter?
57
+
58
+ This library is released under the terms of the BSD.
59
+
data/Rakefile ADDED
@@ -0,0 +1,95 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'lib/contacts'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "contacts"
9
+
10
+ gem.summary = %Q{Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail}
11
+ gem.description = <<-DESC
12
+ Seamless importing of contact data from Yahoo, Gmail, Hotmail, and Plaxo. OAuth support is coming.
13
+ DESC
14
+ gem.homepage = "http://rubyforge.org/projects/contacts"
15
+
16
+ gem.email = "jeff@jwhitmire.com"
17
+ gem.homepage = "http://github.com/jwhitmire/contacts"
18
+ gem.authors = ["Lucas Carlson", "Jeff Whitmire"]
19
+
20
+ gem.add_dependency('json', '>= 0.4.1')
21
+ gem.requirements << "A json parser"
22
+
23
+ gem.require_path = 'lib'
24
+
25
+ gem.has_rdoc = true
26
+ gem.files.exclude 'test/accounts.yml'
27
+ gem.files.include 'spec/**/*'
28
+
29
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
30
+ end
31
+ rescue LoadError
32
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
33
+ end
34
+
35
+ require 'rake/testtask'
36
+ Rake::TestTask.new(:test) do |test|
37
+ test.libs << 'lib' << 'test'
38
+ test.pattern = 'test/**/*_test.rb'
39
+ test.verbose = true
40
+ end
41
+
42
+ begin
43
+ require 'rcov/rcovtask'
44
+ Rcov::RcovTask.new do |test|
45
+ test.libs << 'test'
46
+ test.pattern = 'test/**/*_test.rb'
47
+ test.verbose = true
48
+ end
49
+ rescue LoadError
50
+ task :rcov do
51
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
52
+ end
53
+ end
54
+
55
+ task :test => :check_dependencies
56
+
57
+ require 'spec/rake/spectask'
58
+ Spec::Rake::SpecTask.new(:spec) do |spec|
59
+ spec.libs << 'lib' << 'spec'
60
+ spec.spec_files = FileList['spec/**/*_spec.rb']
61
+ end
62
+
63
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
64
+ spec.libs << 'lib' << 'spec'
65
+ spec.pattern = 'spec/**/*_spec.rb'
66
+ spec.rcov = true
67
+ end
68
+
69
+ task :spec => :check_dependencies
70
+
71
+ # task :default => :spec
72
+ task :default => :test
73
+
74
+ require 'rake/rdoctask'
75
+ Rake::RDocTask.new do |rdoc|
76
+ if File.exist?('VERSION')
77
+ version = File.read('VERSION')
78
+ else
79
+ version = ""
80
+ end
81
+
82
+ rdoc.rdoc_dir = 'rdoc'
83
+ rdoc.title = "gemtest #{version}"
84
+ rdoc.rdoc_files.include('README*')
85
+ rdoc.rdoc_files.include('lib/**/*.rb')
86
+ end
87
+
88
+ desc "Report code statistics (KLOCs, etc) from the application"
89
+ task :stats do
90
+ require 'code_statistics'
91
+ CodeStatistics.new(
92
+ ["Library", "lib"],
93
+ ["Units", "test"]
94
+ ).to_s
95
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.17
data/contacts.gemspec ADDED
@@ -0,0 +1,78 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{contacts}
8
+ s.version = "1.0.17"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Lucas Carlson", "Jeff Whitmire"]
12
+ s.date = %q{2009-08-10}
13
+ s.description = %q{ Seamless importing of contact data from Yahoo, Gmail, Hotmail, and Plaxo. OAuth support is coming.
14
+ }
15
+ s.email = %q{jeff@jwhitmire.com}
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "contacts.gemspec",
27
+ "cruise_config.rb",
28
+ "examples/grab_contacts.rb",
29
+ "geminstaller.yml",
30
+ "lib/contacts.rb",
31
+ "lib/contacts/base.rb",
32
+ "lib/contacts/gmail.rb",
33
+ "lib/contacts/hotmail.rb",
34
+ "lib/contacts/plaxo.rb",
35
+ "lib/contacts/yahoo.rb",
36
+ "spec/gemtest_spec.rb",
37
+ "spec/gemtest_spec.rb",
38
+ "spec/spec_helper.rb",
39
+ "spec/spec_helper.rb",
40
+ "test/example_accounts.yml",
41
+ "test/test_helper.rb",
42
+ "test/test_suite.rb",
43
+ "test/unit/gmail_contact_importer_test.rb",
44
+ "test/unit/hotmail_contact_importer_test.rb",
45
+ "test/unit/test_accounts_test.rb",
46
+ "test/unit/yahoo_csv_contact_importer_test.rb"
47
+ ]
48
+ s.homepage = %q{http://github.com/jwhitmire/contacts}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.requirements = ["A json parser"]
52
+ s.rubygems_version = %q{1.3.4}
53
+ s.summary = %q{Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail}
54
+ s.test_files = [
55
+ "spec/gemtest_spec.rb",
56
+ "spec/spec_helper.rb",
57
+ "test/test_helper.rb",
58
+ "test/test_suite.rb",
59
+ "test/unit/gmail_contact_importer_test.rb",
60
+ "test/unit/hotmail_contact_importer_test.rb",
61
+ "test/unit/test_accounts_test.rb",
62
+ "test/unit/yahoo_csv_contact_importer_test.rb",
63
+ "examples/grab_contacts.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
71
+ s.add_runtime_dependency(%q<json>, [">= 0.4.1"])
72
+ else
73
+ s.add_dependency(%q<json>, [">= 0.4.1"])
74
+ end
75
+ else
76
+ s.add_dependency(%q<json>, [">= 0.4.1"])
77
+ end
78
+ end
data/cruise_config.rb ADDED
@@ -0,0 +1,22 @@
1
+ # Project-specific configuration for CruiseControl.rb
2
+
3
+ Project.configure do |project|
4
+
5
+ # Send email notifications about broken and fixed builds to email1@your.site, email2@your.site (default: send to nobody)
6
+ # if building this on your own CI box, please remove!
7
+ project.email_notifier.emails = ['opensource@pivotallabs.com']
8
+
9
+ # Set email 'from' field to john@doe.com:
10
+ # project.email_notifier.from = 'john@doe.com'
11
+
12
+ # Build the project by invoking rake task 'custom'
13
+ # project.rake_task = 'custom'
14
+
15
+ # Build the project by invoking shell script "build_my_app.sh". Keep in mind that when the script is invoked, current working directory is
16
+ # [cruise]/projects/your_project/work, so if you do not keep build_my_app.sh in version control, it should be '../build_my_app.sh' instead
17
+ # project.build_command = 'build_my_app.sh'
18
+
19
+ # Ping Subversion for new revisions every 5 minutes (default: 30 seconds)
20
+ # project.scheduler.polling_interval = 5.minutes
21
+
22
+ end
@@ -0,0 +1,12 @@
1
+ require File.dirname(__FILE__)+"/../lib/contacts"
2
+
3
+ login = ARGV[0]
4
+ password = ARGV[1]
5
+
6
+ Contacts::Gmail.new(login, password).contacts
7
+
8
+ Contacts.new(:gmail, login, password).contacts
9
+
10
+ Contacts.new("gmail", login, password).contacts
11
+
12
+ Contacts.guess(login, password).contacts
data/geminstaller.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ defaults:
3
+ install_options: --no-ri --no-rdoc
4
+ gems:
5
+ - name: json
6
+ version: >= 1.1.1
data/lib/contacts.rb ADDED
@@ -0,0 +1,8 @@
1
+ $:.unshift(File.dirname(__FILE__)+"/contacts/")
2
+
3
+ require 'rubygems'
4
+ require 'base'
5
+ require 'gmail'
6
+ require 'hotmail'
7
+ require 'yahoo'
8
+ require 'plaxo'
@@ -0,0 +1,209 @@
1
+ require "cgi"
2
+ require "net/http"
3
+ require "net/https"
4
+ require "uri"
5
+ require "zlib"
6
+ require "stringio"
7
+ require "thread"
8
+
9
+ class Contacts
10
+ TYPES = {}
11
+
12
+ class Base
13
+ def initialize(login, password)
14
+ @login = login
15
+ @password = password
16
+ @connections = {}
17
+ connect
18
+ end
19
+
20
+ def connect
21
+ raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @password.nil?
22
+ real_connect
23
+ end
24
+
25
+ def connected?
26
+ @cookies && !@cookies.empty?
27
+ end
28
+
29
+ def contacts(options = {})
30
+ return @contacts if @contacts
31
+ if connected?
32
+ url = URI.parse(contact_list_url)
33
+ http = open_http(url)
34
+ resp, data = http.get("#{url.path}?#{url.query}",
35
+ "Cookie" => @cookies
36
+ )
37
+
38
+ if resp.code_type != Net::HTTPOK
39
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
40
+ end
41
+
42
+ parse(data, options)
43
+ end
44
+ end
45
+
46
+ def login
47
+ @attempt ||= 0
48
+ @attempt += 1
49
+
50
+ if @attempt == 1
51
+ @login
52
+ else
53
+ if @login.include?("@#{domain}")
54
+ @login.sub("@#{domain}","")
55
+ else
56
+ "#{@login}@#{domain}"
57
+ end
58
+ end
59
+ end
60
+
61
+ def password
62
+ @password
63
+ end
64
+
65
+ private
66
+
67
+ def domain
68
+ @d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
69
+ end
70
+
71
+ def contact_list_url
72
+ self.class.const_get(:CONTACT_LIST_URL)
73
+ end
74
+
75
+ def address_book_url
76
+ self.class.const_get(:ADDRESS_BOOK_URL)
77
+ end
78
+
79
+ def open_http(url)
80
+ c = @connections[Thread.current.object_id] ||= {}
81
+ http = c["#{url.host}:#{url.port}"]
82
+ unless http
83
+ http = Net::HTTP.new(url.host, url.port)
84
+ if url.port == 443
85
+ http.use_ssl = true
86
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
87
+ end
88
+ c["#{url.host}:#{url.port}"] = http
89
+ end
90
+ http.start unless http.started?
91
+ http
92
+ end
93
+
94
+ def parse_cookies(data, existing="")
95
+ return existing if data.nil?
96
+
97
+ cookies = existing.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
98
+
99
+ data.gsub!(/ ?[\w]+=EXPIRED;/,'')
100
+ data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
101
+ data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
102
+ data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
103
+ data.gsub!(/(;\s*){2,}/,', ')
104
+ data.gsub!(/(,\s*){2,}/,', ')
105
+ data.sub!(/^,\s*/,'')
106
+ data.sub!(/\s*,$/,'')
107
+
108
+ data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
109
+ k, v = data.split("=", 2).map{|j|j.strip}
110
+ if cookies[k] && v.empty?
111
+ cookies.delete(k)
112
+ elsif v && !v.empty?
113
+ cookies[k] = v
114
+ end
115
+ end
116
+
117
+ cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
118
+ end
119
+
120
+ def remove_cookie(cookie, cookies)
121
+ parse_cookies("#{cookie}=", cookies)
122
+ end
123
+
124
+ def post(url, postdata, cookies="", referer="")
125
+ url = URI.parse(url)
126
+ http = open_http(url)
127
+ resp, data = http.post(url.path, postdata,
128
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
129
+ "Accept-Encoding" => "gzip",
130
+ "Cookie" => cookies,
131
+ "Referer" => referer,
132
+ "Content-Type" => 'application/x-www-form-urlencoded'
133
+ )
134
+ data = uncompress(resp, data)
135
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
136
+ forward = resp.response['Location']
137
+ forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
138
+ if (not forward.nil?) && URI.parse(forward).host.nil?
139
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
140
+ end
141
+ return data, resp, cookies, forward
142
+ end
143
+
144
+ def get(url, cookies="", referer="")
145
+ url = URI.parse(url)
146
+ http = open_http(url)
147
+ resp, data = http.get("#{url.path}?#{url.query}",
148
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
149
+ "Accept-Encoding" => "gzip",
150
+ "Cookie" => cookies,
151
+ "Referer" => referer
152
+ )
153
+ data = uncompress(resp, data)
154
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
155
+ forward = resp.response['Location']
156
+ if (not forward.nil?) && URI.parse(forward).host.nil?
157
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
158
+ end
159
+ return data, resp, cookies, forward
160
+ end
161
+
162
+ def uncompress(resp, data)
163
+ case resp.response['content-encoding']
164
+ when 'gzip':
165
+ gz = Zlib::GzipReader.new(StringIO.new(data))
166
+ data = gz.read
167
+ gz.close
168
+ resp.response['content-encoding'] = nil
169
+ # FIXME: Not sure what Hotmail was feeding me with their 'deflate',
170
+ # but the headers definitely were not right
171
+ when 'deflate':
172
+ data = Zlib::Inflate.inflate(data)
173
+ resp.response['content-encoding'] = nil
174
+ end
175
+
176
+ data
177
+ end
178
+ end
179
+
180
+ class ContactsError < StandardError
181
+ end
182
+
183
+ class AuthenticationError < ContactsError
184
+ end
185
+
186
+ class ConnectionError < ContactsError
187
+ end
188
+
189
+ class TypeNotFound < ContactsError
190
+ end
191
+
192
+ def self.new(type, login, password)
193
+ if TYPES.include?(type.to_s.intern)
194
+ TYPES[type.to_s.intern].new(login, password)
195
+ else
196
+ raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect}"
197
+ end
198
+ end
199
+
200
+ def self.guess(login, password)
201
+ TYPES.inject([]) do |a, t|
202
+ begin
203
+ a + t[1].new(login, password).contacts
204
+ rescue AuthenticationError
205
+ a
206
+ end
207
+ end.uniq
208
+ end
209
+ end
@@ -0,0 +1,94 @@
1
+ begin
2
+ # If the json gem is available, use it
3
+ require "json/add/rails"
4
+ rescue MissingSourceFile
5
+ # Otherwise wrap the ActiveSupport JSON implementation for our simple use case
6
+ class JSON
7
+ def self.parse(i)
8
+ ActiveSupport::JSON.decode(i)
9
+ end
10
+ end
11
+ end
12
+
13
+ class Contacts
14
+ class Gmail < Base
15
+ URL = "https://mail.google.com/mail/"
16
+ LOGIN_URL = "https://www.google.com/accounts/ServiceLoginAuth"
17
+ LOGIN_REFERER_URL = "https://www.google.com/accounts/ServiceLogin?service=mail&passive=true&rm=false&continue=http%3A%2F%2Fmail.google.com%2Fmail%3Fui%3Dhtml%26zy%3Dl&ltmpl=yj_blanco&ltmplcache=2&hl=en"
18
+ CONTACT_LIST_URL = "https://mail.google.com/mail/contacts/data/contacts?thumb=true&show=ALL&enums=true&psort=Name&max=10000&out=js&rf=&jsx=true"
19
+ PROTOCOL_ERROR = "Gmail has changed its protocols, please upgrade this library first. If that does not work, dive into the code and submit a patch at http://github.com/cardmagic/contacts"
20
+
21
+ def real_connect
22
+ postdata = "ltmpl=yj_blanco"
23
+ postdata += "&continue=%s" % CGI.escape(URL)
24
+ postdata += "&ltmplcache=2"
25
+ postdata += "&service=mail"
26
+ postdata += "&rm=false"
27
+ postdata += "&ltmpl=yj_blanco"
28
+ postdata += "&hl=en"
29
+ postdata += "&Email=%s" % CGI.escape(login)
30
+ postdata += "&Passwd=%s" % CGI.escape(password)
31
+ postdata += "&rmShown=1"
32
+ postdata += "&null=Sign+in"
33
+
34
+ time = Time.now.to_i
35
+ time_past = Time.now.to_i - 8 - rand(12)
36
+ cookie = "GMAIL_LOGIN=T#{time_past}/#{time_past}/#{time}"
37
+
38
+ data, resp, cookies, forward, old_url = post(LOGIN_URL, postdata, cookie, LOGIN_REFERER_URL) + [LOGIN_URL]
39
+
40
+ cookies = remove_cookie("GMAIL_LOGIN", cookies)
41
+
42
+ if data.index("Username and password do not match") || data.index("New to Gmail? It's free and easy")
43
+ raise AuthenticationError, "Username and password do not match"
44
+ elsif data.index("The username or password you entered is incorrect")
45
+ raise AuthenticationError, "Username and password do not match"
46
+ elsif data.index("Required field must not be blank")
47
+ raise AuthenticationError, "Login and password must not be blank"
48
+ elsif data.index("errormsg_0_logincaptcha")
49
+ raise AuthenticationError, "Captcha error"
50
+ elsif data.index("Invalid request")
51
+ raise ConnectionError, PROTOCOL_ERROR
52
+ elsif cookies == ""
53
+ raise ConnectionError, PROTOCOL_ERROR
54
+ end
55
+
56
+ cookies = remove_cookie("LSID", cookies)
57
+ cookies = remove_cookie("GV", cookies)
58
+
59
+ @cookies = cookies
60
+ end
61
+
62
+ private
63
+
64
+ def parse(data, options)
65
+ data.gsub!(/^while \(true\); &&&START&&&/, '')
66
+ data.gsub!(/ &&&END&&&$/, '')
67
+ data.gsub!(/\t/, ' ') # tabs in the note field cause errors with JSON.parse
68
+ data.gsub!(/[\t\x00-\x1F]/, " ") # strip control characters
69
+
70
+ @contacts = JSON.parse(data)['Body']['Contacts'] || {}
71
+
72
+ # Determine in which format to return the data.
73
+
74
+ # Return the full JSON Hash.
75
+ return @contacts if(options[:details])
76
+
77
+ # Default format.
78
+ # ['Name', 'Email1', 'Email2', ...]
79
+ if @contacts != nil
80
+ @contacts = @contacts.delete_if {|c| c["Emails"].nil?}.map do |c|
81
+ name, emails = c.values_at "Name", "Emails"
82
+ # emails are returned in a form of
83
+ # [{"Address"=>"home.email@gmail.com"}, {"Type"=>{"Id"=>"WORK"}, "Address"=>"work.email@gmail.com"}]
84
+ emails = emails.collect{|a| a.values_at("Address")}
85
+ [name, emails].flatten
86
+ end
87
+ else
88
+ []
89
+ end
90
+ end
91
+ end
92
+
93
+ TYPES[:gmail] = Gmail
94
+ end
@@ -0,0 +1,125 @@
1
+ class Contacts
2
+ class Hotmail < Base
3
+ URL = "https://login.live.com/login.srf?id=2"
4
+ OLD_CONTACT_LIST_URL = "http://%s/cgi-bin/addresses"
5
+ NEW_CONTACT_LIST_URL = "http://%s/mail/GetContacts.aspx"
6
+ CONTACT_LIST_URL = "http://mpeople.live.com/default.aspx?pg=0"
7
+ COMPOSE_URL = "http://%s/cgi-bin/compose?"
8
+ PROTOCOL_ERROR = "Hotmail has changed its protocols, please upgrade this library first. If that does not work, report this error at http://rubyforge.org/forum/?group_id=2693"
9
+ PWDPAD = "IfYouAreReadingThisYouHaveTooMuchFreeTime"
10
+ MAX_HTTP_THREADS = 8
11
+
12
+ def real_connect
13
+ data, resp, cookies, forward = get(URL)
14
+ old_url = URL
15
+ until forward.nil?
16
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
17
+ end
18
+
19
+ postdata = "PPSX=%s&PwdPad=%s&login=%s&passwd=%s&LoginOptions=2&PPFT=%s" % [
20
+ CGI.escape(data.split("><").grep(/PPSX/).first[/=\S+$/][2..-3]),
21
+ PWDPAD[0...(PWDPAD.length-@password.length)],
22
+ CGI.escape(login),
23
+ CGI.escape(password),
24
+ CGI.escape(data.split("><").grep(/PPFT/).first[/=\S+$/][2..-3])
25
+ ]
26
+
27
+ form_url = data.split("><").grep(/form/).first.split[5][8..-2]
28
+ data, resp, cookies, forward = post(form_url, postdata, cookies)
29
+
30
+ if data.index("The e-mail address or password is incorrect")
31
+ raise AuthenticationError, "Username and password do not match"
32
+ elsif data != ""
33
+ raise AuthenticationError, "Required field must not be blank"
34
+ elsif cookies == ""
35
+ raise ConnectionError, PROTOCOL_ERROR
36
+ end
37
+
38
+ old_url = form_url
39
+
40
+ until forward.nil?
41
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
42
+ end
43
+
44
+ data, resp, cookies, forward = get("http://mail.live.com/mail", cookies)
45
+ until forward.nil?
46
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
47
+ end
48
+
49
+ @domain = URI.parse(old_url).host
50
+ @cookies = cookies
51
+ rescue AuthenticationError => m
52
+ if @attempt == 1
53
+ retry
54
+ else
55
+ raise m
56
+ end
57
+ end
58
+
59
+ def contacts(options = {})
60
+ if connected?
61
+ url = URI.parse(contact_list_url)
62
+ data, resp, cookies, forward = get( contact_list_url, @cookies )
63
+
64
+ if resp.code_type != Net::HTTPOK
65
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
66
+ end
67
+
68
+ @contacts = []
69
+ build_contacts = []
70
+ go = true
71
+ index = 0
72
+
73
+ while(go) do
74
+ go = false
75
+ url = URI.parse(get_contact_list_url(index))
76
+ http = open_http(url)
77
+ resp, data = http.get(get_contact_list_url(index), "Cookie" => @cookies)
78
+
79
+ email_match_text_beginning = Regexp.escape("http://m.mail.live.com/?rru=compose&amp;to=")
80
+ email_match_text_end = Regexp.escape("&amp;")
81
+
82
+ raw_html = resp.body.grep(/(?:e|dn)lk[0-9]+/)
83
+ raw_html.delete_at 0
84
+ raw_html.inject do |memo, row|
85
+ c_info = row.match(/(e|dn)lk([0-9])+/)
86
+
87
+ # Same contact, or different?
88
+ build_contacts << [] if memo != c_info[2]
89
+
90
+ # Grab info
91
+ case c_info[1]
92
+ when "e" # Email
93
+ build_contacts.last[1] = row.match(/#{email_match_text_beginning}(.*)#{email_match_text_end}/)[1]
94
+ when "dn" # Name
95
+ build_contacts.last[0] = row.match(/<a[^>]*>(.+)<\/a>/)[1]
96
+ end
97
+
98
+ # Set memo to contact id
99
+ c_info[2]
100
+ end
101
+
102
+ go = resp.body.include?("Next page")
103
+ index += 1
104
+ end
105
+
106
+ build_contacts.each do |contact|
107
+ unless contact[1].nil?
108
+ # Only return contacts with email addresses
109
+ contact[1] = CGI::unescape(contact[1])
110
+ @contacts << contact
111
+ end
112
+ end
113
+ return @contacts
114
+ end
115
+ end
116
+
117
+ def get_contact_list_url(index)
118
+ "http://mpeople.live.com/default.aspx?pg=#{index}"
119
+ end
120
+
121
+ private
122
+
123
+ TYPES[:hotmail] = Hotmail
124
+ end
125
+ end
@@ -0,0 +1,130 @@
1
+ require 'rexml/document'
2
+
3
+ class Contacts
4
+ class Plaxo < Base
5
+ URL = "http://www.plaxo.com/"
6
+ LOGIN_URL = "https://www.plaxo.com/signin"
7
+ ADDRESS_BOOK_URL = "http://www.plaxo.com/po3/?module=ab&operation=viewFull&mode=normal"
8
+ CONTACT_LIST_URL = "http://www.plaxo.com/axis/soap/contact?_action=getContacts&_format=xml"
9
+ PROTOCOL_ERROR = "Plaxo has changed its protocols, please upgrade this library first. If that does not work, dive into the code and submit a patch at http://github.com/cardmagic/contacts"
10
+
11
+ def real_connect
12
+
13
+ end # real_connect
14
+
15
+ def contacts
16
+ getdata = "&authInfo.authByEmail.email=%s" % CGI.escape(login)
17
+ getdata += "&authInfo.authByEmail.password=%s" % CGI.escape(password)
18
+ data, resp, cookies, forward = get(CONTACT_LIST_URL + getdata)
19
+
20
+ if resp.code_type != Net::HTTPOK
21
+ raise ConnectionError, PROTOCOL_ERROR
22
+ end
23
+
24
+ parse data
25
+ end # contacts
26
+
27
+ private
28
+ def parse(data, options={})
29
+ doc = REXML::Document.new(data)
30
+ code = doc.elements['//response/code'].text
31
+
32
+ if code == '401'
33
+ raise AuthenticationError, "Username and password do not match"
34
+ elsif code == '200'
35
+ @contacts = []
36
+ doc.elements.each('//contact') do |cont|
37
+ name = if cont.elements['fullName']
38
+ cont.elements['fullName'].text
39
+ elsif cont.elements['displayName']
40
+ cont.elements['displayName'].text
41
+ end
42
+ email = if cont.elements['email1']
43
+ cont.elements['email1'].text
44
+ end
45
+ if name || email
46
+ @contacts << [name, email]
47
+ end
48
+ end
49
+ @contacts
50
+ else
51
+ raise ConnectionError, PROTOCOL_ERROR
52
+ end
53
+
54
+ end # parse
55
+
56
+ end # Plaxo
57
+
58
+ TYPES[:plaxo] = Plaxo
59
+
60
+ end # Contacts
61
+
62
+
63
+ # sample contacts responses
64
+ =begin
65
+ Bad email
66
+ =========
67
+ <?xml version="1.0" encoding="utf-8" ?>
68
+ <ns1:GetContactsResponse xmlns:ns1="Plaxo">
69
+ <response>
70
+ <code>401</code>
71
+ <subCode>1</subCode>
72
+ <message>User not found.</message>
73
+ </response>
74
+ </ns1:GetContactsResponse>
75
+
76
+
77
+ Bad password
78
+ ============
79
+ <?xml version="1.0" encoding="utf-8" ?>
80
+ <ns1:GetContactsResponse xmlns:ns1="Plaxo">
81
+ <response>
82
+ <code>401</code>
83
+ <subCode>4</subCode>
84
+ <message>Bad password or security token.</message>
85
+ </response>
86
+ </ns1:GetContactsResponse>
87
+
88
+
89
+ Success
90
+ =======
91
+ <?xml version="1.0" encoding="utf-8" ?>
92
+ <ns1:GetContactsResponse xmlns:ns1="Plaxo">
93
+
94
+ <response>
95
+ <code>200</code>
96
+ <message>OK</message>
97
+ <userId>77311236242</userId>
98
+ </response>
99
+
100
+ <contacts>
101
+
102
+ <contact>
103
+ <itemId>61312569</itemId>
104
+ <displayName>Joe Blow1</displayName>
105
+ <fullName>Joe Blow1</fullName>
106
+ <firstName>Joe</firstName>
107
+ <lastName>Blow1</lastName>
108
+ <homeEmail1>joeblow1@mailinator.com</homeEmail1>
109
+ <email1>joeblow1@mailinator.com</email1>
110
+ <folderId>5291351</folderId>
111
+ </contact>
112
+
113
+ <contact>
114
+ <itemId>61313159</itemId>
115
+ <displayName>Joe Blow2</displayName>
116
+ <fullName>Joe Blow2</fullName>
117
+ <firstName>Joe</firstName>
118
+ <lastName>Blow2</lastName>
119
+ <homeEmail1>joeblow2@mailinator.com</homeEmail1>
120
+ <email1>joeblow2@mailinator.com</email1>
121
+ <folderId>5291351</folderId>
122
+ </contact>
123
+
124
+ </contacts>
125
+
126
+ <totalCount>2</totalCount>
127
+ <editCounter>3</editCounter>
128
+
129
+ </ns1:GetContactsResponse>
130
+ =end
@@ -0,0 +1,82 @@
1
+ require 'csv'
2
+
3
+ class Contacts
4
+ class Yahoo < Base
5
+ URL = "http://mail.yahoo.com/"
6
+ LOGIN_URL = "https://login.yahoo.com/config/login"
7
+ ADDRESS_BOOK_URL = "http://address.mail.yahoo.com/?1&VPC=import_export"
8
+ CONTACT_LIST_URL = "http://address.yahoo.com/index.php?VPC=import_export&A=B&submit[action_export_yahoo]=Export%20Now"
9
+ PROTOCOL_ERROR = "Yahoo has changed its protocols, please upgrade this library first. If that does not work, dive into the code and submit a patch at http://github.com/cardmagic/contacts"
10
+
11
+ def real_connect
12
+ postdata = ".tries=2&.src=ym&.md5=&.hash=&.js=&.last=&promo=&.intl=us&.bypass="
13
+ postdata += "&.partner=&.u=4eo6isd23l8r3&.v=0&.challenge=gsMsEcoZP7km3N3NeI4mX"
14
+ postdata += "kGB7zMV&.yplus=&.emailCode=&pkg=&stepid=&.ev=&hasMsgr=1&.chkP=Y&."
15
+ postdata += "done=#{CGI.escape(URL)}&login=#{CGI.escape(login)}&passwd=#{CGI.escape(password)}"
16
+
17
+ data, resp, cookies, forward = post(LOGIN_URL, postdata)
18
+
19
+ if data.index("Invalid ID or password") || data.index("This ID is not yet taken")
20
+ raise AuthenticationError, "Username and password do not match"
21
+ elsif data.index("Sign in") && data.index("to Yahoo!")
22
+ raise AuthenticationError, "Required field must not be blank"
23
+ elsif !data.match(/uncompressed\/chunked/)
24
+ raise ConnectionError, PROTOCOL_ERROR
25
+ elsif cookies == ""
26
+ raise ConnectionError, PROTOCOL_ERROR
27
+ end
28
+
29
+ data, resp, cookies, forward = get(forward, cookies, LOGIN_URL)
30
+
31
+ if resp.code_type != Net::HTTPOK
32
+ raise ConnectionError, PROTOCOL_ERROR
33
+ end
34
+
35
+ @cookies = cookies
36
+ end
37
+
38
+ def contacts
39
+ return @contacts if @contacts
40
+ if connected?
41
+ # first, get the addressbook site with the new crumb parameter
42
+ url = URI.parse(address_book_url)
43
+ http = open_http(url)
44
+ resp, data = http.get("#{url.path}?#{url.query}",
45
+ "Cookie" => @cookies
46
+ )
47
+
48
+ if resp.code_type != Net::HTTPOK
49
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
50
+ end
51
+
52
+ crumb = data.to_s[/id="crumb2" value="(.*?)"/][19...-1]
53
+
54
+ # now proceed with the new ".crumb" parameter to get the csv data
55
+ url = URI.parse("#{contact_list_url}&.crumb=#{crumb}")
56
+ http = open_http(url)
57
+ resp, data = http.get("#{url.path}?#{url.query}",
58
+ "Cookie" => @cookies
59
+ )
60
+
61
+ if resp.code_type != Net::HTTPOK
62
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
63
+ end
64
+
65
+ parse data
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def parse(data, options={})
72
+ data = CSV.parse(data)
73
+ col_names = data.shift
74
+ @contacts = data.map do |person|
75
+ [[person[0], person[1], person[2]].delete_if{|i|i.empty?}.join(" "), person[4]] unless person[4].empty?
76
+ end.compact
77
+ end
78
+
79
+ end
80
+
81
+ TYPES[:yahoo] = Yahoo
82
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Gemtest" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'gemtest'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
@@ -0,0 +1,40 @@
1
+ gmail:
2
+ username: <changeme>
3
+ password: <changeme>
4
+ contacts:
5
+ -
6
+ name: "FirstName1 LastName1"
7
+ email_address: "firstname1@example.com"
8
+ -
9
+ name: "FirstName2 LastName2"
10
+ email_address: "firstname2@example.com"
11
+ yahoo:
12
+ username: <changeme>
13
+ password: <changeme>
14
+ contacts:
15
+ -
16
+ name: "FirstName1 LastName1"
17
+ email_address: "firstname1@example.com"
18
+ -
19
+ name: "FirstName2 LastName2"
20
+ email_address: "firstname2@example.com"
21
+ hotmail:
22
+ username: <changeme>
23
+ password: <changeme>
24
+ contacts:
25
+ -
26
+ name: "FirstName1 LastName1"
27
+ email_address: "firstname1@example.com"
28
+ -
29
+ name: "FirstName2 LastName2"
30
+ email_address: "firstname2@example.com"
31
+ aol:
32
+ username: <changeme>
33
+ password: <changeme>
34
+ contacts:
35
+ -
36
+ name: "FirstName1 LastName1"
37
+ email_address: "firstname1@example.com"
38
+ -
39
+ name: "FirstName2 LastName2"
40
+ email_address: "firstname2@example.com"
@@ -0,0 +1,30 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(dir + "/../lib/")
3
+ require 'test/unit'
4
+ require 'contacts'
5
+
6
+ class ContactImporterTestCase < Test::Unit::TestCase
7
+ # Add more helper methods to be used by all tests here...
8
+ def default_test
9
+ assert true
10
+ end
11
+ end
12
+
13
+ class TestAccounts
14
+ def self.[](type)
15
+ load[type]
16
+ end
17
+
18
+ def self.load(file = File.dirname(__FILE__) + "/accounts.yml")
19
+ raise "/test/accounts.yml file not found, please create, see /test/example_accounts.yml for information" unless File.exist?(file)
20
+
21
+ accounts = {}
22
+ YAML::load(File.open(file)).each do |type, contents|
23
+ contacts = contents["contacts"].collect {|contact| [contact["name"], contact["email_address"]]}
24
+ accounts[type.to_sym] = Account.new(type.to_sym, contents["username"], contents["password"], contacts)
25
+ end
26
+ accounts
27
+ end
28
+
29
+ Account = Struct.new :type, :username, :password, :contacts
30
+ end
@@ -0,0 +1,4 @@
1
+ dir = File.dirname(__FILE__)
2
+ Dir["#{dir}/**/*_test.rb"].each do |file|
3
+ require file
4
+ end
@@ -0,0 +1,39 @@
1
+ dir = File.dirname(__FILE__)
2
+ require "#{dir}/../test_helper"
3
+ require 'contacts'
4
+
5
+ class GmailContactImporterTest < ContactImporterTestCase
6
+ def setup
7
+ super
8
+ @account = TestAccounts[:gmail]
9
+ end
10
+
11
+ def test_successful_login
12
+ Contacts.new(:gmail, @account.username, @account.password)
13
+ end
14
+
15
+ def test_importer_fails_with_invalid_password
16
+ assert_raise(Contacts::AuthenticationError) do
17
+ Contacts.new(:gmail, @account.username, "wrong_password")
18
+ end
19
+ end
20
+
21
+ def test_importer_fails_with_blank_password
22
+ assert_raise(Contacts::AuthenticationError) do
23
+ Contacts.new(:gmail, @account.username, "")
24
+ end
25
+ end
26
+
27
+ def test_importer_fails_with_blank_username
28
+ assert_raise(Contacts::AuthenticationError) do
29
+ Contacts.new(:gmail, "", @account.password)
30
+ end
31
+ end
32
+
33
+ def test_fetch_contacts
34
+ contacts = Contacts.new(:gmail, @account.username, @account.password).contacts
35
+ @account.contacts.each do |contact|
36
+ assert contacts.include?(contact), "Could not find: #{contact.inspect} in #{contacts.inspect}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ dir = File.dirname(__FILE__)
2
+ require "#{dir}/../test_helper"
3
+ require 'contacts'
4
+
5
+ class HotmailContactImporterTest < ContactImporterTestCase
6
+ def setup
7
+ super
8
+ @account = TestAccounts[:hotmail]
9
+ end
10
+
11
+ def test_successful_login
12
+ Contacts.new(:hotmail, @account.username, @account.password)
13
+ end
14
+
15
+ def test_importer_fails_with_invalid_password
16
+ assert_raise(Contacts::AuthenticationError) do
17
+ Contacts.new(:hotmail, @account.username,"wrong_password")
18
+ end
19
+ end
20
+
21
+ def test_fetch_contacts
22
+ contacts = Contacts.new(:hotmail, @account.username, @account.password).contacts
23
+ @account.contacts.each do |contact|
24
+ assert contacts.include?(contact), "Could not find: #{contact.inspect} in #{contacts.inspect}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ dir = File.dirname(__FILE__)
2
+ require "#{dir}/../test_helper"
3
+
4
+ class TestAccountsTest < ContactImporterTestCase
5
+ def test_test_accounts_loads_data_from_example_accounts_file
6
+ account = TestAccounts.load(File.dirname(__FILE__) + "/../example_accounts.yml")[:gmail]
7
+
8
+ assert_equal :gmail, account.type
9
+ assert_equal "<changeme>", account.username
10
+ assert_equal "<changeme>", account.password
11
+ assert_equal [["FirstName1 LastName1", "firstname1@example.com"], ["FirstName2 LastName2", "firstname2@example.com"]], account.contacts
12
+ end
13
+
14
+ def test_test_accounts_blows_up_if_file_doesnt_exist
15
+ assert_raise(RuntimeError) do
16
+ TestAccounts.load("file_that_does_not_exist.yml")
17
+ end
18
+ end
19
+
20
+ def test_we_can_load_from_account_file
21
+ assert_not_nil TestAccounts[:gmail].username
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ dir = File.dirname(__FILE__)
2
+ require "#{dir}/../test_helper"
3
+ require 'contacts'
4
+
5
+ class YahooContactImporterTest < ContactImporterTestCase
6
+ def setup
7
+ super
8
+ @account = TestAccounts[:yahoo]
9
+ end
10
+
11
+ def test_successful_login
12
+ Contacts.new(:yahoo, @account.username, @account.password)
13
+ end
14
+
15
+ def test_importer_fails_with_invalid_password
16
+ assert_raise(Contacts::AuthenticationError) do
17
+ Contacts.new(:yahoo, @account.username,"wrong_password")
18
+ end
19
+ # run the "successful" login test to ensure we reset yahoo's failed login lockout counter
20
+ # See http://www.pivotaltracker.com/story/show/138210
21
+ assert_nothing_raised do
22
+ Contacts.new(:yahoo, @account.username, @account.password)
23
+ end
24
+ end
25
+
26
+ def test_fetch_contacts
27
+ contacts = Contacts.new(:yahoo, @account.username, @account.password).contacts
28
+ @account.contacts.each do |contact|
29
+ assert contacts.include?(contact), "Could not find: #{contact.inspect} in #{contacts.inspect}"
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwhitmire-contacts
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.17
5
+ platform: ruby
6
+ authors:
7
+ - Lucas Carlson
8
+ - Jeff Whitmire
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-08-10 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: json
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 0.4.1
25
+ version:
26
+ description: Seamless importing of contact data from Yahoo, Gmail, Hotmail, and Plaxo. OAuth support is coming.
27
+ email: jeff@jwhitmire.com
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - LICENSE
34
+ - README
35
+ files:
36
+ - .gitignore
37
+ - LICENSE
38
+ - README
39
+ - Rakefile
40
+ - VERSION
41
+ - contacts.gemspec
42
+ - cruise_config.rb
43
+ - examples/grab_contacts.rb
44
+ - geminstaller.yml
45
+ - lib/contacts.rb
46
+ - lib/contacts/base.rb
47
+ - lib/contacts/gmail.rb
48
+ - lib/contacts/hotmail.rb
49
+ - lib/contacts/plaxo.rb
50
+ - lib/contacts/yahoo.rb
51
+ - spec/gemtest_spec.rb
52
+ - spec/spec_helper.rb
53
+ - test/example_accounts.yml
54
+ - test/test_helper.rb
55
+ - test/test_suite.rb
56
+ - test/unit/gmail_contact_importer_test.rb
57
+ - test/unit/hotmail_contact_importer_test.rb
58
+ - test/unit/test_accounts_test.rb
59
+ - test/unit/yahoo_csv_contact_importer_test.rb
60
+ has_rdoc: false
61
+ homepage: http://github.com/jwhitmire/contacts
62
+ licenses:
63
+ post_install_message:
64
+ rdoc_options:
65
+ - --charset=UTF-8
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ requirements:
81
+ - A json parser
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.5
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
87
+ test_files:
88
+ - spec/gemtest_spec.rb
89
+ - spec/spec_helper.rb
90
+ - test/test_helper.rb
91
+ - test/test_suite.rb
92
+ - test/unit/gmail_contact_importer_test.rb
93
+ - test/unit/hotmail_contact_importer_test.rb
94
+ - test/unit/test_accounts_test.rb
95
+ - test/unit/yahoo_csv_contact_importer_test.rb
96
+ - examples/grab_contacts.rb