Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Carlson committed Mar 21, 2008
0 parents commit a4a6489
Show file tree
Hide file tree
Showing 10 changed files with 768 additions and 0 deletions.
10 changes: 10 additions & 0 deletions LICENSE
@@ -0,0 +1,10 @@
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.

43 changes: 43 additions & 0 deletions README
@@ -0,0 +1,43 @@
== Welcome to Contacts

Contacts is a universal interface to grab contact list information from various providers including Hotmail, Gmail and Yahoo.

== Download

* gem install contacts
* http://rubyforge.org/projects/contacts
* svn co svn://rubyforge.org/var/svn/contacts

== 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.

== 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

This library is released under the terms of the BSD.

90 changes: 90 additions & 0 deletions Rakefile
@@ -0,0 +1,90 @@
require 'rubygems'
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/**/*"
]

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 = '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.requirements << "A json parser"

#### 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
12 changes: 12 additions & 0 deletions examples/grab_contacts.rb
@@ -0,0 +1,12 @@
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
8 changes: 8 additions & 0 deletions lib/contacts.rb
@@ -0,0 +1,8 @@
$:.unshift(File.dirname(__FILE__)+"/contacts/")

require 'rubygems'
require 'base'
require 'gmail'
require 'hotmail'
require 'yahoo'
require 'plaxo'
204 changes: 204 additions & 0 deletions lib/contacts/base.rb
@@ -0,0 +1,204 @@
require "cgi"
require "net/http"
require "net/https"
require "uri"
require "zlib"
require "stringio"
require "thread"

class Contacts
TYPES = {}
VERSION = "1.0.11"

class Base
def initialize(login, password)
@login = login
@password = password
@connections = {}
connect
end

def connect
raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @password.nil?
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

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 parse_cookies(data, existing="")
return existing if data.nil?

cookies = existing.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}

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)
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",
"Accept-Encoding" => "gzip",
"Cookie" => cookies,
"Referer" => referer,
"Content-Type" => 'application/x-www-form-urlencoded'
)
data = uncompress(resp, data)
cookies = parse_cookies(resp.response['set-cookie'], cookies)
forward = resp.response['Location']
forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
return data, resp, cookies, forward
end

def get(url, cookies="", referer="")
url = URI.parse(url)
http = open_http(url)
resp, data = http.get("#{url.path}?#{url.query}",
"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
)
data = uncompress(resp, data)
cookies = parse_cookies(resp.response['set-cookie'], cookies)
forward = resp.response['Location']
return data, resp, cookies, forward
end

def uncompress(resp, data)
case resp.response['content-encoding']
when 'gzip':
gz = Zlib::GzipReader.new(StringIO.new(data))
data = gz.read
gz.close
resp.response['content-encoding'] = nil
# FIXME: Not sure what Hotmail was feeding me with their 'deflate',
# but the headers definitely were not right
when 'deflate':
data = Zlib::Inflate.inflate(data)
resp.response['content-encoding'] = nil
end

data
end
end

class ContactsError < StandardError
end

class AuthenticationError < ContactsError
end

class ConnectionError < ContactsError
end

class TypeNotFound < ContactsError
end

def self.new(type, login, password)
if TYPES.include?(type.to_s.intern)
TYPES[type.to_s.intern].new(login, password)
else
raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect}"
end
end

def self.guess(login, password)
TYPES.inject([]) do |a, t|
begin
a + t[1].new(login, password).contacts
rescue AuthenticationError
a
end
end.uniq
end
end

0 comments on commit a4a6489

Please sign in to comment.