liangzan-contacts 1.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +10 -0
- data/README.rdoc +60 -0
- data/Rakefile +41 -0
- data/examples/grab_contacts.rb +12 -0
- data/lib/contacts.rb +12 -0
- data/lib/contacts/aol.rb +154 -0
- data/lib/contacts/base.rb +221 -0
- data/lib/contacts/gmail.rb +35 -0
- data/lib/contacts/hotmail.rb +78 -0
- data/lib/contacts/json_picker.rb +16 -0
- data/lib/contacts/mailru.rb +68 -0
- data/lib/contacts/plaxo.rb +130 -0
- data/lib/contacts/yahoo.rb +105 -0
- metadata +82 -0
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.rdoc
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
= liangzan-contacts
|
2
|
+
|
3
|
+
liangzan-contacts is a fork of the contacts_19 gem. It aims to be compatible with 1.9 and above.
|
4
|
+
|
5
|
+
== Intro
|
6
|
+
|
7
|
+
Contacts is a universal interface to grab contact list information from various providers including Hotmail, AOL, Gmail, Plaxo and Yahoo.
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
$ gem install liangzan-contacts
|
12
|
+
|
13
|
+
== Background
|
14
|
+
|
15
|
+
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.
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
|
19
|
+
Contacts::Hotmail.new(login, password).contacts # => [["name", "foo@bar.com"], ["another name", "bow@wow.com"]]
|
20
|
+
Contacts::Yahoo.new(login, password).contacts
|
21
|
+
Contacts::Gmail.new(login, password).contacts
|
22
|
+
|
23
|
+
Contacts.new(:gmail, login, password).contacts
|
24
|
+
Contacts.new(:hotmail, login, password).contacts
|
25
|
+
Contacts.new(:yahoo, login, password).contacts
|
26
|
+
|
27
|
+
Contacts.guess(login, password).contacts
|
28
|
+
|
29
|
+
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.
|
30
|
+
|
31
|
+
== Captcha error
|
32
|
+
|
33
|
+
If there are too many failed attempts with the gmail login info, Google will raise a captcha response. To integrate the captcha handling, pass in the token and response via:
|
34
|
+
|
35
|
+
Contacts::Gmail.new(login, password, :captcha_token => params[:captcha_token], :captcha_response => params[:captcha_response]).contacts
|
36
|
+
|
37
|
+
== Examples
|
38
|
+
|
39
|
+
See the examples/ directory.
|
40
|
+
|
41
|
+
== Authors
|
42
|
+
|
43
|
+
* Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
|
44
|
+
|
45
|
+
== Contributors
|
46
|
+
|
47
|
+
* Britt Selvitelle from Twitter (mailto:anotherbritt@gmail.com) - http://twitter.com
|
48
|
+
* Tony Targonski from GigPark (mailto:tony@gigpark.com) - http://gigpark.com
|
49
|
+
* Waheed Barghouthi from Watwet (mailto:waheed.barghouthi@gmail.com) - http://watwet.com
|
50
|
+
* Glenn Sidney from Glenn Fu (mailto:glenn@glennfu.com) - http://glennfu.com
|
51
|
+
* Brian McQuay from Onomojo (mailto:brian@onomojo.com) - http://onomojo.com
|
52
|
+
* Adam Hunter (mailto:adamhunter@me.com) - http://adamhunter.me/
|
53
|
+
* Glenn Ford (mailto:glenn@glennfu.com) - http://www.glennfu.com/
|
54
|
+
* Leonardo Wong (mailto:mac@boy.name)
|
55
|
+
* Rusty Burchfield
|
56
|
+
* justintv
|
57
|
+
* Wong Liang Zan (mailto:zan@liangzan.net) - http://liangzan.net
|
58
|
+
|
59
|
+
This library is released under the terms of the BSD.
|
60
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
def gemspec
|
6
|
+
@gemspec ||= begin
|
7
|
+
file = File.expand_path("../liangzan-contacts.gemspec", __FILE__)
|
8
|
+
eval(File.read(file), binding, file)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Run the unit tests
|
13
|
+
desc "Run all unit tests"
|
14
|
+
Rake::TestTask.new do |t|
|
15
|
+
t.libs << "lib"
|
16
|
+
t.test_files = FileList['test/test*.rb']
|
17
|
+
t.verbose = true
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rdoc/task'
|
21
|
+
Rake::RDocTask.new
|
22
|
+
|
23
|
+
begin
|
24
|
+
require 'rubygems/package_task'
|
25
|
+
Gem::PackageTask.new(gemspec) do |pkg|
|
26
|
+
pkg.gem_spec = gemspec
|
27
|
+
end
|
28
|
+
task :gem => :gemspec
|
29
|
+
rescue LoadError
|
30
|
+
task(:gem){abort "`gem install rake` to package gems"}
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Install the gem locally"
|
34
|
+
task :install => :gem do
|
35
|
+
sh "gem install pkg/#{gemspec.full_name}.gem"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Validate the gemspec"
|
39
|
+
task :gemspec do
|
40
|
+
gemspec.validate
|
41
|
+
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/lib/contacts.rb
ADDED
data/lib/contacts/aol.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
class Contacts
|
2
|
+
require 'hpricot' if RUBY_VERSION < '1.9'
|
3
|
+
require 'csv'
|
4
|
+
class Aol < Base
|
5
|
+
URL = "http://www.aol.com/"
|
6
|
+
LOGIN_URL = "https://my.screenname.aol.com/_cqr/login/login.psp"
|
7
|
+
LOGIN_REFERER_URL = "http://webmail.aol.com/"
|
8
|
+
LOGIN_REFERER_PATH = "sitedomain=sns.webmail.aol.com&lang=en&locale=us&authLev=0&uitype=mini&loginId=&redirType=js&xchk=false"
|
9
|
+
AOL_NUM = "29970-343" # this seems to change each time they change the protocol
|
10
|
+
|
11
|
+
CONTACT_LIST_URL = "http://webmail.aol.com/#{AOL_NUM}/aim-2/en-us/Lite/ContactList.aspx?folder=Inbox&showUserFolders=False"
|
12
|
+
CONTACT_LIST_CSV_URL = "http://webmail.aol.com/#{AOL_NUM}/aim-2/en-us/Lite/ABExport.aspx?command=all"
|
13
|
+
PROTOCOL_ERROR = "AOL 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"
|
14
|
+
|
15
|
+
def real_connect
|
16
|
+
if login.strip =~ /^(.+)@aol\.com$/ # strip off the @aol.com for AOL logins
|
17
|
+
login = $1
|
18
|
+
end
|
19
|
+
|
20
|
+
postdata = {
|
21
|
+
"loginId" => login,
|
22
|
+
"password" => password,
|
23
|
+
"rememberMe" => "on",
|
24
|
+
"_sns_fg_color_" => "",
|
25
|
+
"_sns_err_color_" => "",
|
26
|
+
"_sns_link_color_" => "",
|
27
|
+
"_sns_width_" => "",
|
28
|
+
"_sns_height_" => "",
|
29
|
+
"offerId" => "mail-second-en-us",
|
30
|
+
"_sns_bg_color_" => "",
|
31
|
+
"sitedomain" => "sns.webmail.aol.com",
|
32
|
+
"regPromoCode" => "",
|
33
|
+
"mcState" => "initialized",
|
34
|
+
"uitype" => "std",
|
35
|
+
"siteId" => "",
|
36
|
+
"lang" => "en",
|
37
|
+
"locale" => "us",
|
38
|
+
"authLev" => "0",
|
39
|
+
"siteState" => "",
|
40
|
+
"isSiteStateEncoded" => "false",
|
41
|
+
"use_aam" => "0",
|
42
|
+
"seamless" => "novl",
|
43
|
+
"aolsubmit" => CGI.escape("Sign In"),
|
44
|
+
"idType" => "SN",
|
45
|
+
"usrd" => "",
|
46
|
+
"doSSL" => "",
|
47
|
+
"redirType" => "",
|
48
|
+
"xchk" => "false"
|
49
|
+
}
|
50
|
+
|
51
|
+
# Get this cookie and stick it in the form to confirm to Aol that your cookies work
|
52
|
+
data, resp, cookies, forward = get(URL)
|
53
|
+
postdata["stips"] = cookie_hash_from_string(cookies)["stips"]
|
54
|
+
postdata["tst"] = cookie_hash_from_string(cookies)["tst"]
|
55
|
+
|
56
|
+
data, resp, cookies, forward, old_url = get(LOGIN_REFERER_URL, cookies) + [URL]
|
57
|
+
until forward.nil?
|
58
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
59
|
+
end
|
60
|
+
|
61
|
+
data, resp, cookies, forward, old_url = get("#{LOGIN_URL}?#{LOGIN_REFERER_PATH}", cookies) + [LOGIN_REFERER_URL]
|
62
|
+
until forward.nil?
|
63
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
64
|
+
end
|
65
|
+
|
66
|
+
doc = Hpricot(data)
|
67
|
+
(doc/:input).each do |input|
|
68
|
+
postdata["usrd"] = input.attributes["value"] if input.attributes["name"] == "usrd"
|
69
|
+
end
|
70
|
+
# parse data for <input name="usrd" value="2726212" type="hidden"> and add it to the postdata
|
71
|
+
|
72
|
+
postdata["SNS_SC"] = cookie_hash_from_string(cookies)["SNS_SC"]
|
73
|
+
postdata["SNS_LDC"] = cookie_hash_from_string(cookies)["SNS_LDC"]
|
74
|
+
postdata["LTState"] = cookie_hash_from_string(cookies)["LTState"]
|
75
|
+
# raise data.inspect
|
76
|
+
|
77
|
+
data, resp, cookies, forward, old_url = post(LOGIN_URL, h_to_query_string(postdata), cookies, LOGIN_REFERER_URL) + [LOGIN_REFERER_URL]
|
78
|
+
|
79
|
+
until forward.nil?
|
80
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
81
|
+
end
|
82
|
+
|
83
|
+
if data.index("Invalid Username or Password. Please try again.")
|
84
|
+
raise AuthenticationError, "Username and password do not match"
|
85
|
+
elsif data.index("Required field must not be blank")
|
86
|
+
raise AuthenticationError, "Login and password must not be blank"
|
87
|
+
elsif data.index("errormsg_0_logincaptcha")
|
88
|
+
raise AuthenticationError, "Captcha error"
|
89
|
+
elsif data.index("Invalid request")
|
90
|
+
raise ConnectionError, PROTOCOL_ERROR
|
91
|
+
elsif cookies == ""
|
92
|
+
raise ConnectionError, PROTOCOL_ERROR
|
93
|
+
end
|
94
|
+
|
95
|
+
@cookies = cookies
|
96
|
+
end
|
97
|
+
|
98
|
+
def contacts
|
99
|
+
postdata = {
|
100
|
+
"file" => 'contacts',
|
101
|
+
"fileType" => 'csv'
|
102
|
+
}
|
103
|
+
|
104
|
+
return @contacts if @contacts
|
105
|
+
if connected?
|
106
|
+
data, resp, cookies, forward, old_url = get(CONTACT_LIST_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
|
107
|
+
|
108
|
+
until forward.nil?
|
109
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
110
|
+
end
|
111
|
+
|
112
|
+
if resp.code_type != Net::HTTPOK
|
113
|
+
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
|
114
|
+
end
|
115
|
+
|
116
|
+
# parse data and grab <input name="user" value="8QzMPIAKs2" type="hidden">
|
117
|
+
doc = Hpricot(data)
|
118
|
+
(doc/:input).each do |input|
|
119
|
+
postdata["user"] = input.attributes["value"] if input.attributes["name"] == "user"
|
120
|
+
end
|
121
|
+
|
122
|
+
data, resp, cookies, forward, old_url = get(CONTACT_LIST_CSV_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
|
123
|
+
|
124
|
+
until forward.nil?
|
125
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
126
|
+
end
|
127
|
+
|
128
|
+
if data.include?("error.gif")
|
129
|
+
raise AuthenticationError, "Account invalid"
|
130
|
+
end
|
131
|
+
|
132
|
+
parse data
|
133
|
+
end
|
134
|
+
end
|
135
|
+
private
|
136
|
+
|
137
|
+
def parse(data, options={})
|
138
|
+
data = CSV::Reader.parse(data)
|
139
|
+
col_names = data.shift
|
140
|
+
@contacts = data.map do |person|
|
141
|
+
["#{person[0]} #{person[1]}", person[4]] if person[4] && !person[4].empty?
|
142
|
+
end.compact
|
143
|
+
end
|
144
|
+
|
145
|
+
def h_to_query_string(hash)
|
146
|
+
u = ERB::Util.method(:u)
|
147
|
+
hash.map { |k, v|
|
148
|
+
u.call(k) + "=" + u.call(v)
|
149
|
+
}.join("&")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
TYPES[:aol] = Aol
|
154
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "net/http"
|
3
|
+
require "net/https"
|
4
|
+
require "uri"
|
5
|
+
require "zlib"
|
6
|
+
require "stringio"
|
7
|
+
require "thread"
|
8
|
+
require "erb"
|
9
|
+
|
10
|
+
class Contacts
|
11
|
+
TYPES = {}
|
12
|
+
|
13
|
+
class Base
|
14
|
+
def initialize(login, password, options={})
|
15
|
+
@login = login
|
16
|
+
@password = password
|
17
|
+
@captcha_token = options[:captcha_token]
|
18
|
+
@captcha_response = options[:captcha_response]
|
19
|
+
@connections = {}
|
20
|
+
connect
|
21
|
+
end
|
22
|
+
|
23
|
+
def connect
|
24
|
+
raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @login.empty? || @password.nil? || @password.empty?
|
25
|
+
real_connect
|
26
|
+
end
|
27
|
+
|
28
|
+
def connected?
|
29
|
+
@cookies && !@cookies.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def contacts(options = {})
|
33
|
+
return @contacts if @contacts
|
34
|
+
if connected?
|
35
|
+
url = URI.parse(contact_list_url)
|
36
|
+
http = open_http(url)
|
37
|
+
resp, data = http.get("#{url.path}?#{url.query}",
|
38
|
+
"Cookie" => @cookies
|
39
|
+
)
|
40
|
+
|
41
|
+
if resp.code_type != Net::HTTPOK
|
42
|
+
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
|
43
|
+
end
|
44
|
+
|
45
|
+
parse(data, options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def login
|
50
|
+
@attempt ||= 0
|
51
|
+
@attempt += 1
|
52
|
+
|
53
|
+
if @attempt == 1
|
54
|
+
@login
|
55
|
+
else
|
56
|
+
if @login.include?("@#{domain}")
|
57
|
+
@login.sub("@#{domain}","")
|
58
|
+
else
|
59
|
+
"#{@login}@#{domain}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def password
|
65
|
+
@password
|
66
|
+
end
|
67
|
+
|
68
|
+
def skip_gzip?
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def domain
|
75
|
+
@d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
|
76
|
+
end
|
77
|
+
|
78
|
+
def contact_list_url
|
79
|
+
self.class.const_get(:CONTACT_LIST_URL)
|
80
|
+
end
|
81
|
+
|
82
|
+
def address_book_url
|
83
|
+
self.class.const_get(:ADDRESS_BOOK_URL)
|
84
|
+
end
|
85
|
+
|
86
|
+
def open_http(url)
|
87
|
+
c = @connections[Thread.current.object_id] ||= {}
|
88
|
+
http = c["#{url.host}:#{url.port}"]
|
89
|
+
unless http
|
90
|
+
http = Net::HTTP.new(url.host, url.port)
|
91
|
+
if url.port == 443
|
92
|
+
http.use_ssl = true
|
93
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
94
|
+
end
|
95
|
+
c["#{url.host}:#{url.port}"] = http
|
96
|
+
end
|
97
|
+
http.start unless http.started?
|
98
|
+
http
|
99
|
+
end
|
100
|
+
|
101
|
+
def cookie_hash_from_string(cookie_string)
|
102
|
+
cookie_string.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_cookies(data, existing="")
|
106
|
+
return existing if data.nil?
|
107
|
+
|
108
|
+
cookies = cookie_hash_from_string(existing)
|
109
|
+
|
110
|
+
data.gsub!(/ ?[\w]+=EXPIRED;/,'')
|
111
|
+
data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
|
112
|
+
data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
|
113
|
+
data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
|
114
|
+
data.gsub!(/(;\s*){2,}/,', ')
|
115
|
+
data.gsub!(/(,\s*){2,}/,', ')
|
116
|
+
data.sub!(/^,\s*/,'')
|
117
|
+
data.sub!(/\s*,$/,'')
|
118
|
+
|
119
|
+
data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
|
120
|
+
k, v = data.split("=", 2).map{|j|j.strip}
|
121
|
+
if cookies[k] && v.empty?
|
122
|
+
cookies.delete(k)
|
123
|
+
elsif v && !v.empty?
|
124
|
+
cookies[k] = v
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
|
129
|
+
end
|
130
|
+
|
131
|
+
def remove_cookie(cookie, cookies)
|
132
|
+
parse_cookies("#{cookie}=", cookies)
|
133
|
+
end
|
134
|
+
|
135
|
+
def post(url, postdata, cookies="", referer="")
|
136
|
+
url = URI.parse(url)
|
137
|
+
http = open_http(url)
|
138
|
+
http_header = { "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
|
139
|
+
"Accept-Encoding" => "gzip",
|
140
|
+
"Cookie" => cookies,
|
141
|
+
"Referer" => referer,
|
142
|
+
"Content-Type" => 'application/x-www-form-urlencoded'
|
143
|
+
}
|
144
|
+
http_header.reject!{|k, v| k == 'Accept-Encoding'} if skip_gzip?
|
145
|
+
resp, data = http.post(url.path, postdata, http_header)
|
146
|
+
data = uncompress(resp, data)
|
147
|
+
cookies = parse_cookies(resp.response['set-cookie'], cookies)
|
148
|
+
forward = resp.response['Location']
|
149
|
+
forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
|
150
|
+
if (not forward.nil?) && URI.parse(forward).host.nil?
|
151
|
+
forward = url.scheme.to_s + "://" + url.host.to_s + forward
|
152
|
+
end
|
153
|
+
return data, resp, cookies, forward
|
154
|
+
end
|
155
|
+
|
156
|
+
def get(url, cookies="", referer="")
|
157
|
+
url = URI.parse(url)
|
158
|
+
http = open_http(url)
|
159
|
+
resp, data = http.get("#{url.path}?#{url.query}",
|
160
|
+
"User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
|
161
|
+
"Accept-Encoding" => "gzip",
|
162
|
+
"Cookie" => cookies,
|
163
|
+
"Referer" => referer
|
164
|
+
)
|
165
|
+
data = uncompress(resp, data)
|
166
|
+
cookies = parse_cookies(resp.response['set-cookie'], cookies)
|
167
|
+
forward = resp.response['Location']
|
168
|
+
if (not forward.nil?) && URI.parse(forward).host.nil?
|
169
|
+
forward = url.scheme.to_s + "://" + url.host.to_s + forward
|
170
|
+
end
|
171
|
+
return data, resp, cookies, forward
|
172
|
+
end
|
173
|
+
|
174
|
+
def uncompress(resp, data)
|
175
|
+
case resp.response['content-encoding']
|
176
|
+
when 'gzip'
|
177
|
+
gz = Zlib::GzipReader.new(StringIO.new(data))
|
178
|
+
data = gz.read
|
179
|
+
gz.close
|
180
|
+
resp.response['content-encoding'] = nil
|
181
|
+
# FIXME: Not sure what Hotmail was feeding me with their 'deflate',
|
182
|
+
# but the headers definitely were not right
|
183
|
+
when 'deflate'
|
184
|
+
data = Zlib::Inflate.inflate(data)
|
185
|
+
resp.response['content-encoding'] = nil
|
186
|
+
end
|
187
|
+
|
188
|
+
data
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class ContactsError < StandardError
|
193
|
+
end
|
194
|
+
|
195
|
+
class AuthenticationError < ContactsError
|
196
|
+
end
|
197
|
+
|
198
|
+
class ConnectionError < ContactsError
|
199
|
+
end
|
200
|
+
|
201
|
+
class TypeNotFound < ContactsError
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.new(type, login, password, options={})
|
205
|
+
if TYPES.include?(type.to_s.intern)
|
206
|
+
TYPES[type.to_s.intern].new(login, password, options)
|
207
|
+
else
|
208
|
+
raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.guess(login, password, options={})
|
213
|
+
TYPES.inject([]) do |a, t|
|
214
|
+
begin
|
215
|
+
a + t[1].new(login, password, options).contacts
|
216
|
+
rescue AuthenticationError
|
217
|
+
a
|
218
|
+
end
|
219
|
+
end.uniq
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'gdata'
|
2
|
+
|
3
|
+
class Contacts
|
4
|
+
class Gmail < Base
|
5
|
+
|
6
|
+
CONTACTS_SCOPE = 'http://www.google.com/m8/feeds/'
|
7
|
+
CONTACTS_FEED = CONTACTS_SCOPE + 'contacts/default/full/?max-results=1000'
|
8
|
+
|
9
|
+
def contacts
|
10
|
+
return @contacts if @contacts
|
11
|
+
end
|
12
|
+
|
13
|
+
def real_connect
|
14
|
+
@client = GData::Client::Contacts.new
|
15
|
+
@client.clientlogin(@login, @password, @captcha_token, @captcha_response)
|
16
|
+
|
17
|
+
feed = @client.get(CONTACTS_FEED).to_xml
|
18
|
+
|
19
|
+
@contacts = feed.elements.to_a('entry').collect do |entry|
|
20
|
+
title, email = entry.elements['title'].text, nil
|
21
|
+
entry.elements.each('gd:email') do |e|
|
22
|
+
email = e.attribute('address').value if e.attribute('primary')
|
23
|
+
end
|
24
|
+
[title, email] unless email.nil?
|
25
|
+
end
|
26
|
+
@contacts.compact!
|
27
|
+
rescue GData::Client::AuthorizationError => e
|
28
|
+
raise AuthenticationError, "Username or password are incorrect"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
TYPES[:gmail] = Gmail
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
class Contacts
|
4
|
+
class Hotmail < Base
|
5
|
+
URL = "https://login.live.com/login.srf?id=2"
|
6
|
+
CONTACT_LIST_URL = "https://col115.mail.live.com/mail/GetContacts.aspx"
|
7
|
+
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"
|
8
|
+
PWDPAD = ""
|
9
|
+
|
10
|
+
def real_connect
|
11
|
+
|
12
|
+
data, resp, cookies, forward = get(URL)
|
13
|
+
old_url = URL
|
14
|
+
until forward.nil?
|
15
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
16
|
+
end
|
17
|
+
|
18
|
+
postdata = "PPSX=%s&PwdPad=%s&login=%s&passwd=%s&LoginOptions=2&PPFT=%s" % [
|
19
|
+
CGI.escape(data.split("><").grep(/PPSX/).first[/=\S+$/][2..-3]),
|
20
|
+
PWDPAD[0...(PWDPAD.length-@password.length)],
|
21
|
+
CGI.escape(login),
|
22
|
+
CGI.escape(password),
|
23
|
+
CGI.escape(data.split("><").grep(/PPFT/).first[/=\S+$/][2..-3])
|
24
|
+
]
|
25
|
+
|
26
|
+
form_url = data.split("><").grep(/form/).first.split[5][8..-2]
|
27
|
+
data, resp, cookies, forward = post(form_url, postdata, cookies)
|
28
|
+
|
29
|
+
old_url = form_url
|
30
|
+
until cookies =~ /; PPAuth=/ || forward.nil?
|
31
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
32
|
+
end
|
33
|
+
|
34
|
+
if data.index("The e-mail address or password is incorrect")
|
35
|
+
raise AuthenticationError, "Username and password do not match"
|
36
|
+
elsif data != ""
|
37
|
+
raise AuthenticationError, "Required field must not be blank"
|
38
|
+
elsif cookies == ""
|
39
|
+
raise ConnectionError, PROTOCOL_ERROR
|
40
|
+
end
|
41
|
+
|
42
|
+
data, resp, cookies, forward = get("http://mail.live.com/mail", cookies)
|
43
|
+
until forward.nil?
|
44
|
+
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
@domain = URI.parse(old_url).host
|
49
|
+
@cookies = cookies
|
50
|
+
rescue AuthenticationError => m
|
51
|
+
if @attempt == 1
|
52
|
+
retry
|
53
|
+
else
|
54
|
+
raise m
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def contacts(options = {})
|
59
|
+
if @contacts.nil? && connected?
|
60
|
+
url = URI.parse(contact_list_url)
|
61
|
+
data, resp, cookies, forward = get(CONTACT_LIST_URL, @cookies )
|
62
|
+
|
63
|
+
@contacts = CSV.parse(data, {:headers => true}).map do |row|
|
64
|
+
name = ""
|
65
|
+
name = row["First Name"] if !row["First Name"].nil?
|
66
|
+
name << " #{row["Last Name"]}" if !row["Last Name"].nil?
|
67
|
+
[name, row["E-mail Address"]]
|
68
|
+
end
|
69
|
+
else
|
70
|
+
@contacts || []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
TYPES[:hotmail] = Hotmail
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
if !Object.const_defined?('ActiveSupport')
|
2
|
+
require 'json'
|
3
|
+
end
|
4
|
+
|
5
|
+
class Contacts
|
6
|
+
def self.parse_json( string )
|
7
|
+
if Object.const_defined?('ActiveSupport') and
|
8
|
+
ActiveSupport.const_defined?('JSON')
|
9
|
+
ActiveSupport::JSON.decode( string )
|
10
|
+
elsif Object.const_defined?('JSON')
|
11
|
+
JSON.parse( string )
|
12
|
+
else
|
13
|
+
raise 'Contacts requires JSON or Rails (with ActiveSupport::JSON)'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
class Contacts
|
4
|
+
class Mailru < Base
|
5
|
+
LOGIN_URL = "https://auth.mail.ru/cgi-bin/auth"
|
6
|
+
ADDRESS_BOOK_URL = "http://win.mail.ru/cgi-bin/abexport/addressbook.csv"
|
7
|
+
|
8
|
+
attr_accessor :cookies
|
9
|
+
|
10
|
+
def real_connect
|
11
|
+
username = login
|
12
|
+
|
13
|
+
postdata = "Login=%s&Domain=%s&Password=%s" % [
|
14
|
+
CGI.escape(username),
|
15
|
+
CGI.escape(domain_param(username)),
|
16
|
+
CGI.escape(password)
|
17
|
+
]
|
18
|
+
|
19
|
+
data, resp, self.cookies, forward = post(LOGIN_URL, postdata, "")
|
20
|
+
|
21
|
+
if data.index("fail=1")
|
22
|
+
raise AuthenticationError, "Username and password do not match"
|
23
|
+
elsif cookies == "" or data == ""
|
24
|
+
raise ConnectionError, PROTOCOL_ERROR
|
25
|
+
end
|
26
|
+
|
27
|
+
data, resp, cookies, forward = get(login_token_link(data), login_cookies.join(';'))
|
28
|
+
end
|
29
|
+
|
30
|
+
def contacts
|
31
|
+
postdata = "confirm=1&abtype=6"
|
32
|
+
data, resp, cookies, forward = post(ADDRESS_BOOK_URL, postdata, login_cookies.join(';'))
|
33
|
+
|
34
|
+
@contacts = []
|
35
|
+
CSV.parse(data) do |row|
|
36
|
+
@contacts << [row[0], row[4]] unless header_row?(row)
|
37
|
+
end
|
38
|
+
|
39
|
+
@contacts
|
40
|
+
end
|
41
|
+
|
42
|
+
def skip_gzip?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def login_token_link(data)
|
48
|
+
data.match(/url=(.+)\">/)[1]
|
49
|
+
end
|
50
|
+
|
51
|
+
def login_cookies
|
52
|
+
self.cookies.split(';').collect{|c| c if (c.include?('t=') or c.include?('Mpop='))}.compact.collect{|c| c.strip}
|
53
|
+
end
|
54
|
+
|
55
|
+
def header_row?(row)
|
56
|
+
row[0] == 'AB-Name'
|
57
|
+
end
|
58
|
+
|
59
|
+
def domain_param(login)
|
60
|
+
login.include?('@') ?
|
61
|
+
login.match(/.+@(.+)/)[1] :
|
62
|
+
'mail.ru'
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
TYPES[:mailru] = Mailru
|
68
|
+
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,105 @@
|
|
1
|
+
class Contacts
|
2
|
+
class Yahoo < Base
|
3
|
+
URL = "http://mail.yahoo.com/"
|
4
|
+
LOGIN_URL = "https://login.yahoo.com/config/login"
|
5
|
+
ADDRESS_BOOK_URL = "http://address.mail.yahoo.com/?.rand=430244936"
|
6
|
+
CONTACT_LIST_URL = "http://address.mail.yahoo.com/?_src=&_crumb=crumb&sortfield=3&bucket=1&scroll=1&VPC=social_list&.r=time"
|
7
|
+
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"
|
8
|
+
|
9
|
+
def real_connect
|
10
|
+
postdata = ".tries=2&.src=ym&.md5=&.hash=&.js=&.last=&promo=&.intl=us&.bypass="
|
11
|
+
postdata += "&.partner=&.u=4eo6isd23l8r3&.v=0&.challenge=gsMsEcoZP7km3N3NeI4mX"
|
12
|
+
postdata += "kGB7zMV&.yplus=&.emailCode=&pkg=&stepid=&.ev=&hasMsgr=1&.chkP=Y&."
|
13
|
+
postdata += "done=#{CGI.escape(URL)}&login=#{CGI.escape(login)}&passwd=#{CGI.escape(password)}"
|
14
|
+
|
15
|
+
data, resp, cookies, forward = post(LOGIN_URL, postdata)
|
16
|
+
|
17
|
+
if data.index("Invalid ID or password") || data.index("This ID is not yet taken")
|
18
|
+
raise AuthenticationError, "Username and password do not match"
|
19
|
+
elsif data.index("Sign in") && data.index("to Yahoo!")
|
20
|
+
raise AuthenticationError, "Required field must not be blank"
|
21
|
+
elsif !data.match(/uncompressed\/chunked/)
|
22
|
+
raise ConnectionError, PROTOCOL_ERROR
|
23
|
+
elsif cookies == ""
|
24
|
+
raise ConnectionError, PROTOCOL_ERROR
|
25
|
+
end
|
26
|
+
|
27
|
+
data, resp, cookies, forward = get(forward, cookies, LOGIN_URL)
|
28
|
+
|
29
|
+
if resp.code_type != Net::HTTPOK
|
30
|
+
raise ConnectionError, PROTOCOL_ERROR
|
31
|
+
end
|
32
|
+
|
33
|
+
@cookies = cookies
|
34
|
+
end
|
35
|
+
|
36
|
+
def contacts
|
37
|
+
return @contacts if @contacts
|
38
|
+
@contacts = []
|
39
|
+
|
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[/dotCrumb: '(.*?)'/][13...-1]
|
53
|
+
|
54
|
+
# now proceed with the new ".crumb" parameter to get the csv data
|
55
|
+
url = URI.parse(contact_list_url.sub("_crumb=crumb","_crumb=#{crumb}").sub("time", Time.now.to_f.to_s.sub(".","")[0...-2]))
|
56
|
+
http = open_http(url)
|
57
|
+
resp, more_data = http.get("#{url.path}?#{url.query}",
|
58
|
+
"Cookie" => @cookies,
|
59
|
+
"X-Requested-With" => "XMLHttpRequest",
|
60
|
+
"Referer" => address_book_url
|
61
|
+
)
|
62
|
+
|
63
|
+
if resp.code_type != Net::HTTPOK
|
64
|
+
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
|
65
|
+
end
|
66
|
+
|
67
|
+
if more_data =~ /"TotalABContacts":(\d+)/
|
68
|
+
total = $1.to_i
|
69
|
+
((total / 50.0).ceil).times do |i|
|
70
|
+
# now proceed with the new ".crumb" parameter to get the csv data
|
71
|
+
url = URI.parse(contact_list_url.sub("bucket=1","bucket=#{i}").sub("_crumb=crumb","_crumb=#{crumb}").sub("time", Time.now.to_f.to_s.sub(".","")[0...-2]))
|
72
|
+
http = open_http(url)
|
73
|
+
resp, more_data = http.get("#{url.path}?#{url.query}",
|
74
|
+
"Cookie" => @cookies,
|
75
|
+
"X-Requested-With" => "XMLHttpRequest",
|
76
|
+
"Referer" => address_book_url
|
77
|
+
)
|
78
|
+
|
79
|
+
if resp.code_type != Net::HTTPOK
|
80
|
+
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
|
81
|
+
end
|
82
|
+
|
83
|
+
parse more_data
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
@contacts
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def parse(data, options={})
|
94
|
+
@contacts ||= []
|
95
|
+
@contacts += Contacts.parse_json(data)["response"]["ResultSet"]["Contacts"].to_a.select{|contact|!contact["email"].to_s.empty?}.map do |contact|
|
96
|
+
name = contact["contactName"].split(",")
|
97
|
+
[[name.pop, name.join(",")].join(" ").strip, contact["email"]]
|
98
|
+
end if data =~ /^\{"response":/
|
99
|
+
@contacts
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
TYPES[:yahoo] = Yahoo
|
105
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: liangzan-contacts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.6
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lucas Carlson
|
9
|
+
- Brad Imbierowicz
|
10
|
+
- Wong Liang Zan
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-02-21 00:00:00.000000000Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: json
|
18
|
+
requirement: &15704640 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.1.1
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *15704640
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: gdata_19
|
29
|
+
requirement: &15703120 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.1.3
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *15703120
|
38
|
+
description: A universal interface to grab contact list information from various providers
|
39
|
+
including Yahoo, AOL, Gmail, Hotmail, and Plaxo. Now supporting Ruby 1.9.
|
40
|
+
email: zan@liangzan.net
|
41
|
+
executables: []
|
42
|
+
extensions: []
|
43
|
+
extra_rdoc_files: []
|
44
|
+
files:
|
45
|
+
- lib/contacts/base.rb
|
46
|
+
- lib/contacts/gmail.rb
|
47
|
+
- lib/contacts/plaxo.rb
|
48
|
+
- lib/contacts/yahoo.rb
|
49
|
+
- lib/contacts/hotmail.rb
|
50
|
+
- lib/contacts/mailru.rb
|
51
|
+
- lib/contacts/aol.rb
|
52
|
+
- lib/contacts/json_picker.rb
|
53
|
+
- lib/contacts.rb
|
54
|
+
- examples/grab_contacts.rb
|
55
|
+
- LICENSE
|
56
|
+
- README.rdoc
|
57
|
+
- Rakefile
|
58
|
+
homepage: http://github.com/liangzan/contacts
|
59
|
+
licenses: []
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.8.10
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: grab contacts from Yahoo, AOL, Gmail, Hotmail, and Plaxo
|
82
|
+
test_files: []
|