j-contacts 1.2.4

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,57 @@
1
+ == Welcome to Contacts
2
+
3
+ Contacts is a universal interface to grab contact list information from various providers including Hotmail, AOL, Gmail, Plaxo and Yahoo.
4
+
5
+ == Download
6
+
7
+ * gem install contacts
8
+ * http://github.com/cardmagic/contacts
9
+ * git clone git://github.com/cardmagic/contacts.git
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
+ == Captcha error
30
+
31
+ 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:
32
+
33
+ Contacts::Gmail.new(login, password, :captcha_token => params[:captcha_token], :captcha_response => params[:captcha_response]).contacts
34
+
35
+ == Examples
36
+
37
+ See the examples/ directory.
38
+
39
+ == Authors
40
+
41
+ * Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
42
+
43
+ == Contributors
44
+
45
+ * Britt Selvitelle from Twitter (mailto:anotherbritt@gmail.com) - http://twitter.com
46
+ * Tony Targonski from GigPark (mailto:tony@gigpark.com) - http://gigpark.com
47
+ * Waheed Barghouthi from Watwet (mailto:waheed.barghouthi@gmail.com) - http://watwet.com
48
+ * Glenn Sidney from Glenn Fu (mailto:glenn@glennfu.com) - http://glennfu.com
49
+ * Brian McQuay from Onomojo (mailto:brian@onomojo.com) - http://onomojo.com
50
+ * Adam Hunter (mailto:adamhunter@me.com) - http://adamhunter.me/
51
+ * Glenn Ford (mailto:glenn@glennfu.com) - http://www.glennfu.com/
52
+ * Leonardo Wong (mailto:mac@boy.name)
53
+ * Rusty Burchfield
54
+ * justintv
55
+
56
+ This library is released under the terms of the BSD.
57
+
data/Rakefile ADDED
@@ -0,0 +1,91 @@
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
+ ] - ["test/accounts.yml"]
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 = 'adamhunter-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.add_dependency('gdata', '= 1.1.1')
66
+ s.requirements << "A json parser, the gdata ruby gem"
67
+
68
+ #### Documentation and testing.
69
+
70
+ s.has_rdoc = true
71
+
72
+ #### Author and project details.
73
+
74
+ s.author = "Lucas Carlson"
75
+ s.email = "lucas@rufy.com"
76
+ s.homepage = "http://rubyforge.org/projects/contacts"
77
+ end
78
+
79
+ Rake::GemPackageTask.new(spec) do |pkg|
80
+ pkg.need_zip = true
81
+ pkg.need_tar = true
82
+ end
83
+
84
+ desc "Report code statistics (KLOCs, etc) from the application"
85
+ task :stats do
86
+ require 'code_statistics'
87
+ CodeStatistics.new(
88
+ ["Library", "lib"],
89
+ ["Units", "test"]
90
+ ).to_s
91
+ 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,12 @@
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'
@@ -0,0 +1,154 @@
1
+ class Contacts
2
+ require 'hpricot'
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,222 @@
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
+ VERSION = "1.2.4"
13
+
14
+ class Base
15
+ def initialize(login, password, options={})
16
+ @login = login
17
+ @password = password
18
+ @captcha_token = options[:captcha_token]
19
+ @captcha_response = options[:captcha_response]
20
+ @connections = {}
21
+ connect
22
+ end
23
+
24
+ def connect
25
+ raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @login.empty? || @password.nil? || @password.empty?
26
+ real_connect
27
+ end
28
+
29
+ def connected?
30
+ @cookies && !@cookies.empty?
31
+ end
32
+
33
+ def contacts(options = {})
34
+ return @contacts if @contacts
35
+ if connected?
36
+ url = URI.parse(contact_list_url)
37
+ http = open_http(url)
38
+ resp, data = http.get("#{url.path}?#{url.query}",
39
+ "Cookie" => @cookies
40
+ )
41
+
42
+ if resp.code_type != Net::HTTPOK
43
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
44
+ end
45
+
46
+ parse(data, options)
47
+ end
48
+ end
49
+
50
+ def login
51
+ @attempt ||= 0
52
+ @attempt += 1
53
+
54
+ if @attempt == 1
55
+ @login
56
+ else
57
+ if @login.include?("@#{domain}")
58
+ @login.sub("@#{domain}","")
59
+ else
60
+ "#{@login}@#{domain}"
61
+ end
62
+ end
63
+ end
64
+
65
+ def password
66
+ @password
67
+ end
68
+
69
+ def skip_gzip?
70
+ false
71
+ end
72
+
73
+ private
74
+
75
+ def domain
76
+ @d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
77
+ end
78
+
79
+ def contact_list_url
80
+ self.class.const_get(:CONTACT_LIST_URL)
81
+ end
82
+
83
+ def address_book_url
84
+ self.class.const_get(:ADDRESS_BOOK_URL)
85
+ end
86
+
87
+ def open_http(url)
88
+ c = @connections[Thread.current.object_id] ||= {}
89
+ http = c["#{url.host}:#{url.port}"]
90
+ unless http
91
+ http = Net::HTTP.new(url.host, url.port)
92
+ if url.port == 443
93
+ http.use_ssl = true
94
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
95
+ end
96
+ c["#{url.host}:#{url.port}"] = http
97
+ end
98
+ http.start unless http.started?
99
+ http
100
+ end
101
+
102
+ def cookie_hash_from_string(cookie_string)
103
+ cookie_string.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
104
+ end
105
+
106
+ def parse_cookies(data, existing="")
107
+ return existing if data.nil?
108
+
109
+ cookies = cookie_hash_from_string(existing)
110
+
111
+ data.gsub!(/ ?[\w]+=EXPIRED;/,'')
112
+ data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
113
+ data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
114
+ data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
115
+ data.gsub!(/(;\s*){2,}/,', ')
116
+ data.gsub!(/(,\s*){2,}/,', ')
117
+ data.sub!(/^,\s*/,'')
118
+ data.sub!(/\s*,$/,'')
119
+
120
+ data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
121
+ k, v = data.split("=", 2).map{|j|j.strip}
122
+ if cookies[k] && v.empty?
123
+ cookies.delete(k)
124
+ elsif v && !v.empty?
125
+ cookies[k] = v
126
+ end
127
+ end
128
+
129
+ cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
130
+ end
131
+
132
+ def remove_cookie(cookie, cookies)
133
+ parse_cookies("#{cookie}=", cookies)
134
+ end
135
+
136
+ def post(url, postdata, cookies="", referer="")
137
+ url = URI.parse(url)
138
+ http = open_http(url)
139
+ http_header = { "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
140
+ "Accept-Encoding" => "gzip",
141
+ "Cookie" => cookies,
142
+ "Referer" => referer,
143
+ "Content-Type" => 'application/x-www-form-urlencoded'
144
+ }
145
+ http_header.reject!{|k, v| k == 'Accept-Encoding'} if skip_gzip?
146
+ resp, data = http.post(url.path, postdata, http_header)
147
+ data = uncompress(resp, data)
148
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
149
+ forward = resp.response['Location']
150
+ forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
151
+ if (not forward.nil?) && URI.parse(forward).host.nil?
152
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
153
+ end
154
+ return data, resp, cookies, forward
155
+ end
156
+
157
+ def get(url, cookies="", referer="")
158
+ url = URI.parse(url)
159
+ http = open_http(url)
160
+ resp, data = http.get("#{url.path}?#{url.query}",
161
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
162
+ "Accept-Encoding" => "gzip",
163
+ "Cookie" => cookies,
164
+ "Referer" => referer
165
+ )
166
+ data = uncompress(resp, data)
167
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
168
+ forward = resp.response['Location']
169
+ if (not forward.nil?) && URI.parse(forward).host.nil?
170
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
171
+ end
172
+ return data, resp, cookies, forward
173
+ end
174
+
175
+ def uncompress(resp, data)
176
+ case resp.response['content-encoding']
177
+ when 'gzip'
178
+ gz = Zlib::GzipReader.new(StringIO.new(data))
179
+ data = gz.read
180
+ gz.close
181
+ resp.response['content-encoding'] = nil
182
+ # FIXME: Not sure what Hotmail was feeding me with their 'deflate',
183
+ # but the headers definitely were not right
184
+ when 'deflate'
185
+ data = Zlib::Inflate.inflate(data)
186
+ resp.response['content-encoding'] = nil
187
+ end
188
+
189
+ data
190
+ end
191
+ end
192
+
193
+ class ContactsError < StandardError
194
+ end
195
+
196
+ class AuthenticationError < ContactsError
197
+ end
198
+
199
+ class ConnectionError < ContactsError
200
+ end
201
+
202
+ class TypeNotFound < ContactsError
203
+ end
204
+
205
+ def self.new(type, login, password, options={})
206
+ if TYPES.include?(type.to_s.intern)
207
+ TYPES[type.to_s.intern].new(login, password, options)
208
+ else
209
+ raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect}"
210
+ end
211
+ end
212
+
213
+ def self.guess(login, password, options={})
214
+ TYPES.inject([]) do |a, t|
215
+ begin
216
+ a + t[1].new(login, password, options).contacts
217
+ rescue AuthenticationError
218
+ a
219
+ end
220
+ end.uniq
221
+ end
222
+ 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,126 @@
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
+ old_url = form_url
31
+ until cookies =~ /; PPAuth=/ || forward.nil?
32
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
33
+ end
34
+
35
+ if data.index("The e-mail address or password is incorrect")
36
+ raise AuthenticationError, "Username and password do not match"
37
+ elsif data != ""
38
+ raise AuthenticationError, "Required field must not be blank"
39
+ elsif cookies == ""
40
+ raise ConnectionError, PROTOCOL_ERROR
41
+ end
42
+
43
+ data, resp, cookies, forward = get("http://mail.live.com/mail", cookies)
44
+ until forward.nil?
45
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
46
+ end
47
+
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;ru=")
81
+
82
+ raw_html = resp.body.split("
83
+ ").grep(/(?:e|dn)lk[0-9]+/)
84
+ raw_html.inject(-1) 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?("ContactList_next")
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 CGI::unescape(contact[1])
110
+ @contacts << contact
111
+ end
112
+ end
113
+
114
+ return @contacts
115
+ end
116
+ end
117
+
118
+ def get_contact_list_url(index)
119
+ "http://mpeople.live.com/default.aspx?pg=#{index}"
120
+ end
121
+
122
+ private
123
+
124
+ TYPES[:hotmail] = Hotmail
125
+ end
126
+ 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,116 @@
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
+ INVALID_PASS = '<div class="yregertxt"><strong><strong>Invalid ID or password.</strong><br/> Please try again using your full Yahoo! ID.</strong></div>'
9
+ NOT_YET_TAKEN = '<div class="yregertxt"><strong><strong>This ID is not yet taken.</strong><br>Are you trying to '
10
+
11
+ def real_connect(attempt_count=0)
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_PASS) || data.index(NOT_YET_TAKEN)
20
+ if attempt_count < 1
21
+ sleep(5)
22
+ return real_connect(attempt_count + 1)
23
+ else
24
+ raise AuthenticationError, "Username and password do not match"
25
+ end
26
+ elsif data.index("Sign in") && data.index("to Yahoo!")
27
+ raise AuthenticationError, "Required field must not be blank"
28
+ elsif !data.match(/uncompressed\/chunked/)
29
+ raise ConnectionError, PROTOCOL_ERROR
30
+ elsif cookies == ""
31
+ raise ConnectionError, PROTOCOL_ERROR
32
+ end
33
+
34
+ data, resp, cookies, forward = get(forward, cookies, LOGIN_URL)
35
+
36
+ if resp.code_type != Net::HTTPOK
37
+ raise ConnectionError, PROTOCOL_ERROR
38
+ end
39
+
40
+ @cookies = cookies
41
+ end
42
+
43
+ def contacts
44
+ return @contacts if @contacts
45
+ if connected?
46
+ # first, get the addressbook site with the new crumb parameter
47
+ url = URI.parse(address_book_url)
48
+ http = open_http(url)
49
+ resp, data = http.get("#{url.path}?#{url.query}",
50
+ "Cookie" => @cookies
51
+ )
52
+
53
+ if resp.code_type != Net::HTTPOK
54
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
55
+ end
56
+
57
+ crumb = data.to_s[/dotCrumb: '(.*?)'/][13...-1]
58
+
59
+ # now proceed with the new ".crumb" parameter to get the csv data
60
+ url = URI.parse(contact_list_url.sub("_crumb=crumb","_crumb=#{crumb}").sub("time", Time.now.to_f.to_s.sub(".","")[0...-2]))
61
+ http = open_http(url)
62
+ resp, more_data = http.get("#{url.path}?#{url.query}",
63
+ "Cookie" => @cookies,
64
+ "X-Requested-With" => "XMLHttpRequest",
65
+ "Referer" => address_book_url
66
+ )
67
+
68
+ if resp.code_type != Net::HTTPOK
69
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
70
+ end
71
+
72
+ parse data
73
+
74
+ parse more_data
75
+
76
+ if more_data =~ /"TotalABContacts":(\d+)/
77
+ total = $1.to_i
78
+ ((total / 50)).times do |i|
79
+ # now proceed with the new ".crumb" parameter to get the csv data
80
+ url = URI.parse(contact_list_url.sub("bucket=1","bucket=#{i+1}").sub("_crumb=crumb","_crumb=#{crumb}").sub("time", Time.now.to_f.to_s.sub(".","")[0...-2]))
81
+ http = open_http(url)
82
+ resp, more_data = http.get("#{url.path}?#{url.query}",
83
+ "Cookie" => @cookies,
84
+ "X-Requested-With" => "XMLHttpRequest",
85
+ "Referer" => address_book_url
86
+ )
87
+
88
+ if resp.code_type != Net::HTTPOK
89
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
90
+ end
91
+
92
+ parse more_data
93
+ end
94
+ end
95
+
96
+ @contacts
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def parse(data, options={})
103
+ @contacts ||= []
104
+ if data =~ /var InitialContacts = (\[.*?\]);/
105
+ @contacts += Contacts.parse_json($1).select{|contact|!contact["email"].to_s.empty?}.map{|contact|[contact["contactName"], contact["email"]]}
106
+ elsif data =~ /^\{"response":/
107
+ @contacts += Contacts.parse_json(data)["response"]["ResultSet"]["Contacts"].to_a.select{|contact|!contact["email"].to_s.empty?}.map{|contact|[contact["contactName"], contact["email"]]}
108
+ else
109
+ @contacts
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ TYPES[:yahoo] = Yahoo
116
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: j-contacts
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 2
9
+ - 4
10
+ version: 1.2.4
11
+ platform: ruby
12
+ authors:
13
+ - Lucas Carlson
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-06 00:00:00 +03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 17
30
+ segments:
31
+ - 1
32
+ - 1
33
+ - 1
34
+ version: 1.1.1
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: gdata
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 17
46
+ segments:
47
+ - 1
48
+ - 1
49
+ - 1
50
+ version: 1.1.1
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ description: A universal interface to grab contact list information from various providers including Yahoo, AOL, Gmail, Hotmail, and Plaxo.
54
+ email: lucas@rufy.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - LICENSE
63
+ - Rakefile
64
+ - README
65
+ - examples/grab_contacts.rb
66
+ - lib/contacts.rb
67
+ - lib/contacts/base.rb
68
+ - lib/contacts/json_picker.rb
69
+ - lib/contacts/gmail.rb
70
+ - lib/contacts/aol.rb
71
+ - lib/contacts/hotmail.rb
72
+ - lib/contacts/plaxo.rb
73
+ - lib/contacts/yahoo.rb
74
+ - lib/contacts/mailru.rb
75
+ has_rdoc: true
76
+ homepage: http://github.com/cardmagic/contacts
77
+ licenses: []
78
+
79
+ post_install_message:
80
+ rdoc_options: []
81
+
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ requirements: []
103
+
104
+ rubyforge_project:
105
+ rubygems_version: 1.3.7
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: A universal interface to grab contact list information from various providers including Yahoo, AOL, Gmail, Hotmail, and Plaxo.
109
+ test_files: []
110
+