pp-contacts 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2006, Lucas Carlson, MOG
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ Neither the name of the Lucas Carlson nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10
+
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ ## Welcome to Contacts
2
+
3
+ Contacts is a universal interface to grab contact list information from various providers including Outlook, Address Book, Hotmail, AOL, Gmail, Plaxo and Yahoo.
4
+
5
+ ## Download
6
+
7
+ * `gem install contacts`
8
+ * `git clone git://github.com/cardmagic/contacts.git`
9
+
10
+ ## Background
11
+
12
+ 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.
13
+
14
+ ## Usage
15
+
16
+ ```ruby
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
+
28
+ 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.
29
+
30
+ ## Captcha error
31
+
32
+ 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:
33
+
34
+ ```ruby
35
+ Contacts::Gmail.new(login, password, :captcha_token => params[:captcha_token], :captcha_response => params[:captcha_response]).contacts
36
+ ```
37
+
38
+ ## Examples
39
+
40
+ See the examples/ directory.
41
+
42
+ ## Authors
43
+
44
+ * Lucas Carlson from MOG - http://mog.com
45
+ * Paperless Post
46
+
47
+ ## Contributors
48
+
49
+ * [Contributors](https://github.com/paperlesspost/contacts/graphs/contributors)
50
+
51
+ This library is released under the terms of the BSD.
52
+
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 = 'contacts19'
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,33 @@
1
+ $:.unshift(File.dirname(__FILE__)+"/contacts/")
2
+
3
+ ## Use ActiveSupport's version of JSON if available
4
+ #if Object.const_defined?('ActiveSupport') && ActiveSupport.const_defined?('JSON')
5
+ # module ActiveSupportJsonParseFunction
6
+ # def parse(i)
7
+ # ActiveSupport::JSON.decode(i)
8
+ # end
9
+ # end
10
+ # # newer versions of ActiveSupport define a root JSON module to extend...
11
+ # if Object.const_defined?('JSON')
12
+ # JSON.send(:extend, ActiveSupportJsonParseFunction)
13
+ # else
14
+ # # ... older need it defined from scratch
15
+ # class JSON
16
+ # extend ActiveSupportJsonParseFunction
17
+ # end
18
+ # end
19
+ #else
20
+ # require 'json/add/rails'
21
+ #end
22
+
23
+ require 'rubygems'
24
+
25
+ require 'json_picker'
26
+ require 'base'
27
+ require 'gmail'
28
+ require 'hotmail'
29
+ require 'yahoo'
30
+ require 'plaxo'
31
+ require 'vcf'
32
+ require 'outlook'
33
+ require 'aol_importer'
@@ -0,0 +1,151 @@
1
+ require 'hpricot'
2
+ require 'csv'
3
+
4
+ class Contacts
5
+ class AolImporter < Base
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 contacts
17
+ postdata = {
18
+ "file" => 'contacts',
19
+ "fileType" => 'csv'
20
+ }
21
+
22
+ return @contacts if @contacts
23
+ if connected?
24
+ data, resp, cookies, forward, old_url = get(CONTACT_LIST_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
25
+
26
+ until forward.nil?
27
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
28
+ end
29
+
30
+ if resp.code_type != Net::HTTPOK
31
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
32
+ end
33
+
34
+ # parse data and grab <input name="user" value="8QzMPIAKs2" type="hidden">
35
+ doc = Hpricot(data)
36
+ (doc/:input).each do |input|
37
+ postdata["user"] = input.attributes["value"] if input.attributes["name"] == "user"
38
+ end
39
+
40
+ data, resp, cookies, forward, old_url = get(CONTACT_LIST_CSV_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
41
+
42
+ until forward.nil?
43
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
44
+ end
45
+
46
+ if data.include?("error.gif")
47
+ raise AuthenticationError, "Account invalid"
48
+ end
49
+
50
+ parse data
51
+ end
52
+ end
53
+
54
+
55
+ def real_connect
56
+ login.gsub!(/@aol.com/, '') # strip off the @aol.com for AOL logins
57
+
58
+ postdata = {
59
+ "loginId" => login,
60
+ "password" => password,
61
+ "rememberMe" => "on",
62
+ "_sns_fg_color_" => "",
63
+ "_sns_err_color_" => "",
64
+ "_sns_link_color_" => "",
65
+ "_sns_width_" => "",
66
+ "_sns_height_" => "",
67
+ "offerId" => "mail-second-en-us",
68
+ "_sns_bg_color_" => "",
69
+ "sitedomain" => "sns.webmail.aol.com",
70
+ "regPromoCode" => "",
71
+ "mcState" => "initialized",
72
+ "uitype" => "std",
73
+ "siteId" => "",
74
+ "lang" => "en",
75
+ "locale" => "us",
76
+ "authLev" => "0",
77
+ "siteState" => "",
78
+ "isSiteStateEncoded" => "false",
79
+ "use_aam" => "0",
80
+ "seamless" => "novl",
81
+ "aolsubmit" => CGI.escape("Sign In"),
82
+ "idType" => "SN",
83
+ "usrd" => "",
84
+ "doSSL" => "",
85
+ "redirType" => "",
86
+ "xchk" => "false"
87
+ }
88
+
89
+ # Get this cookie and stick it in the form to confirm to Aol that your cookies work
90
+ data, resp, cookies, forward = get(URL)
91
+ postdata["stips"] = cookie_hash_from_string(cookies)["stips"]
92
+ postdata["tst"] = cookie_hash_from_string(cookies)["tst"]
93
+
94
+ data, resp, cookies, forward, old_url = get(LOGIN_REFERER_URL, cookies) + [URL]
95
+ until forward.nil?
96
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
97
+ end
98
+
99
+ data, resp, cookies, forward, old_url = get("#{LOGIN_URL}?#{LOGIN_REFERER_PATH}", cookies) + [LOGIN_REFERER_URL]
100
+ until forward.nil?
101
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
102
+ end
103
+
104
+ doc = Hpricot(data)
105
+ (doc/:input).each do |input|
106
+ postdata["usrd"] = input.attributes["value"] if input.attributes["name"] == "usrd"
107
+ end
108
+ # parse data for <input name="usrd" value="2726212" type="hidden"> and add it to the postdata
109
+
110
+ postdata["SNS_SC"] = cookie_hash_from_string(cookies)["SNS_SC"]
111
+ postdata["SNS_LDC"] = cookie_hash_from_string(cookies)["SNS_LDC"]
112
+ postdata["LTState"] = cookie_hash_from_string(cookies)["LTState"]
113
+
114
+ data, resp, cookies, forward, old_url = post(LOGIN_URL, h_to_query_string(postdata), cookies, LOGIN_REFERER_URL) + [LOGIN_REFERER_URL]
115
+
116
+ until forward.nil?
117
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
118
+ end
119
+
120
+ if data.index("Invalid Username or Password. Please try again.")
121
+ raise AuthenticationError, "Username and password do not match"
122
+ elsif data.index("Required field must not be blank")
123
+ raise AuthenticationError, "Login and password must not be blank"
124
+ elsif data.index("errormsg_0_logincaptcha")
125
+ raise AuthenticationError, "Captcha error"
126
+ elsif data.index("Invalid request")
127
+ raise ConnectionError, PROTOCOL_ERROR
128
+ elsif cookies == ""
129
+ raise ConnectionError, PROTOCOL_ERROR
130
+ end
131
+
132
+ @cookies = cookies
133
+ end
134
+
135
+
136
+ def parse(data, options={})
137
+ data.gsub!(/\"/, "") # remove "s because AOL doesn't escape them properly
138
+ data = CSV.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| u.call(k) + "=" + u.call(v) }.join("&")
148
+ end
149
+ end
150
+ TYPES[:aolImporter] = AolImporter
151
+ end
@@ -0,0 +1,220 @@
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
+ FILETYPES = {}
13
+ VERSION = "1.4.1"
14
+
15
+ class Base
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
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, data = 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(data, 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
+ private
71
+
72
+ def domain
73
+ @d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
74
+ end
75
+
76
+ def contact_list_url
77
+ self.class.const_get(:CONTACT_LIST_URL)
78
+ end
79
+
80
+ def address_book_url
81
+ self.class.const_get(:ADDRESS_BOOK_URL)
82
+ end
83
+
84
+ def open_http(url)
85
+ c = @connections[Thread.current.object_id] ||= {}
86
+ http = c["#{url.host}:#{url.port}"]
87
+ unless http
88
+ http = Net::HTTP.new(url.host, url.port)
89
+ if url.port == 443
90
+ http.use_ssl = true
91
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
92
+ end
93
+ c["#{url.host}:#{url.port}"] = http
94
+ end
95
+ http.start unless http.started?
96
+ http
97
+ end
98
+
99
+ def cookie_hash_from_string(cookie_string)
100
+ cookie_string.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
101
+ end
102
+
103
+ def parse_cookies(data, existing="")
104
+ return existing if data.nil?
105
+
106
+ cookies = cookie_hash_from_string(existing)
107
+
108
+ data.gsub!(/ ?[\w]+=EXPIRED;/,'')
109
+ data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
110
+ data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
111
+ data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
112
+ data.gsub!(/(;\s*){2,}/,', ')
113
+ data.gsub!(/(,\s*){2,}/,', ')
114
+ data.sub!(/^,\s*/,'')
115
+ data.sub!(/\s*,$/,'')
116
+
117
+ data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
118
+ k, v = data.split("=", 2).map{|j|j.strip}
119
+ if cookies[k] && v.empty?
120
+ cookies.delete(k)
121
+ elsif v && !v.empty?
122
+ cookies[k] = v
123
+ end
124
+ end
125
+
126
+ cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
127
+ end
128
+
129
+ def remove_cookie(cookie, cookies)
130
+ parse_cookies("#{cookie}=", cookies)
131
+ end
132
+
133
+ def post(url, postdata, cookies="", referer="")
134
+ url = URI.parse(url)
135
+ http = open_http(url)
136
+ resp, data = http.post(url.path, postdata,
137
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
138
+ "Accept-Encoding" => "gzip",
139
+ "Cookie" => cookies,
140
+ "Referer" => referer,
141
+ "Content-Type" => 'application/x-www-form-urlencoded'
142
+ )
143
+ data = uncompress(resp, data)
144
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
145
+ forward = resp.response['Location']
146
+ forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
147
+ if (not forward.nil?) && URI.parse(forward).host.nil?
148
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
149
+ end
150
+ return data, resp, cookies, forward
151
+ end
152
+
153
+ def get(url, cookies="", referer="")
154
+ url = URI.parse(url)
155
+ http = open_http(url)
156
+ resp, data = http.get("#{url.path}?#{url.query}",
157
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
158
+ "Accept-Encoding" => "gzip",
159
+ "Cookie" => cookies,
160
+ "Referer" => referer
161
+ )
162
+ data = uncompress(resp, data)
163
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
164
+ forward = resp.response['Location']
165
+ if !forward.nil?
166
+ forward = forward.gsub(' ', '%20')
167
+ if URI.parse(forward).host.nil?
168
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
169
+ end
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; end
193
+ class AuthenticationError < ContactsError; end
194
+ class CaptchaError < ContactsError; end
195
+ class ConnectionError < ContactsError; end
196
+ class TypeNotFound < ContactsError; end
197
+
198
+ def self.new(type, login, password="", secret_key="", options={})
199
+ if !password.nil? && password != '' && !secret_key.nil? && secret_key != ''
200
+ password = Encryptor.decrypt(URI.unescape(password), :key => secret_key)
201
+ end
202
+ if TYPES.include?(type.to_s.intern)
203
+ TYPES[type.to_s.intern].new(login, password, options)
204
+ elsif FILETYPES.include?(type.to_s.intern)
205
+ FILETYPES[type.to_s.intern].new(login)
206
+ else
207
+ raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect} or #{FILETYPES.keys.inspect}"
208
+ end
209
+ end
210
+
211
+ def self.guess(login, password, options={})
212
+ TYPES.inject([]) do |a, t|
213
+ begin
214
+ a + t[1].new(login, password, options).contacts
215
+ rescue AuthenticationError
216
+ a
217
+ end
218
+ end.uniq
219
+ end
220
+ end
@@ -0,0 +1,34 @@
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
+ rescue GData::Client::CaptchaError => e
30
+ raise CaptchaError
31
+ end
32
+ end
33
+ TYPES[:gmail] = Gmail
34
+ end
@@ -0,0 +1,93 @@
1
+ require 'csv'
2
+ require 'rubygems'
3
+ require 'nokogiri'
4
+
5
+ class Contacts
6
+ class Hotmail < Base
7
+ DETECTED_DOMAINS = [ /hotmail/i, /live/i, /msn/i, /chaishop/i ]
8
+ URL = "https://login.live.com/login.srf?id=2"
9
+ CONTACT_LIST_URL = "https://mail.live.com/mail/GetContacts.aspx"
10
+ 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"
11
+ PWDPAD = "IfYouAreReadingThisYouHaveTooMuchFreeTime"
12
+
13
+ def real_connect
14
+
15
+ data, resp, cookies, forward = get(URL)
16
+ old_url = URL
17
+ until forward.nil?
18
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
19
+ end
20
+
21
+ postdata = "PPSX=%s&PwdPad=%s&login=%s&passwd=%s&LoginOptions=2&PPFT=%s" % [
22
+ CGI.escape(data.split("><").grep(/PPSX/).first[/=\S+$/][2..-3]),
23
+ PWDPAD[0...(PWDPAD.length-@password.length)],
24
+ CGI.escape(login),
25
+ CGI.escape(password),
26
+ CGI.escape(data.split("><").grep(/PPFT/).first[/=\S+$/][2..-3])
27
+ ]
28
+
29
+ form_url = data.split("><").grep(/form/).first.split[5][8..-2]
30
+ data, resp, cookies, forward = post(form_url, postdata, cookies)
31
+
32
+ old_url = form_url
33
+ until cookies =~ /; PPAuth=/ || forward.nil?
34
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
35
+ end
36
+
37
+ if data.index("The e-mail address or password is incorrect")
38
+ raise AuthenticationError, "Username and password do not match"
39
+ elsif data != ""
40
+ raise AuthenticationError, "Required field must not be blank"
41
+ elsif cookies == ""
42
+ raise ConnectionError, PROTOCOL_ERROR
43
+ end
44
+
45
+ data, resp, cookies, forward = get("http://mail.live.com/mail", cookies)
46
+ until forward.nil?
47
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
48
+ end
49
+
50
+
51
+ @domain = URI.parse(old_url).host
52
+ @cookies = cookies
53
+ rescue AuthenticationError => m
54
+ if @attempt == 1
55
+ retry
56
+ else
57
+ raise m
58
+ end
59
+ end
60
+
61
+ def contacts(options = {})
62
+ if @contacts.nil? && connected?
63
+ url = URI.parse(contact_list_url)
64
+ data, resp, cookies, forward = get(get_contact_list_url, @cookies )
65
+
66
+
67
+ data.force_encoding('ISO-8859-1')
68
+
69
+ @contacts = CSV.parse(data, {:headers => true, :col_sep => data[7]}).map do |row|
70
+ name = ""
71
+ name = row["First Name"] if !row["First Name"].nil?
72
+ name << " #{row["Last Name"]}" if !row["Last Name"].nil?
73
+ [name, row["E-mail Address"] || ""]
74
+ end
75
+ else
76
+ @contacts || []
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ TYPES[:hotmail] = Hotmail
83
+
84
+ # the contacts url is dynamic
85
+ # luckily it tells us where to find it
86
+ def get_contact_list_url
87
+ data = get(CONTACT_LIST_URL, @cookies)[0]
88
+ html_doc = Nokogiri::HTML(data)
89
+ html_doc.xpath("//a")[0]["href"]
90
+ end
91
+ end
92
+ end
93
+
@@ -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,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,102 @@
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
+ if connected?
39
+ # first, get the addressbook site with the new crumb parameter
40
+ url = URI.parse(address_book_url)
41
+ http = open_http(url)
42
+ resp, data = http.get("#{url.path}?#{url.query}",
43
+ "Cookie" => @cookies
44
+ )
45
+
46
+ if resp.code_type != Net::HTTPOK
47
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
48
+ end
49
+
50
+ crumb = data.to_s[/dotCrumb: '(.*?)'/][13...-1]
51
+
52
+ # now proceed with the new ".crumb" parameter to get the csv data
53
+ url = URI.parse(contact_list_url.sub("_crumb=crumb","_crumb=#{crumb}").sub("time", Time.now.to_f.to_s.sub(".","")[0...-2]))
54
+ http = open_http(url)
55
+ resp, more_data = http.get("#{url.path}?#{url.query}",
56
+ "Cookie" => @cookies,
57
+ "X-Requested-With" => "XMLHttpRequest",
58
+ "Referer" => address_book_url
59
+ )
60
+
61
+ if resp.code_type != Net::HTTPOK
62
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
63
+ end
64
+
65
+ if more_data =~ /"TotalABContacts":(\d+)/
66
+ total = $1.to_i
67
+ ((total / 50.0).ceil).times do |i|
68
+ # now proceed with the new ".crumb" parameter to get the csv data
69
+ 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]))
70
+ http = open_http(url)
71
+ resp, more_data = http.get("#{url.path}?#{url.query}",
72
+ "Cookie" => @cookies,
73
+ "X-Requested-With" => "XMLHttpRequest",
74
+ "Referer" => address_book_url
75
+ )
76
+
77
+ if resp.code_type != Net::HTTPOK
78
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
79
+ end
80
+
81
+ parse more_data
82
+ end
83
+ end
84
+
85
+ @contacts
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def parse(data, options={})
92
+ @contacts ||= []
93
+ @contacts += Contacts.parse_json(data)["response"]["ResultSet"]["Contacts"].to_a.select{|contact|!contact["email"].to_s.empty?}.map do |contact|
94
+ name = contact["contactName"].split(",")
95
+ [[name.pop, name.join(",")].join(" ").strip, contact["email"].strip]
96
+ end if data =~ /^\{"response":/
97
+ @contacts
98
+ end
99
+ end
100
+
101
+ TYPES[:yahoo] = Yahoo
102
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pp-contacts
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lucas Carlson
9
+ - Paperless
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-04-26 00:00:00.000000000Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ requirement: &70297907976860 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *70297907976860
26
+ - !ruby/object:Gem::Dependency
27
+ name: gdata19
28
+ requirement: &70297907976440 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *70297907976440
37
+ - !ruby/object:Gem::Dependency
38
+ name: hpricot
39
+ requirement: &70297907975980 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *70297907975980
48
+ - !ruby/object:Gem::Dependency
49
+ name: nokogiri
50
+ requirement: &70297907975540 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: *70297907975540
59
+ - !ruby/object:Gem::Dependency
60
+ name: encryptor
61
+ requirement: &70297907975120 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: *70297907975120
70
+ description: A universal interface to grab contact list information from various providers
71
+ including Outlook, Address Book, Yahoo, AOL, Gmail, Hotmail, and Plaxo.
72
+ email:
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - LICENSE
78
+ - Rakefile
79
+ - README.md
80
+ - examples/grab_contacts.rb
81
+ - lib/contacts.rb
82
+ - lib/contacts/base.rb
83
+ - lib/contacts/json_picker.rb
84
+ - lib/contacts/gmail.rb
85
+ - lib/contacts/aol_importer.rb
86
+ - lib/contacts/hotmail.rb
87
+ - lib/contacts/plaxo.rb
88
+ - lib/contacts/yahoo.rb
89
+ homepage: http://github.com/paperlesspost/contacts
90
+ licenses: []
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 1.8.15
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: A universal interface to grab contact list information from various providers
113
+ including Outlook, Address Book, Yahoo, AOL, Gmail, Hotmail, and Plaxo.
114
+ test_files: []
115
+ has_rdoc: false