contacts 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,37 @@
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
+ * Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
35
+
36
+ This library is released under the terms of the BSD.
37
+
@@ -0,0 +1,90 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/contrib/rubyforgepublisher'
7
+ require 'lib/contacts'
8
+
9
+ PKG_VERSION = Contacts::VERSION
10
+
11
+ PKG_FILES = FileList[
12
+ "lib/**/*", "bin/*", "test/**/*", "[A-Z]*", "Rakefile", "doc/**/*", "examples/**/*"
13
+ ]
14
+
15
+ desc "Default Task"
16
+ task :default => [ :test ]
17
+
18
+ # Run the unit tests
19
+ desc "Run all unit tests"
20
+ Rake::TestTask.new("test") { |t|
21
+ t.libs << "lib"
22
+ t.pattern = 'test/*/*_test.rb'
23
+ t.verbose = true
24
+ }
25
+
26
+ # Make a console, useful when working on tests
27
+ desc "Generate a test console"
28
+ task :console do
29
+ verbose( false ) { sh "irb -I lib/ -r 'contacts'" }
30
+ end
31
+
32
+ # Genereate the RDoc documentation
33
+ desc "Create documentation"
34
+ Rake::RDocTask.new("doc") { |rdoc|
35
+ rdoc.title = "Contact List - ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail"
36
+ rdoc.rdoc_dir = 'doc'
37
+ rdoc.rdoc_files.include('README')
38
+ rdoc.rdoc_files.include('lib/**/*.rb')
39
+ }
40
+
41
+ # Genereate the package
42
+ spec = Gem::Specification.new do |s|
43
+
44
+ #### Basic information.
45
+
46
+ s.name = 'contacts'
47
+ s.version = PKG_VERSION
48
+ s.summary = <<-EOF
49
+ Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
50
+ EOF
51
+ s.description = <<-EOF
52
+ Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
53
+ EOF
54
+
55
+ #### Which files are to be included in this gem? Everything! (Except CVS directories.)
56
+
57
+ s.files = PKG_FILES
58
+
59
+ #### Load-time details: library and application (you will need one or both).
60
+
61
+ s.require_path = 'lib'
62
+ s.autorequire = 'contacts'
63
+
64
+ s.add_dependency('json', '>= 0.4.1')
65
+ s.requirements << "A json parser"
66
+
67
+ #### Documentation and testing.
68
+
69
+ s.has_rdoc = true
70
+
71
+ #### Author and project details.
72
+
73
+ s.author = "Lucas Carlson"
74
+ s.email = "lucas@rufy.com"
75
+ s.homepage = "http://rubyforge.org/projects/contacts"
76
+ end
77
+
78
+ Rake::GemPackageTask.new(spec) do |pkg|
79
+ pkg.need_zip = true
80
+ pkg.need_tar = true
81
+ end
82
+
83
+ desc "Report code statistics (KLOCs, etc) from the application"
84
+ task :stats do
85
+ require 'code_statistics'
86
+ CodeStatistics.new(
87
+ ["Library", "lib"],
88
+ ["Units", "test"]
89
+ ).to_s
90
+ 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
@@ -0,0 +1,7 @@
1
+ $:.unshift(File.dirname(__FILE__)+"/contacts/")
2
+
3
+ require 'rubygems'
4
+ require 'base'
5
+ require 'gmail'
6
+ require 'hotmail'
7
+ require 'yahoo'
@@ -0,0 +1,171 @@
1
+ require "cgi"
2
+ require "net/http"
3
+ require "net/https"
4
+ require "uri"
5
+
6
+ class Contacts
7
+ TYPES = {}
8
+ VERSION = "1.0.0"
9
+
10
+ class Base
11
+ def initialize(login, password)
12
+ @login = login
13
+ @password = password
14
+ connect
15
+ end
16
+
17
+ def connect
18
+ raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @password.nil?
19
+ real_connect
20
+ end
21
+
22
+ def connected?
23
+ @cookies && !@cookies.empty?
24
+ end
25
+
26
+ def contacts
27
+ return @contacts if @contacts
28
+ if connected?
29
+ url = URI.parse(contact_list_url)
30
+ http = Net::HTTP.new(url.host, url.port)
31
+ if url.port == 443
32
+ http.use_ssl = true
33
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
34
+ end
35
+ resp, data = http.get("#{url.path}?#{url.query}",
36
+ "Cookie" => @cookies
37
+ )
38
+
39
+ if resp.code_type != Net::HTTPOK
40
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
41
+ end
42
+
43
+ parse data
44
+ end
45
+ end
46
+
47
+ def login
48
+ @attempt ||= 0
49
+ @attempt += 1
50
+
51
+ if @attempt == 1
52
+ @login
53
+ else
54
+ if @login.include?("@#{domain}")
55
+ @login.sub("@#{domain}","")
56
+ else
57
+ "#{@login}@#{domain}"
58
+ end
59
+ end
60
+ end
61
+
62
+ def password
63
+ @password
64
+ end
65
+
66
+ private
67
+
68
+ def domain
69
+ @d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
70
+ end
71
+
72
+ def contact_list_url
73
+ self.class.const_get(:CONTACT_LIST_URL)
74
+ end
75
+
76
+ def parse_cookies(data, existing="")
77
+ return existing if data.nil?
78
+
79
+ cookies = existing.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
80
+
81
+ data.gsub!(/ ?[\w]+=EXPIRED;/,'')
82
+ data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
83
+ data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
84
+ data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
85
+ data.gsub!(/(;\s*){2,}/,', ')
86
+ data.gsub!(/(,\s*){2,}/,', ')
87
+ data.sub!(/^,\s*/,'')
88
+ data.sub!(/\s*,$/,'')
89
+
90
+ data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
91
+ k, v = data.split("=", 2).map{|j|j.strip}
92
+ if cookies[k] && v.empty?
93
+ cookies.delete(k)
94
+ elsif v && !v.empty?
95
+ cookies[k] = v
96
+ end
97
+ end
98
+
99
+ cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
100
+ end
101
+
102
+ def remove_cookie(cookie, cookies)
103
+ parse_cookies("#{cookie}=", cookies)
104
+ end
105
+
106
+ def post(url, postdata, cookies="", referer="")
107
+ url = URI.parse(url)
108
+ http = Net::HTTP.new(url.host, url.port)
109
+ if url.port == 443
110
+ http.use_ssl = true
111
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
112
+ end
113
+ resp, data = http.post(url.path, postdata,
114
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
115
+ "Cookie" => cookies,
116
+ "Referer" => referer,
117
+ "Content-Type" => 'application/x-www-form-urlencoded'
118
+ )
119
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
120
+ forward = resp.response['Location']
121
+ return data, resp, cookies, forward
122
+ end
123
+
124
+ def get(url, cookies="", referer="")
125
+ url = URI.parse(url)
126
+ http = Net::HTTP.new(url.host, url.port)
127
+ if url.port == 443
128
+ http.use_ssl = true
129
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
130
+ end
131
+ resp, data = http.get("#{url.path}?#{url.query}",
132
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
133
+ "Cookie" => cookies,
134
+ "Referer" => referer
135
+ )
136
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
137
+ forward = resp.response['Location']
138
+ return data, resp, cookies, forward
139
+ end
140
+ end
141
+
142
+ class ContactsError < StandardError
143
+ end
144
+
145
+ class AuthenticationError < ContactsError
146
+ end
147
+
148
+ class ConnectionError < ContactsError
149
+ end
150
+
151
+ class TypeNotFound < ContactsError
152
+ end
153
+
154
+ def self.new(type, login, password)
155
+ if TYPES.include?(type.to_s.intern)
156
+ TYPES[type.to_s.intern].new(login, password)
157
+ else
158
+ raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect}"
159
+ end
160
+ end
161
+
162
+ def self.guess(login, password)
163
+ TYPES.inject([]) do |a, t|
164
+ begin
165
+ a + t[1].new(login, password).contacts
166
+ rescue AuthenticationError
167
+ a
168
+ end
169
+ end.uniq
170
+ end
171
+ end
@@ -0,0 +1,81 @@
1
+ require "json"
2
+
3
+ class Contacts
4
+ class Gmail < Base
5
+ URL = "https://mail.google.com/mail/"
6
+ LOGIN_URL = "https://www.google.com/accounts/ServiceLoginAuth"
7
+ 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"
8
+ CONTACT_LIST_URL = "https://mail.google.com/mail/?view=cl&search=contacts&pnl=a"
9
+ PROTOCOL_ERROR = "Gmail has changed its protocols, please upgrade this library first. If that does not work, contact lucas@rufy.com with this error"
10
+
11
+ def real_connect
12
+ postdata = "ltmpl=yj_blanco"
13
+ postdata += "&continue=#{CGI.escape(URL)}"
14
+ postdata += "&ltmplcache=2"
15
+ postdata += "&service=mail"
16
+ postdata += "&rm=false"
17
+ postdata += "&ltmpl=yj_blanco"
18
+ postdata += "&hl=en"
19
+ postdata += "&Email=#{CGI.escape(login)}"
20
+ postdata += "&Passwd=#{CGI.escape(password)}"
21
+ postdata += "&rmShown=1"
22
+ postdata += "&null=Sign+in"
23
+
24
+ time = Time.now.to_i
25
+ time_past = Time.now.to_i - 8 - rand(12)
26
+ cookie = "GMAIL_LOGIN=T#{time_past}/#{time_past}/#{time}"
27
+
28
+ data, resp, cookies, forward, old_url = post(LOGIN_URL, postdata, cookie, LOGIN_REFERER_URL) + [LOGIN_URL]
29
+
30
+ cookies = remove_cookie("GMAIL_LOGIN", cookies)
31
+
32
+ if data.index("Username and password do not match")
33
+ raise AuthenticationError, "Username and password do not match"
34
+ elsif data.index("Required field must not be blank")
35
+ raise AuthenticationError, "Login and password must not be blank"
36
+ elsif data.index("errormsg_0_logincaptcha")
37
+ raise AuthenticationError, "Captcha error"
38
+ elsif data.index("Invalid request")
39
+ raise ConnectionError, PROTOCOL_ERROR
40
+ elsif cookies == ""
41
+ raise ConnectionError, PROTOCOL_ERROR
42
+ end
43
+
44
+ until forward.nil?
45
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
46
+ end
47
+ cookies = remove_cookie("LSID", cookies)
48
+ cookies = remove_cookie("GV", cookies)
49
+
50
+ if resp.code_type != Net::HTTPOK
51
+ raise ConnectionError, PROTOCOL_ERROR
52
+ end
53
+
54
+ @cookies = cookies
55
+ end
56
+
57
+ private
58
+
59
+ def parse(data)
60
+ data.gsub!("\n","")
61
+ data.gsub!("D([", "\nD([")
62
+ data.gsub!("]);", "]);\n")
63
+ data.gsub!("u003d", "=")
64
+ data.gsub!("u002f", "/")
65
+
66
+ parsed_data = []
67
+ data.scan(%r{D\(\[(.*)\]\)}) do |match|
68
+ parsed_data << JSON.parse("[#{match[0]}]") rescue next
69
+ end
70
+
71
+ @contacts = parsed_data.select{|d| d[0] == "cl"}.inject([]) do |a,d|
72
+ a.concat(d.inject([]) do |ar,dr|
73
+ ar.concat(dr.is_a?(Array) ? [[dr[3],dr[4]]] : [])
74
+ end)
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ TYPES[:gmail] = Gmail
81
+ end
@@ -0,0 +1,77 @@
1
+ class Contacts
2
+ class Hotmail < Base
3
+ URL = "http://www.hotmail.com/"
4
+ CONTACT_LIST_URL = "http://%s/cgi-bin/addresses"
5
+ PROTOCOL_ERROR = "Hotmail has changed its protocols, please upgrade this library first. If that does not work, contact lucas@rufy.com with this error"
6
+ PWDPAD = "IfYouAreReadingThisYouHaveTooMuchFreeTime"
7
+
8
+ def real_connect
9
+ data, resp, cookies, forward = get(URL)
10
+ data, resp, cookies, forward = get(forward, cookies, URL)
11
+
12
+ postdata = "PPSX=#{CGI.escape(data.split("><").grep(/PPSX/).first[/=\S+$/][2..-3])}&"
13
+ postdata += "PwdPad=#{PWDPAD[0...(PWDPAD.length-@password.length)]}&"
14
+ postdata += "login=#{CGI.escape(login)}&passwd=#{CGI.escape(password)}&LoginOptions=2&"
15
+ postdata += "PPFT=#{CGI.escape(data.split("><").grep(/PPFT/).first[/=\S+$/][2..-3])}"
16
+
17
+ form_url = data.split("><").grep(/form/).first.split[5][8..-2]
18
+ data, resp, cookies, forward = post(form_url, postdata, cookies)
19
+
20
+ if data.index("The e-mail address or password is incorrect")
21
+ raise AuthenticationError, "Username and password do not match"
22
+ elsif data != ""
23
+ raise AuthenticationError, "Required field must not be blank"
24
+ elsif cookies == ""
25
+ raise ConnectionError, PROTOCOL_ERROR
26
+ end
27
+
28
+ old_url = form_url
29
+ until forward.nil?
30
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
31
+ end
32
+
33
+ if resp.code_type != Net::HTTPOK
34
+ raise ConnectionError, PROTOCOL_ERROR
35
+ end
36
+
37
+ forward = "http://www.hotmail.msn.com/cgi-bin/sbox"
38
+
39
+ until forward.nil?
40
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
41
+ end
42
+
43
+ if resp.code_type != Net::HTTPOK
44
+ raise ConnectionError, PROTOCOL_ERROR
45
+ end
46
+
47
+ @domain = URI.parse(old_url).host
48
+ @cookies = cookies
49
+ rescue AuthenticationError => m
50
+ if @attempt == 1
51
+ retry
52
+ else
53
+ raise m
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def contact_list_url
60
+ CONTACT_LIST_URL % @domain
61
+ end
62
+
63
+ def parse(data)
64
+ chunks = data.split('id="hotmail"')
65
+ chunks.delete_at(0)
66
+
67
+ @contacts = chunks.map do |chunk|
68
+ name = chunk.split('return false;">')[1].split('</a>')[0] rescue nil
69
+ email = chunk.split('return false;">')[2].split('</a>')[0] rescue nil
70
+ [name, email] if email
71
+ end.compact
72
+ end
73
+
74
+ end
75
+
76
+ TYPES[:hotmail] = Hotmail
77
+ end
@@ -0,0 +1,57 @@
1
+ class Contacts
2
+ class Yahoo < Base
3
+ URL = "http://mail.yahoo.com/"
4
+ LOGIN_URL = "https://login.yahoo.com/config/login"
5
+ CONTACT_LIST_URL = "http://address.yahoo.com/"
6
+ PROTOCOL_ERROR = "Yahoo has changed its protocols, please upgrade this library first. If that does not work, contact lucas@rufy.com with this error"
7
+
8
+ def real_connect
9
+ postdata = ".tries=2&.src=ym&.md5=&.hash=&.js=&.last=&promo=&.intl=us&.bypass="
10
+ postdata += "&.partner=&.u=4eo6isd23l8r3&.v=0&.challenge=gsMsEcoZP7km3N3NeI4mX"
11
+ postdata += "kGB7zMV&.yplus=&.emailCode=&pkg=&stepid=&.ev=&hasMsgr=1&.chkP=Y&."
12
+ postdata += "done=#{CGI.escape(URL)}&login=#{CGI.escape(login)}&passwd=#{CGI.escape(password)}"
13
+
14
+ data, resp, cookies, forward = post(LOGIN_URL, postdata)
15
+
16
+ if data.index("Invalid ID or password")
17
+ raise AuthenticationError, "Username and password do not match"
18
+ elsif data.index("Sign in") && data.index("to Yahoo!")
19
+ raise AuthenticationError, "Required field must not be blank"
20
+ elsif data != ""
21
+ raise ConnectionError, PROTOCOL_ERROR
22
+ elsif cookies == ""
23
+ raise ConnectionError, PROTOCOL_ERROR
24
+ end
25
+
26
+ data, resp, cookies, forward = get(forward, cookies, LOGIN_URL)
27
+
28
+ if resp.code_type != Net::HTTPOK
29
+ raise ConnectionError, PROTOCOL_ERROR
30
+ end
31
+
32
+ @cookies = cookies
33
+ end
34
+
35
+ private
36
+
37
+ def parse(data)
38
+ chunks = data.split("c_row")
39
+ chunks.delete_at(0)
40
+
41
+ processed_data = {}
42
+ chunks.each_with_index do |chunk, i|
43
+ processed_data[i/2] ||= []
44
+ if i%2 == 0 # then it is a name
45
+ processed_data[i/2] << chunk.split(/[^,]\n/).grep(/contactname/).first.gsub(/(<|^).*?(>|$)/,'').strip
46
+ else # it is an email
47
+ processed_data[i/2] << chunk.split.grep(/compose.mail.yahoo.com/).first.gsub(/(<|^).*?(>|$)/,'').strip rescue next
48
+ end
49
+ end
50
+
51
+ @contacts = processed_data.values.delete_if{|data|data[1].nil?}
52
+ end
53
+
54
+ end
55
+
56
+ TYPES[:yahoo] = Yahoo
57
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: contacts
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2006-12-04 00:00:00 -08:00
8
+ summary: Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
9
+ require_paths:
10
+ - lib
11
+ email: lucas@rufy.com
12
+ homepage: http://rubyforge.org/projects/contacts
13
+ rubyforge_project:
14
+ description: Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
15
+ autorequire: contacts
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Lucas Carlson
30
+ files:
31
+ - lib/contacts
32
+ - lib/contacts.rb
33
+ - lib/contacts/base.rb
34
+ - lib/contacts/gmail.rb
35
+ - lib/contacts/hotmail.rb
36
+ - lib/contacts/yahoo.rb
37
+ - LICENSE
38
+ - Rakefile
39
+ - README
40
+ - examples/grab_contacts.rb
41
+ test_files: []
42
+
43
+ rdoc_options: []
44
+
45
+ extra_rdoc_files: []
46
+
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ requirements:
52
+ - A json parser
53
+ dependencies:
54
+ - !ruby/object:Gem::Dependency
55
+ name: json
56
+ version_requirement:
57
+ version_requirements: !ruby/object:Gem::Version::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.4.1
62
+ version: