foco-contacts 1.2.18

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.rdoc ADDED
@@ -0,0 +1,68 @@
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
+ or on the *Gemfile*
14
+
15
+ gem 'liangzan-contacts', '~>1.2.8', :require => 'contacts'
16
+
17
+ == Background
18
+
19
+ 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.
20
+
21
+ == Usage
22
+
23
+ # returns [["name", "foo@bar.com"], ["another name", "bow@wow.com"]]
24
+ Contacts::Hotmail.new(login, password).contacts
25
+ Contacts::Yahoo.new(login, password).contacts
26
+ Contacts::Gmail.new(login, password).contacts
27
+
28
+ Contacts.new(:gmail, login, password).contacts
29
+ Contacts.new(:hotmail, login, password).contacts
30
+ Contacts.new(:yahoo, login, password).contacts
31
+
32
+ Contacts.guess(login, password).contacts
33
+
34
+ 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.
35
+
36
+ == Captcha error
37
+
38
+ 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:
39
+
40
+ Contacts::Gmail.new(login,
41
+ password,
42
+ :captcha_token => params[:captcha_token],
43
+ :captcha_response => params[:captcha_response]).contacts
44
+
45
+ == Examples
46
+
47
+ See the examples/ directory.
48
+
49
+ == Authors
50
+
51
+ * Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
52
+
53
+ == Contributors
54
+
55
+ * Britt Selvitelle from Twitter (mailto:anotherbritt@gmail.com) - http://twitter.com
56
+ * Tony Targonski from GigPark (mailto:tony@gigpark.com) - http://gigpark.com
57
+ * Waheed Barghouthi from Watwet (mailto:waheed.barghouthi@gmail.com) - http://watwet.com
58
+ * Glenn Sidney from Glenn Fu (mailto:glenn@glennfu.com) - http://glennfu.com
59
+ * Brian McQuay from Onomojo (mailto:brian@onomojo.com) - http://onomojo.com
60
+ * Adam Hunter (mailto:adamhunter@me.com) - http://adamhunter.me/
61
+ * Glenn Ford (mailto:glenn@glennfu.com) - http://www.glennfu.com/
62
+ * Leonardo Wong (mailto:mac@boy.name)
63
+ * Rusty Burchfield
64
+ * justintv
65
+ * Wong Liang Zan (mailto:zan@liangzan.net) - http://liangzan.net
66
+
67
+ This library is released under the terms of the BSD.
68
+
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
@@ -0,0 +1,18 @@
1
+ $:.unshift(File.dirname(__FILE__)+"/contacts/")
2
+
3
+ require 'rubygems'
4
+
5
+ require 'base'
6
+ require 'json_picker'
7
+ require 'gmail'
8
+ require 'hotmail'
9
+ require 'yahoo'
10
+ # require 'plaxo'
11
+ # require 'aol'
12
+ # require 'mailru'
13
+ # require 'gmx'
14
+ # require 'web_de'
15
+ # require 'seznam'
16
+ # require 'onelt'
17
+ # require 'inbox_lt'
18
+ # require 'tonline_de'
@@ -0,0 +1,157 @@
1
+ class Contacts
2
+ require 'hpricot' if RUBY_VERSION < '1.9'
3
+ require 'csv'
4
+ class Aol < Base
5
+ DETECTED_DOMAINS = [ /aol.com/i ]
6
+ URL = "http://www.aol.com/"
7
+ LOGIN_URL = "https://my.screenname.aol.com/_cqr/login/login.psp"
8
+ LOGIN_REFERER_URL = "http://webmail.aol.com/"
9
+ LOGIN_REFERER_PATH = "sitedomain=sns.webmail.aol.com&lang=en&locale=us&authLev=0&uitype=mini&loginId=&redirType=js&xchk=false"
10
+ AOL_NUM = "35752-111" # this seems to change each time they change the protocol
11
+
12
+ CONTACT_LIST_URL = "http://mail.aol.com/#{AOL_NUM}/aol-6/en-us/Lite/ContactList.aspx"
13
+ CONTACT_LIST_CSV_URL = "http://mail.aol.com/#{AOL_NUM}/aol-6/en-us/Lite/ABExport.aspx?command=all"
14
+ 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"
15
+
16
+ def real_connect
17
+ if login.strip =~ /^(.+)@aol\.com$/ # strip off the @aol.com for AOL logins
18
+ login = $1
19
+ end
20
+
21
+ postdata = {
22
+ "loginId" => login,
23
+ "password" => password,
24
+ "rememberMe" => "on",
25
+ "_sns_fg_color_" => "",
26
+ "_sns_err_color_" => "",
27
+ "_sns_link_color_" => "",
28
+ "_sns_width_" => "",
29
+ "_sns_height_" => "",
30
+ "offerId" => "mail-second-en-us",
31
+ "_sns_bg_color_" => "",
32
+ "sitedomain" => "sns.webmail.aol.com",
33
+ "regPromoCode" => "",
34
+ "mcState" => "initialized",
35
+ "uitype" => "std",
36
+ "siteId" => "",
37
+ "lang" => "en",
38
+ "locale" => "us",
39
+ "authLev" => "0",
40
+ "siteState" => "",
41
+ "isSiteStateEncoded" => "false",
42
+ "use_aam" => "0",
43
+ "seamless" => "novl",
44
+ "aolsubmit" => CGI.escape("Sign In"),
45
+ "idType" => "SN",
46
+ "usrd" => "",
47
+ "doSSL" => "",
48
+ "redirType" => "",
49
+ "xchk" => "false"
50
+ }
51
+
52
+ # Get this cookie and stick it in the form to confirm to Aol that your cookies work
53
+ data, resp, cookies, forward = get(URL)
54
+ postdata["stips"] = cookie_hash_from_string(cookies)["stips"]
55
+ postdata["tst"] = cookie_hash_from_string(cookies)["tst"]
56
+
57
+ data, resp, cookies, forward, old_url = get(LOGIN_REFERER_URL, cookies) + [URL]
58
+ until forward.nil?
59
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
60
+ end
61
+
62
+ data, resp, cookies, forward, old_url = get("#{LOGIN_URL}?#{LOGIN_REFERER_PATH}", cookies) + [LOGIN_REFERER_URL]
63
+ until forward.nil?
64
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
65
+ end
66
+
67
+ doc = Nokogiri(data)
68
+ (doc/'input[name=usrd]').each do |input|
69
+ postdata["usrd"] = input['value']
70
+ end
71
+ # parse data for <input name="usrd" value="2726212" type="hidden"> and add it to the postdata
72
+
73
+ postdata["SNS_SC"] = cookie_hash_from_string(cookies)["SNS_SC"]
74
+ postdata["SNS_LDC"] = cookie_hash_from_string(cookies)["SNS_LDC"]
75
+ postdata["LTState"] = cookie_hash_from_string(cookies)["LTState"]
76
+ # raise data.inspect
77
+
78
+ data, resp, cookies, forward, old_url = post(LOGIN_URL, h_to_query_string(postdata), cookies, LOGIN_REFERER_URL) + [LOGIN_REFERER_URL]
79
+
80
+ until forward.nil?
81
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
82
+ end
83
+
84
+ if data.index("Incorrect Username or Password.")
85
+ raise AuthenticationError, "Username and password do not match"
86
+ elsif data.index("Required field must not be blank")
87
+ raise AuthenticationError, "Login and password must not be blank"
88
+ elsif data.index("errormsg_0_logincaptcha")
89
+ raise AuthenticationError, "Captcha error"
90
+ elsif data.index("Invalid request")
91
+ raise ConnectionError, PROTOCOL_ERROR
92
+ elsif cookies == ""
93
+ raise ConnectionError, PROTOCOL_ERROR
94
+ end
95
+
96
+ @cookies = cookies
97
+ end
98
+
99
+ def contacts
100
+ postdata = {
101
+ "file" => 'contacts',
102
+ "fileType" => 'csv'
103
+ }
104
+
105
+ return @contacts if @contacts
106
+ if connected?
107
+ data, resp, cookies, forward, old_url = get(CONTACT_LIST_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
108
+
109
+ until forward.nil?
110
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
111
+ end
112
+
113
+ if resp.code_type != Net::HTTPOK
114
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
115
+ end
116
+
117
+ user = nil
118
+
119
+ # parse data and grab <input name="user" value="8QzMPIAKs2" type="hidden">
120
+ doc = Nokogiri(data)
121
+ (doc/'input[name=user]').each do |input|
122
+ user = input["value"]
123
+ end
124
+
125
+ data, resp, cookies, forward, old_url = get(CONTACT_LIST_CSV_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
126
+
127
+ until forward.nil?
128
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
129
+ end
130
+
131
+ if data.include?("error.gif")
132
+ raise AuthenticationError, "Account invalid"
133
+ end
134
+
135
+ parse data
136
+ end
137
+ end
138
+ private
139
+
140
+ def parse(data, options={})
141
+ data = CSV.parse(data)
142
+ col_names = data.shift
143
+ @contacts = data.map do |person|
144
+ ["#{person[0]} #{person[1]}", person[4]] if person[4] && !person[4].empty?
145
+ end.compact
146
+ end
147
+
148
+ def h_to_query_string(hash)
149
+ u = ERB::Util.method(:u)
150
+ hash.map { |k, v|
151
+ u.call(k) + "=" + u.call(v)
152
+ }.join("&")
153
+ end
154
+ end
155
+
156
+ TYPES[:aol] = Aol
157
+ end
@@ -0,0 +1,249 @@
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
+ DETECTED_DOMAINS = []
15
+
16
+ def initialize(login, password, options={})
17
+ @login = login
18
+ @password = password
19
+ @captcha_token = options[:captcha_token]
20
+ @captcha_response = options[:captcha_response]
21
+ @connections = {}
22
+ connect
23
+ end
24
+
25
+ def connect
26
+ # raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @login.empty? || @password.nil? || @password.empty?
27
+ real_connect if !@login.nil? && !@login.empty? && !@password.nil? && !@password.empty?
28
+ end
29
+
30
+ def connected?
31
+ @cookies && !@cookies.empty?
32
+ end
33
+
34
+ def contacts(options = {})
35
+ return @contacts if @contacts
36
+ if connected?
37
+ url = URI.parse(contact_list_url)
38
+ http = open_http(url)
39
+ resp = http.get("#{url.path}?#{url.query}",
40
+ "Cookie" => @cookies
41
+ )
42
+
43
+ if resp.code_type != Net::HTTPOK
44
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
45
+ end
46
+
47
+ parse(resp.body, options)
48
+ end
49
+ end
50
+
51
+ def login
52
+ @attempt ||= 0
53
+ @attempt += 1
54
+
55
+ if @attempt == 1
56
+ @login
57
+ else
58
+ if @login.include?("@#{domain}")
59
+ @login.sub("@#{domain}","")
60
+ else
61
+ "#{@login}@#{domain}"
62
+ end
63
+ end
64
+ end
65
+
66
+ def password
67
+ @password
68
+ end
69
+
70
+ def skip_gzip?
71
+ false
72
+ end
73
+
74
+ private
75
+
76
+ def domain
77
+ @d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
78
+ end
79
+
80
+ def contact_list_url
81
+ self.class.const_get(:CONTACT_LIST_URL)
82
+ end
83
+
84
+ def address_book_url
85
+ self.class.const_get(:ADDRESS_BOOK_URL)
86
+ end
87
+
88
+ def open_http(url)
89
+ c = @connections[Thread.current.object_id] ||= {}
90
+ http = c["#{url.host}:#{url.port}"]
91
+ unless http
92
+ http = Net::HTTP.new(url.host, url.port)
93
+ if url.port == 443
94
+ http.use_ssl = true
95
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
96
+ end
97
+ c["#{url.host}:#{url.port}"] = http
98
+ end
99
+ http.start unless http.started?
100
+ http
101
+ end
102
+
103
+ def cookie_hash_from_string(cookie_string)
104
+ cookie_string.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
105
+ end
106
+
107
+ def parse_cookies(data, existing="")
108
+ return existing if data.nil?
109
+
110
+ cookies = cookie_hash_from_string(existing)
111
+
112
+ data.gsub!(/ ?[\w]+=EXPIRED;/,'')
113
+ data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
114
+ data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
115
+ data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
116
+ data.gsub!(/(;\s*){2,}/,', ')
117
+ data.gsub!(/(,\s*){2,}/,', ')
118
+ data.sub!(/^,\s*/,'')
119
+ data.sub!(/\s*,$/,'')
120
+
121
+ data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
122
+ k, v = data.split("=", 2).map{|j|j.strip}
123
+ if cookies[k] && v.empty?
124
+ cookies.delete(k)
125
+ elsif v && !v.empty?
126
+ cookies[k] = v
127
+ end
128
+ end
129
+
130
+ cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
131
+ end
132
+
133
+ def remove_cookie(cookie, cookies)
134
+ parse_cookies("#{cookie}=", cookies)
135
+ end
136
+
137
+ def post(url, postdata, cookies="", referer="")
138
+ url = URI.parse(url)
139
+ http = open_http(url)
140
+ http_header = { "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
141
+ "Accept-Encoding" => "gzip",
142
+ "Cookie" => cookies,
143
+ "Referer" => referer,
144
+ "Content-Type" => 'application/x-www-form-urlencoded'
145
+ }
146
+ http_header.reject!{|k, v| k == 'Accept-Encoding'} if skip_gzip?
147
+ resp = http.post(url.path, postdata, http_header)
148
+ data = uncompress(resp)
149
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
150
+ forward = resp.response['Location']
151
+ forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
152
+ if (not forward.nil?) && URI.parse(forward).host.nil?
153
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
154
+ end
155
+ return data, resp, cookies, forward
156
+ end
157
+
158
+ def get(url, cookies="", referer="")
159
+ url = URI.parse(url)
160
+ http = open_http(url)
161
+ resp = http.get("#{url.path}?#{url.query}",
162
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
163
+ "Accept-Encoding" => "gzip",
164
+ "Cookie" => cookies,
165
+ "Referer" => referer
166
+ )
167
+ data = uncompress(resp)
168
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
169
+ forward = resp.response['Location']
170
+ forward.gsub!(' ', '%20') unless forward.nil?
171
+
172
+ if (not forward.nil?) && URI.parse(forward).host.nil?
173
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
174
+ end
175
+ return data, resp, cookies, forward
176
+ end
177
+
178
+ def uncompress(resp)
179
+ data = resp.body
180
+ case resp.response['content-encoding']
181
+ when 'gzip'
182
+ gz = Zlib::GzipReader.new(StringIO.new(data))
183
+ data = gz.read
184
+ gz.close
185
+ resp.response['content-encoding'] = nil
186
+ # FIXME: Not sure what Hotmail was feeding me with their 'deflate',
187
+ # but the headers definitely were not right
188
+ when 'deflate'
189
+ data = Zlib::Inflate.inflate(data)
190
+ resp.response['content-encoding'] = nil
191
+ end
192
+
193
+ data
194
+ end
195
+ end
196
+
197
+ class ContactsError < StandardError
198
+ end
199
+
200
+ class AuthenticationError < ContactsError
201
+ end
202
+
203
+ class ConnectionError < ContactsError
204
+ end
205
+
206
+ class TypeNotFound < ContactsError
207
+ end
208
+
209
+ def self.new(type, login, password, options={})
210
+ if TYPES.include?(type.to_s.intern)
211
+ TYPES[type.to_s.intern].new(login, password, options)
212
+ else
213
+ raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect}"
214
+ end
215
+ end
216
+
217
+ def self.get_email_domain(email)
218
+ name, domain = email.split('@')
219
+ domain
220
+ end
221
+
222
+ def self.guess_importer(email, options={})
223
+ if keys = options[:types]
224
+ types = TYPES.select{|k, v| keys.include?(k)}
225
+ end
226
+ types ||= TYPES
227
+
228
+ email_domain = get_email_domain(email)
229
+
230
+ types.values.find do |klass|
231
+ klass::DETECTED_DOMAINS.any? { |m| email_domain.to_s.match(m) }
232
+ end
233
+ end
234
+
235
+ def self.guess(email, password, options={})
236
+ klass = guess_importer(email, options)
237
+
238
+ return if klass.nil?
239
+
240
+ a = []
241
+
242
+ begin
243
+ a = klass.new(email, password, options).contacts
244
+ rescue AuthenticationError
245
+ end
246
+
247
+ return a
248
+ end
249
+ end