Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: paperlesspost/contacts
...
head fork: cardmagic/contacts
compare: master
Checking mergeability… Don’t worry, you can still create the pull request.
  • 20 commits
  • 17 files changed
  • 1 commit comment
  • 7 contributors
View
3  .gitignore
@@ -1,3 +1,4 @@
.DS_Store
pkg/*
-test/accounts.yml
+test/accounts.yml
+nbproject/
View
13 .travis.yml
@@ -0,0 +1,13 @@
+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
+
View
6 Gemfile
@@ -0,0 +1,6 @@
+source 'https://rubygems.org'
+gem 'json', ">= 1.1.1"
+gem 'gdata_19', '1.1.5'
+gem 'rspec', :require => 'spec'
+gem 'rake'
+gem 'hpricot'
View
26 Gemfile.lock
@@ -0,0 +1,26 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ diff-lcs (1.2.4)
+ gdata_19 (1.1.5)
+ hpricot (0.8.6)
+ json (1.8.0)
+ rake (0.9.2.2)
+ rspec (2.14.1)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
+ rspec-core (2.14.5)
+ rspec-expectations (2.14.2)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.14.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ gdata_19 (= 1.1.5)
+ hpricot
+ json (>= 1.1.1)
+ rake
+ rspec
View
51 README → README.md
@@ -1,46 +1,57 @@
-== Welcome to Contacts
+Welcome to Contacts
+===================
Contacts is a universal interface to grab contact list information from various providers including Hotmail, AOL, Gmail, Plaxo and Yahoo.
-== Download
+Download
+--------
* gem install contacts
* http://github.com/cardmagic/contacts
* git clone git://github.com/cardmagic/contacts.git
-== Background
+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
+Usage
+-----
+
+<pre>
+ <code>
+ 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
+ </code>
+</pre>
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
+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:
-
+<pre><code>
Contacts::Gmail.new(login, password, :captcha_token => params[:captcha_token], :captcha_response => params[:captcha_response]).contacts
+</code></pre>
-== Examples
+Examples
+--------
See the examples/ directory.
-== Authors
+Authors
+-------
-* Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
+> Lucas Carlson from MOG (mailto:lucas@rufy.com) - http://mog.com
-== Contributors
+Contributors
+------------
* Britt Selvitelle from Twitter (mailto:anotherbritt@gmail.com) - http://twitter.com
* Tony Targonski from GigPark (mailto:tony@gigpark.com) - http://gigpark.com
View
5 Rakefile
@@ -1,10 +1,11 @@
require 'rubygems'
+require 'bundler/setup'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
-require 'lib/contacts'
+require './lib/contacts'
PKG_VERSION = Contacts::VERSION
@@ -88,4 +89,4 @@ task :stats do
["Library", "lib"],
["Units", "test"]
).to_s
-end
+end
View
6 contacts.gemspec
@@ -1,7 +1,7 @@
Gem::Specification.new do |s|
s.name = "contacts"
- s.version = "1.2.3"
- s.date = "2010-01-20"
+ 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"
@@ -10,5 +10,5 @@ Gem::Specification.new do |s|
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.1')
+ s.add_dependency('gdata', '1.1.2')
end
View
5 lib/contacts.rb
@@ -1,11 +1,14 @@
$:.unshift(File.dirname(__FILE__)+"/contacts/")
require 'rubygems'
+require 'bundler/setup'
require 'base'
+require 'json_picker'
require 'gmail'
require 'hotmail'
require 'yahoo'
require 'plaxo'
require 'aol'
-require 'json_picker'
+require 'mailru'
+
View
7 lib/contacts/aol.rb
@@ -13,7 +13,10 @@ class Aol < Base
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,
@@ -77,7 +80,7 @@ def real_connect
data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
end
- if data.index("Invalid Screen Name or Password.")
+ 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"
View
13 lib/contacts/base.rb
@@ -9,7 +9,7 @@
class Contacts
TYPES = {}
- VERSION = "1.2.3"
+ VERSION = "1.2.4"
class Base
def initialize(login, password, options={})
@@ -65,6 +65,10 @@ def login
def password
@password
end
+
+ def skip_gzip?
+ false
+ end
private
@@ -132,13 +136,14 @@ def remove_cookie(cookie, cookies)
def post(url, postdata, cookies="", referer="")
url = URI.parse(url)
http = open_http(url)
- resp, data = http.post(url.path, postdata,
- "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
+ 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']
View
7 lib/contacts/hotmail.rb
@@ -78,9 +78,9 @@ def contacts(options = {})
email_match_text_beginning = Regexp.escape("http://m.mail.live.com/?rru=compose&amp;to=")
email_match_text_end = Regexp.escape("&amp;")
- raw_html = resp.body.grep(/(?:e|dn)lk[0-9]+/)
- raw_html.delete_at 0
- raw_html.inject do |memo, row|
+ raw_html = resp.body.split("
+").grep(/(?:e|dn)lk[0-9]+/)
+ raw_html.inject(-1) do |memo, row|
c_info = row.match(/(e|dn)lk([0-9])+/)
# Same contact, or different?
@@ -109,6 +109,7 @@ def contacts(options = {})
@contacts << contact
end
end
+
return @contacts
end
end
View
68 lib/contacts/mailru.rb
@@ -0,0 +1,68 @@
+require 'csv'
+
+class Contacts
+ class Mailru < Base
+ LOGIN_URL = "https://auth.mail.ru/cgi-bin/auth"
+ ADDRESS_BOOK_URL = "http://win.mail.ru/cgi-bin/abexport/addressbook.csv"
+
+ attr_accessor :cookies
+
+ def real_connect
+ username = login
+
+ postdata = "Login=%s&Domain=%s&Password=%s" % [
+ CGI.escape(username),
+ CGI.escape(domain_param(username)),
+ CGI.escape(password)
+ ]
+
+ data, resp, self.cookies, forward = post(LOGIN_URL, postdata, "")
+
+ if data.index("fail=1")
+ raise AuthenticationError, "Username and password do not match"
+ elsif cookies == "" or data == ""
+ raise ConnectionError, PROTOCOL_ERROR
+ end
+
+ data, resp, cookies, forward = get(login_token_link(data), login_cookies.join(';'))
+ end
+
+ def contacts
+ postdata = "confirm=1&abtype=6"
+ data, resp, cookies, forward = post(ADDRESS_BOOK_URL, postdata, login_cookies.join(';'))
+
+ @contacts = []
+ CSV.parse(data) do |row|
+ @contacts << [row[0], row[4]] unless header_row?(row)
+ end
+
+ @contacts
+ end
+
+ def skip_gzip?
+ true
+ end
+
+ private
+ def login_token_link(data)
+ data.match(/url=(.+)\">/)[1]
+ end
+
+ def login_cookies
+ self.cookies.split(';').collect{|c| c if (c.include?('t=') or c.include?('Mpop='))}.compact.collect{|c| c.strip}
+ end
+
+ def header_row?(row)
+ row[0] == 'AB-Name'
+ end
+
+ def domain_param(login)
+ login.include?('@') ?
+ login.match(/.+@(.+)/)[1] :
+ 'mail.ru'
+ end
+
+ end
+
+ TYPES[:mailru] = Mailru
+end
View
52 lib/contacts/yahoo.rb
@@ -5,15 +5,15 @@ class Yahoo < Base
ADDRESS_BOOK_URL = "http://address.mail.yahoo.com/?.rand=430244936"
CONTACT_LIST_URL = "http://address.mail.yahoo.com/?_src=&_crumb=crumb&sortfield=3&bucket=1&scroll=1&VPC=social_list&.r=time"
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"
-
- def real_connect
+
+ def real_connect
postdata = ".tries=2&.src=ym&.md5=&.hash=&.js=&.last=&promo=&.intl=us&.bypass="
postdata += "&.partner=&.u=4eo6isd23l8r3&.v=0&.challenge=gsMsEcoZP7km3N3NeI4mX"
postdata += "kGB7zMV&.yplus=&.emailCode=&pkg=&stepid=&.ev=&hasMsgr=1&.chkP=Y&."
postdata += "done=#{CGI.escape(URL)}&login=#{CGI.escape(login)}&passwd=#{CGI.escape(password)}"
-
+
data, resp, cookies, forward = post(LOGIN_URL, postdata)
-
+
if data.index("Invalid ID or password") || data.index("This ID is not yet taken")
raise AuthenticationError, "Username and password do not match"
elsif data.index("Sign in") && data.index("to Yahoo!")
@@ -23,18 +23,20 @@ def real_connect
elsif cookies == ""
raise ConnectionError, PROTOCOL_ERROR
end
-
+
data, resp, cookies, forward = get(forward, cookies, LOGIN_URL)
-
+
if resp.code_type != Net::HTTPOK
raise ConnectionError, PROTOCOL_ERROR
end
-
+
@cookies = cookies
end
-
- def contacts
+
+ def contacts
return @contacts if @contacts
+ @contacts = []
+
if connected?
# first, get the addressbook site with the new crumb parameter
url = URI.parse(address_book_url)
@@ -61,16 +63,12 @@ def contacts
if resp.code_type != Net::HTTPOK
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
end
-
- parse data
-
- parse more_data
-
+
if more_data =~ /"TotalABContacts":(\d+)/
total = $1.to_i
- ((total / 50)).times do |i|
+ ((total / 50.0).ceil).times do |i|
# now proceed with the new ".crumb" parameter to get the csv data
- url = URI.parse(contact_list_url.sub("bucket=1","bucket=#{i+2}").sub("_crumb=crumb","_crumb=#{crumb}").sub("time", Time.now.to_f.to_s.sub(".","")[0...-2]))
+ 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]))
http = open_http(url)
resp, more_data = http.get("#{url.path}?#{url.query}",
"Cookie" => @cookies,
@@ -81,29 +79,27 @@ def contacts
if resp.code_type != Net::HTTPOK
raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
end
-
+
parse more_data
end
end
-
+
@contacts
end
end
private
-
+
def parse(data, options={})
@contacts ||= []
- if data =~ /var InitialContacts = (\[.*?\]);/
- @contacts += Contacts.parse_json($1).select{|contact|!contact["email"].to_s.empty?}.map{|contact|[contact["contactName"], contact["email"]]}
- elsif data =~ /^\{"response":/
- @contacts += Contacts.parse_json(data)["response"]["ResultSet"]["Contacts"].to_a.select{|contact|!contact["email"].to_s.empty?}.map{|contact|[contact["contactName"], contact["email"]]}
- else
- @contacts
- end
+ @contacts += Contacts.parse_json(data)["response"]["ResultSet"]["Contacts"].to_a.select{|contact|!contact["email"].to_s.empty?}.map do |contact|
+ name = contact["contactName"].split(",")
+ [[name.pop, name.join(",")].join(" ").strip, contact["email"]]
+ end if data =~ /^\{"response":/
+ @contacts
end
-
+
end
TYPES[:yahoo] = Yahoo
-end
+end
View
18 test/example_accounts.yml
@@ -1,7 +1,7 @@
gmail:
username: <changeme>
password: <changeme>
- contacts:
+ contacts:
-
name: "FirstName1 LastName1"
email_address: "firstname1@example.com"
@@ -11,7 +11,7 @@ gmail:
yahoo:
username: <changeme>
password: <changeme>
- contacts:
+ contacts:
-
name: "FirstName1 LastName1"
email_address: "firstname1@example.com"
@@ -21,7 +21,7 @@ yahoo:
hotmail:
username: <changeme>
password: <changeme>
- contacts:
+ contacts:
-
name: "FirstName1 LastName1"
email_address: "firstname1@example.com"
@@ -31,7 +31,17 @@ hotmail:
aol:
username: <changeme>
password: <changeme>
- contacts:
+ contacts:
+ -
+ name: "FirstName1 LastName1"
+ email_address: "firstname1@example.com"
+ -
+ name: "FirstName2 LastName2"
+ email_address: "firstname2@example.com"
+mailru:
+ username: <changeme>
+ password: <changeme>
+ contacts:
-
name: "FirstName1 LastName1"
email_address: "firstname1@example.com"
View
1  test/test_helper.rb
@@ -2,6 +2,7 @@
$LOAD_PATH.unshift(dir + "/../lib/")
require 'test/unit'
require 'contacts'
+require 'yaml'
class ContactImporterTestCase < Test::Unit::TestCase
# Add more helper methods to be used by all tests here...
View
39 test/unit/mailru_contact_importer_test.rb
@@ -0,0 +1,39 @@
+dir = File.dirname(__FILE__)
+require "#{dir}/../test_helper"
+require 'contacts'
+
+class MailruContactImporterTest < ContactImporterTestCase
+ def setup
+ super
+ @account = TestAccounts[:mailru]
+ end
+
+ def test_successful_login
+ Contacts.new(:mailru, @account.username, @account.password)
+ end
+
+ def test_importer_fails_with_invalid_password
+ assert_raise(Contacts::AuthenticationError) do
+ Contacts.new(:mailru, @account.username, "wrong_password")
+ end
+ end
+
+ def test_importer_fails_with_blank_password
+ assert_raise(Contacts::AuthenticationError) do
+ Contacts.new(:mailru, @account.username, "")
+ end
+ end
+
+ def test_importer_fails_with_blank_username
+ assert_raise(Contacts::AuthenticationError) do
+ Contacts.new(:mailru, "", @account.password)
+ end
+ end
+
+ def test_fetch_contacts
+ contacts = Contacts.new(:mailru, @account.username, @account.password).contacts
+ @account.contacts.each do |contact|
+ assert contacts.include?(contact), "Could not find: #{contact.inspect} in #{contacts.inspect}"
+ end
+ end
+end
View
3  test/unit/yahoo_csv_contact_importer_test.rb
@@ -18,6 +18,9 @@ def test_importer_fails_with_invalid_password
end
# run the "successful" login test to ensure we reset yahoo's failed login lockout counter
# See http://www.pivotaltracker.com/story/show/138210
+ # yahoo needs some time to unset the failed login state, apparently...
+ # ...1 sec and 5 secs still failed sporadically
+ sleep 10
assert_nothing_raised do
Contacts.new(:yahoo, @account.username, @account.password)
end

Showing you all comments on commits in this comparison.

@jensb

This fix only works for aol.com addresses. However, AOL also allows country domains (like @aol.de).

Something went wrong with that request. Please try again.