Repository: cardmagic/contacts Branch: master Commit: e706105e24f0 Files: 28 Total size: 43.7 KB Directory structure: gitextract_qyvd4b8y/ ├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── contacts.gemspec ├── cruise_config.rb ├── examples/ │ └── grab_contacts.rb ├── geminstaller.yml ├── lib/ │ ├── contacts/ │ │ ├── aol.rb │ │ ├── base.rb │ │ ├── gmail.rb │ │ ├── hotmail.rb │ │ ├── json_picker.rb │ │ ├── mailru.rb │ │ ├── plaxo.rb │ │ └── yahoo.rb │ └── contacts.rb └── test/ ├── example_accounts.yml ├── test_helper.rb ├── test_suite.rb └── unit/ ├── aol_contact_importer_test.rb ├── gmail_contact_importer_test.rb ├── hotmail_contact_importer_test.rb ├── mailru_contact_importer_test.rb ├── test_accounts_test.rb └── yahoo_csv_contact_importer_test.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store pkg/* test/accounts.yml nbproject/ ================================================ FILE: .travis.yml ================================================ language: ruby rvm: - 1.9.3 - 1.9.2 - jruby-18mode - jruby-19mode - rbx-18mode - rbx-19mode - ruby-head - jruby-head - 1.8.7 - ree ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gem 'json', ">= 1.1.1" gem 'gdata_19', '1.1.5' gem 'rspec', :require => 'spec' gem 'rake' gem 'hpricot' ================================================ FILE: LICENSE ================================================ Copyright (c) 2006, Lucas Carlson, MOG All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 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. 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. 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. ================================================ FILE: README.md ================================================ Welcome to Contacts =================== Contacts is a universal interface to grab contact list information from various providers including Hotmail, AOL, Gmail, Plaxo and Yahoo. Download -------- * gem install contacts * http://github.com/cardmagic/contacts * git clone git://github.com/cardmagic/contacts.git Background ---------- 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. Usage -----
Contacts::Hotmail.new(login, password).contacts # => [["name", "foo@bar.com"], ["another name", "bow@wow.com"]]
Contacts::Yahoo.new(login, password).contacts
Contacts::Gmail.new(login, password).contacts
Contacts.new(:gmail, login, password).contacts
Contacts.new(:hotmail, login, password).contacts
Contacts.new(:yahoo, login, password).contacts
Contacts.guess(login, password).contacts
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.
Captcha error
-------------
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:
Contacts::Gmail.new(login, password, :captcha_token => params[:captcha_token], :captcha_response => params[:captcha_response]).contacts
Examples
--------
See the examples/ directory.
Authors
-------
> Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
Contributors
------------
* Britt Selvitelle from Twitter (mailto:anotherbritt@gmail.com) - http://twitter.com
* Tony Targonski from GigPark (mailto:tony@gigpark.com) - http://gigpark.com
* Waheed Barghouthi from Watwet (mailto:waheed.barghouthi@gmail.com) - http://watwet.com
* Glenn Sidney from Glenn Fu (mailto:glenn@glennfu.com) - http://glennfu.com
* Brian McQuay from Onomojo (mailto:brian@onomojo.com) - http://onomojo.com
* Adam Hunter (mailto:adamhunter@me.com) - http://adamhunter.me/
* Glenn Ford (mailto:glenn@glennfu.com) - http://www.glennfu.com/
* Leonardo Wong (mailto:mac@boy.name)
* Rusty Burchfield
* justintv
This library is released under the terms of the BSD.
================================================
FILE: Rakefile
================================================
require 'rubygems'
require 'bundler/setup'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
require './lib/contacts'
PKG_VERSION = Contacts::VERSION
PKG_FILES = FileList[
"lib/**/*", "bin/*", "test/**/*", "[A-Z]*", "Rakefile", "doc/**/*", "examples/**/*"
] - ["test/accounts.yml"]
desc "Default Task"
task :default => [ :test ]
# Run the unit tests
desc "Run all unit tests"
Rake::TestTask.new("test") { |t|
t.libs << "lib"
t.pattern = 'test/*/*_test.rb'
t.verbose = true
}
# Make a console, useful when working on tests
desc "Generate a test console"
task :console do
verbose( false ) { sh "irb -I lib/ -r 'contacts'" }
end
# Genereate the RDoc documentation
desc "Create documentation"
Rake::RDocTask.new("doc") { |rdoc|
rdoc.title = "Contact List - ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail"
rdoc.rdoc_dir = 'doc'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
}
# Genereate the package
spec = Gem::Specification.new do |s|
#### Basic information.
s.name = 'adamhunter-contacts'
s.version = PKG_VERSION
s.summary = <<-EOF
Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
EOF
s.description = <<-EOF
Ridiculously easy contact list information from various providers including Yahoo, Gmail, and Hotmail
EOF
#### Which files are to be included in this gem? Everything! (Except CVS directories.)
s.files = PKG_FILES
#### Load-time details: library and application (you will need one or both).
s.require_path = 'lib'
s.autorequire = 'contacts'
s.add_dependency('json', '>= 0.4.1')
s.add_dependency('gdata', '= 1.1.1')
s.requirements << "A json parser, the gdata ruby gem"
#### Documentation and testing.
s.has_rdoc = true
#### Author and project details.
s.author = "Lucas Carlson"
s.email = "lucas@rufy.com"
s.homepage = "http://rubyforge.org/projects/contacts"
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_zip = true
pkg.need_tar = true
end
desc "Report code statistics (KLOCs, etc) from the application"
task :stats do
require 'code_statistics'
CodeStatistics.new(
["Library", "lib"],
["Units", "test"]
).to_s
end
================================================
FILE: contacts.gemspec
================================================
Gem::Specification.new do |s|
s.name = "contacts"
s.version = "1.2.4"
s.date = "2010-07-06"
s.summary = "A universal interface to grab contact list information from various providers including Yahoo, AOL, Gmail, Hotmail, and Plaxo."
s.email = "lucas@rufy.com"
s.homepage = "http://github.com/cardmagic/contacts"
s.description = "A universal interface to grab contact list information from various providers including Yahoo, AOL, Gmail, Hotmail, and Plaxo."
s.has_rdoc = false
s.authors = ["Lucas Carlson"]
s.files = ["LICENSE", "Rakefile", "README", "examples/grab_contacts.rb", "lib/contacts.rb", "lib/contacts/base.rb", "lib/contacts/json_picker.rb", "lib/contacts/gmail.rb", "lib/contacts/aol.rb", "lib/contacts/hotmail.rb", "lib/contacts/plaxo.rb", "lib/contacts/yahoo.rb"]
s.add_dependency("json", ">= 1.1.1")
s.add_dependency('gdata', '1.1.2')
end
================================================
FILE: cruise_config.rb
================================================
# Project-specific configuration for CruiseControl.rb
Project.configure do |project|
# Send email notifications about broken and fixed builds to email1@your.site, email2@your.site (default: send to nobody)
# if building this on your own CI box, please remove!
project.email_notifier.emails = ['opensource@pivotallabs.com']
# Set email 'from' field to john@doe.com:
# project.email_notifier.from = 'john@doe.com'
# Build the project by invoking rake task 'custom'
# project.rake_task = 'custom'
# Build the project by invoking shell script "build_my_app.sh". Keep in mind that when the script is invoked, current working directory is
# [cruise]/projects/your_project/work, so if you do not keep build_my_app.sh in version control, it should be '../build_my_app.sh' instead
# project.build_command = 'build_my_app.sh'
# Ping Subversion for new revisions every 5 minutes (default: 30 seconds)
# project.scheduler.polling_interval = 5.minutes
end
================================================
FILE: examples/grab_contacts.rb
================================================
require File.dirname(__FILE__)+"/../lib/contacts"
login = ARGV[0]
password = ARGV[1]
Contacts::Gmail.new(login, password).contacts
Contacts.new(:gmail, login, password).contacts
Contacts.new("gmail", login, password).contacts
Contacts.guess(login, password).contacts
================================================
FILE: geminstaller.yml
================================================
---
defaults:
install_options: --no-ri --no-rdoc
gems:
- name: json
version: >= 1.1.1
- name: gdata
version: >= 1.1.1
================================================
FILE: lib/contacts/aol.rb
================================================
class Contacts
require 'hpricot'
require 'csv'
class Aol < Base
URL = "http://www.aol.com/"
LOGIN_URL = "https://my.screenname.aol.com/_cqr/login/login.psp"
LOGIN_REFERER_URL = "http://webmail.aol.com/"
LOGIN_REFERER_PATH = "sitedomain=sns.webmail.aol.com&lang=en&locale=us&authLev=0&uitype=mini&loginId=&redirType=js&xchk=false"
AOL_NUM = "29970-343" # this seems to change each time they change the protocol
CONTACT_LIST_URL = "http://webmail.aol.com/#{AOL_NUM}/aim-2/en-us/Lite/ContactList.aspx?folder=Inbox&showUserFolders=False"
CONTACT_LIST_CSV_URL = "http://webmail.aol.com/#{AOL_NUM}/aim-2/en-us/Lite/ABExport.aspx?command=all"
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"
def real_connect
if login.strip =~ /^(.+)@aol\.com$/ # strip off the @aol.com for AOL logins
login = $1
end
postdata = {
"loginId" => login,
"password" => password,
"rememberMe" => "on",
"_sns_fg_color_" => "",
"_sns_err_color_" => "",
"_sns_link_color_" => "",
"_sns_width_" => "",
"_sns_height_" => "",
"offerId" => "mail-second-en-us",
"_sns_bg_color_" => "",
"sitedomain" => "sns.webmail.aol.com",
"regPromoCode" => "",
"mcState" => "initialized",
"uitype" => "std",
"siteId" => "",
"lang" => "en",
"locale" => "us",
"authLev" => "0",
"siteState" => "",
"isSiteStateEncoded" => "false",
"use_aam" => "0",
"seamless" => "novl",
"aolsubmit" => CGI.escape("Sign In"),
"idType" => "SN",
"usrd" => "",
"doSSL" => "",
"redirType" => "",
"xchk" => "false"
}
# Get this cookie and stick it in the form to confirm to Aol that your cookies work
data, resp, cookies, forward = get(URL)
postdata["stips"] = cookie_hash_from_string(cookies)["stips"]
postdata["tst"] = cookie_hash_from_string(cookies)["tst"]
data, resp, cookies, forward, old_url = get(LOGIN_REFERER_URL, cookies) + [URL]
until forward.nil?
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
end
data, resp, cookies, forward, old_url = get("#{LOGIN_URL}?#{LOGIN_REFERER_PATH}", cookies) + [LOGIN_REFERER_URL]
until forward.nil?
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
end
doc = Hpricot(data)
(doc/:input).each do |input|
postdata["usrd"] = input.attributes["value"] if input.attributes["name"] == "usrd"
end
# parse data for and add it to the postdata
postdata["SNS_SC"] = cookie_hash_from_string(cookies)["SNS_SC"]
postdata["SNS_LDC"] = cookie_hash_from_string(cookies)["SNS_LDC"]
postdata["LTState"] = cookie_hash_from_string(cookies)["LTState"]
# raise data.inspect
data, resp, cookies, forward, old_url = post(LOGIN_URL, h_to_query_string(postdata), cookies, LOGIN_REFERER_URL) + [LOGIN_REFERER_URL]
until forward.nil?
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
end
if data.index("Invalid Username or Password. Please try again.")
raise AuthenticationError, "Username and password do not match"
elsif data.index("Required field must not be blank")
raise AuthenticationError, "Login and password must not be blank"
elsif data.index("errormsg_0_logincaptcha")
raise AuthenticationError, "Captcha error"
elsif data.index("Invalid request")
raise ConnectionError, PROTOCOL_ERROR
elsif cookies == ""
raise ConnectionError, PROTOCOL_ERROR
end
@cookies = cookies
end
def contacts
postdata = {
"file" => 'contacts',
"fileType" => 'csv'
}
return @contacts if @contacts
if connected?
data, resp, cookies, forward, old_url = get(CONTACT_LIST_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
until forward.nil?
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
end
if resp.code_type != Net::HTTPOK
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
end
# parse data and grab
doc = Hpricot(data)
(doc/:input).each do |input|
postdata["user"] = input.attributes["value"] if input.attributes["name"] == "user"
end
data, resp, cookies, forward, old_url = get(CONTACT_LIST_CSV_URL, @cookies, CONTACT_LIST_URL) + [CONTACT_LIST_URL]
until forward.nil?
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
end
if data.include?("error.gif")
raise AuthenticationError, "Account invalid"
end
parse data
end
end
private
def parse(data, options={})
data = CSV::Reader.parse(data)
col_names = data.shift
@contacts = data.map do |person|
["#{person[0]} #{person[1]}", person[4]] if person[4] && !person[4].empty?
end.compact
end
def h_to_query_string(hash)
u = ERB::Util.method(:u)
hash.map { |k, v|
u.call(k) + "=" + u.call(v)
}.join("&")
end
end
TYPES[:aol] = Aol
end
================================================
FILE: lib/contacts/base.rb
================================================
require "cgi"
require "net/http"
require "net/https"
require "uri"
require "zlib"
require "stringio"
require "thread"
require "erb"
class Contacts
TYPES = {}
VERSION = "1.2.4"
class Base
def initialize(login, password, options={})
@login = login
@password = password
@captcha_token = options[:captcha_token]
@captcha_response = options[:captcha_response]
@connections = {}
connect
end
def connect
raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @login.empty? || @password.nil? || @password.empty?
real_connect
end
def connected?
@cookies && !@cookies.empty?
end
def contacts(options = {})
return @contacts if @contacts
if connected?
url = URI.parse(contact_list_url)
http = open_http(url)
resp, data = http.get("#{url.path}?#{url.query}",
"Cookie" => @cookies
)
if resp.code_type != Net::HTTPOK
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
end
parse(data, options)
end
end
def login
@attempt ||= 0
@attempt += 1
if @attempt == 1
@login
else
if @login.include?("@#{domain}")
@login.sub("@#{domain}","")
else
"#{@login}@#{domain}"
end
end
end
def password
@password
end
def skip_gzip?
false
end
private
def domain
@d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
end
def contact_list_url
self.class.const_get(:CONTACT_LIST_URL)
end
def address_book_url
self.class.const_get(:ADDRESS_BOOK_URL)
end
def open_http(url)
c = @connections[Thread.current.object_id] ||= {}
http = c["#{url.host}:#{url.port}"]
unless http
http = Net::HTTP.new(url.host, url.port)
if url.port == 443
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
c["#{url.host}:#{url.port}"] = http
end
http.start unless http.started?
http
end
def cookie_hash_from_string(cookie_string)
cookie_string.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
end
def parse_cookies(data, existing="")
return existing if data.nil?
cookies = cookie_hash_from_string(existing)
data.gsub!(/ ?[\w]+=EXPIRED;/,'')
data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
data.gsub!(/(;\s*){2,}/,', ')
data.gsub!(/(,\s*){2,}/,', ')
data.sub!(/^,\s*/,'')
data.sub!(/\s*,$/,'')
data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
k, v = data.split("=", 2).map{|j|j.strip}
if cookies[k] && v.empty?
cookies.delete(k)
elsif v && !v.empty?
cookies[k] = v
end
end
cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
end
def remove_cookie(cookie, cookies)
parse_cookies("#{cookie}=", cookies)
end
def post(url, postdata, cookies="", referer="")
url = URI.parse(url)
http = open_http(url)
http_header = { "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
"Accept-Encoding" => "gzip",
"Cookie" => cookies,
"Referer" => referer,
"Content-Type" => 'application/x-www-form-urlencoded'
}
http_header.reject!{|k, v| k == 'Accept-Encoding'} if skip_gzip?
resp, data = http.post(url.path, postdata, http_header)
data = uncompress(resp, data)
cookies = parse_cookies(resp.response['set-cookie'], cookies)
forward = resp.response['Location']
forward ||= (data =~ /401
401
200