omnicontacts 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ module OmniContacts
2
+ module Importer
3
+
4
+ autoload :Gmail, "omnicontacts/importer/gmail"
5
+ autoload :Yahoo, "omnicontacts/importer/yahoo"
6
+ autoload :Hotmail, "omnicontacts/importer/hotmail"
7
+
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ require "omnicontacts/middleware/oauth2"
2
+ require "rexml/document"
3
+
4
+ module OmniContacts
5
+ module Importer
6
+ class Gmail < Middleware::OAuth2
7
+
8
+ attr_reader :auth_host, :authorize_path, :auth_token_path, :scope
9
+
10
+ def initialize *args
11
+ super *args
12
+ @auth_host = "accounts.google.com"
13
+ @authorize_path = "/o/oauth2/auth"
14
+ @auth_token_path = "/o/oauth2/token"
15
+ @scope = "https://www.google.com/m8/feeds"
16
+ @contacts_host = "www.google.com"
17
+ @contacts_path = "/m8/feeds/contacts/default/full"
18
+ end
19
+
20
+ def fetch_contacts_using_access_token access_token, token_type
21
+ contacts_response = https_get(@contacts_host, @contacts_path, contacts_req_params, contacts_req_headers(access_token, token_type))
22
+ parse_contacts contacts_response
23
+ end
24
+
25
+ private
26
+
27
+ def contacts_req_params
28
+ { "max-results" => "100"}
29
+ end
30
+
31
+ def contacts_req_headers token, token_type
32
+ {"GData-Version" => "3.0", "Authorization" => "#{token_type} #{token}"}
33
+ end
34
+
35
+ def parse_contacts contacts_as_xml
36
+ xml = REXML::Document.new(contacts_as_xml)
37
+ contacts = []
38
+ xml.elements.each('//entry') do |entry|
39
+ gd_email = entry.elements['gd:email']
40
+ if gd_email
41
+ contact = {:email => gd_email.attributes['address']}
42
+ gd_name = entry.elements['gd:name']
43
+ if gd_name
44
+ contact[:name] = gd_name.elements['gd:fullName'].text
45
+ end
46
+ contacts << contact
47
+ end
48
+ end
49
+ contacts
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,46 @@
1
+ require "omnicontacts/middleware/oauth2"
2
+ require "json"
3
+
4
+ module OmniContacts
5
+ module Importer
6
+ class Hotmail < Middleware::OAuth2
7
+
8
+ attr_reader :auth_host, :authorize_path, :auth_token_path, :scope
9
+
10
+ def initialize *args
11
+ super *args
12
+ @auth_host = "oauth.live.com"
13
+ @authorize_path = "/authorize"
14
+ @scope = "wl.basic"
15
+ @auth_token_path = "/token"
16
+ @contacts_host = "apis.live.net"
17
+ @contacts_path = "/v5.0/me/contacts"
18
+ end
19
+
20
+ def fetch_contacts_using_access_token access_token, access_token_secret
21
+ contacts_response = https_get(@contacts_host, @contacts_path, :access_token =>access_token)
22
+ contacts_from_response contacts_response
23
+ end
24
+
25
+ private
26
+
27
+ def contacts_from_response contacts_as_json
28
+ json = JSON.parse(escape_windows_format(contacts_as_json))
29
+ result = []
30
+ json["data"].each do |contact|
31
+ result << {:email => contact["name"]} if valid_email? contact["name"]
32
+ end
33
+ result
34
+ end
35
+
36
+ def escape_windows_format value
37
+ value.gsub(/[\r\s]/,'')
38
+ end
39
+
40
+ def valid_email? value
41
+ /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/.match(value)
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,64 @@
1
+ require "omnicontacts/middleware/oauth1"
2
+ require "json"
3
+
4
+ module OmniContacts
5
+ module Importer
6
+ class Yahoo < Middleware::OAuth1
7
+
8
+ attr_reader :auth_host, :auth_token_path, :auth_path, :access_token_path
9
+
10
+ def initialize *args
11
+ super *args
12
+ @auth_host = "api.login.yahoo.com"
13
+ @auth_token_path = "/oauth/v2/get_request_token"
14
+ @auth_path = "/oauth/v2/request_auth"
15
+ @access_token_path = "/oauth/v2/get_token"
16
+ @contacts_host = "social.yahooapis.com"
17
+ end
18
+
19
+ def fetch_contacts_from_token_and_verifier auth_token, auth_token_secret, auth_verifier
20
+ (access_token, access_token_secret, guid) = fetch_access_token(auth_token, auth_token_secret, auth_verifier, ["xoauth_yahoo_guid"])
21
+ contacts_path = "/v1/user/#{guid}/contacts"
22
+ contacts_response = http_get(@contacts_host, contacts_path, contacts_req_params(access_token, access_token_secret, contacts_path) )
23
+ contacts_from_response contacts_response
24
+ end
25
+
26
+ private
27
+
28
+ def contacts_req_params access_token, access_token_secret, contacts_path
29
+ params = {
30
+ :format => "json",
31
+ :oauth_consumer_key => consumer_key,
32
+ :oauth_nonce => encode(random_string),
33
+ :oauth_signature_method => "HMAC-SHA1",
34
+ :oauth_timestamp => timestamp,
35
+ :oauth_token => access_token,
36
+ :oauth_version => OmniContacts::Authorization::OAuth1::OAUTH_VERSION,
37
+ :view => "compact"
38
+ }
39
+ contacts_url = "http://#{@contacts_host}#{contacts_path}"
40
+ params["oauth_signature"] = oauth_signature("GET", contacts_url, params, access_token_secret)
41
+ params
42
+ end
43
+
44
+ def contacts_from_response contacts_as_json
45
+ json = JSON.parse(contacts_as_json)
46
+ result = []
47
+ json["contacts"]["contact"].each do |entry|
48
+ contact = {}
49
+ entry["fields"].each do |field|
50
+ contact[:email] = field["value"] if field["type"] == "email"
51
+ if field["type"] == "name"
52
+ name = field["value"]["givenName"]
53
+ surname = field["value"]["familyName"]
54
+ contact[:name] = "#{name} #{surname}" if name && surname
55
+ end
56
+ end
57
+ result << contact if contact[:email]
58
+ end
59
+ result
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,97 @@
1
+ # This class contains the common behavior for middlewares
2
+ # implementing either versions of OAuth.
3
+ #
4
+ # Extending classes are required to implement
5
+ # the following methods:
6
+ # * request_authorization_from_user
7
+ # * fetch_contatcs
8
+ module OmniContacts
9
+ module Middleware
10
+ class BaseOAuth
11
+
12
+ attr_reader :ssl_ca_file
13
+
14
+ def initialize app, options
15
+ @app = app
16
+ @listening_path = "/contacts/" + class_name
17
+ @ssl_ca_file = options[:ssl_ca_file]
18
+ end
19
+
20
+ private
21
+
22
+ def class_name
23
+ self.class.name.split('::').last.downcase
24
+ end
25
+
26
+ public
27
+
28
+ # Rack callback. It handles three cases:
29
+ # * user visit middleware entru point.
30
+ # In this case request_authorization_from_user is called
31
+ # * user is redirected back to the application
32
+ # from the authorization site. In this case the list
33
+ # of contacts is fetched and stored in the variables
34
+ # omnicontacts.contacts within the Rack env variable.
35
+ # Once that is done the next middleware component is called.
36
+ # * user visits any other resource. In this case the request
37
+ # is simply forwarded to the next middleware component.
38
+ def call env
39
+ @env = env
40
+ if env["PATH_INFO"] =~ /^#{@listening_path}\/?$/
41
+ handle_initial_request
42
+ elsif env["PATH_INFO"] =~ /^#{redirect_path}/
43
+ handle_callback
44
+ else
45
+ @app.call(env)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def handle_initial_request
52
+ execute_and_rescue_exceptions do
53
+ request_authorization_from_user
54
+ end
55
+ end
56
+
57
+ def handle_callback
58
+ execute_and_rescue_exceptions do
59
+ @env["omnicontacts.contacts"] = fetch_contacts
60
+ @app.call(@env)
61
+ end
62
+ end
63
+
64
+ # This method rescues executes a block of code and
65
+ # rescue all exceptions. In case of an exception the
66
+ # user is redirected to the failure endpoint.
67
+ def execute_and_rescue_exceptions
68
+ yield
69
+ rescue AuthorizationError => e
70
+ handle_error :not_authorized, e
71
+ rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
72
+ handle_error :timeout, e
73
+ rescue ::RuntimeError => e
74
+ handle_error :internal_error, e
75
+ end
76
+
77
+ def handle_error error_type, exception
78
+ logger << ("Error #{error_type} while processing #{@env["PATH_INFO"]}: #{exception.message}") if logger
79
+ [302, {"location" => "/contacts/failure?error_message=#{error_type}"}, []]
80
+ end
81
+
82
+ def session
83
+ raise "You must provide a session to use OmniContacts" unless @env["rack.session"]
84
+ @env["rack.session"]
85
+ end
86
+
87
+ def logger
88
+ @env["rack.errors"] if @env
89
+ end
90
+
91
+ def base_prop_name
92
+ "omnicontacts." + class_name
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,68 @@
1
+ require "omnicontacts/authorization/oauth1"
2
+ require "omnicontacts/middleware/base_oauth"
3
+
4
+ # This class is an OAuth 1.0 Rack middleware.
5
+ #
6
+ # Extending classes are required to
7
+ # implement the following methods:
8
+ # * fetch_token_from_token_and_verifier -> this method has to
9
+ # fetch the list of contacts from the authorization server.
10
+ module OmniContacts
11
+ module Middleware
12
+ class OAuth1 < BaseOAuth
13
+ include Authorization::OAuth1
14
+
15
+ attr_reader :consumer_key, :consumer_secret, :callback_path
16
+
17
+ def initialize app, consumer_key, consumer_secret, options = {}
18
+ super app, options
19
+ @consumer_key = consumer_key
20
+ @consumer_secret = consumer_secret
21
+ @callback_path = options[:callback_path] ||= "/contacts/#{class_name}/callback"
22
+ @token_prop_name = "#{base_prop_name}.oauth_token"
23
+ end
24
+
25
+ def callback
26
+ host_url_from_rack_env(@env) + callback_path
27
+ end
28
+
29
+ alias :redirect_path :callback_path
30
+
31
+ # Obtains an authorization token from the server,
32
+ # stores it and the session and redirect the user
33
+ # to the authorization website.
34
+ def request_authorization_from_user
35
+ (auth_token, auth_token_secret) = fetch_authorization_token
36
+ session[@token_prop_name] = auth_token
37
+ session[token_secret_prop_name(auth_token)] = auth_token_secret
38
+ redirect_to_authorization_site(auth_token)
39
+ end
40
+
41
+ def token_secret_prop_name oauth_token
42
+ "#{base_prop_name}.#{oauth_token}.oauth_token_secret"
43
+ end
44
+
45
+ def redirect_to_authorization_site auth_token
46
+ [302, {"location" => authorization_url(auth_token)}, []]
47
+ end
48
+
49
+ # Parses the authorization token from the query string and
50
+ # obtain the relative secret from the session.
51
+ # Finally it calls fetch_contacts_from_token_and_verifier.
52
+ # If token is found in the query string an AuhorizationError
53
+ # is raised.
54
+ def fetch_contacts
55
+ params = query_string_to_map(@env["QUERY_STRING"])
56
+ oauth_token = params["oauth_token"]
57
+ oauth_verifier = params["oauth_verifier"]
58
+ oauth_token_secret = session[token_secret_prop_name(oauth_token)]
59
+ if oauth_token && oauth_verifier && oauth_token_secret
60
+ fetch_contacts_from_token_and_verifier(oauth_token, oauth_token_secret, oauth_verifier)
61
+ else
62
+ raise AuthorizationError.new("User did not grant access to contacts list")
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,66 @@
1
+ require "omnicontacts/authorization/oauth2"
2
+ require "omnicontacts/middleware/base_oauth"
3
+
4
+ # This class is a OAuth 2 Rack middleware.
5
+ #
6
+ # Extending class are required to implement
7
+ # the following methods:
8
+ # * fetch_contacts_using_access_token -> it
9
+ # fetches the list of contacts from the authorization
10
+ # server.
11
+ module OmniContacts
12
+ module Middleware
13
+ class OAuth2 < BaseOAuth
14
+ include Authorization::OAuth2
15
+
16
+ attr_reader :client_id, :client_secret, :redirect_path
17
+
18
+ def initialize app, client_id, client_secret, options ={}
19
+ super app, options
20
+ @client_id = client_id
21
+ @client_secret = client_secret
22
+ @redirect_path = options[:redirect_path] ||= "/contacts/#{class_name}/callback"
23
+ @ssl_ca_file = options[:ssl_ca_file]
24
+ end
25
+
26
+ def request_authorization_from_user
27
+ [302, {"location" => authorization_url}, []]
28
+ end
29
+
30
+ def redirect_uri
31
+ host_url_from_rack_env(@env) + redirect_path
32
+ end
33
+
34
+ # It extract the authorization code from the query string.
35
+ # It uses it to obtain an access token.
36
+ # If the authorization code has a refresh token associated
37
+ # with it in the session, it uses the obtain an access token.
38
+ # It fetches the list of contacts and stores the refresh token
39
+ # associated with the access token in the session.
40
+ # Finally it returns the list of contacts.
41
+ # If no authorization code is found in the query string an
42
+ # AuthoriazationError is raised.
43
+ def fetch_contacts
44
+ code = query_string_to_map(@env["QUERY_STRING"])["code"]
45
+ if code
46
+ refresh_token = session[refresh_token_prop_name(code)]
47
+ (access_token, token_type, refresh_token) = if refresh_token
48
+ refresh_access_token(refresh_token)
49
+ else
50
+ fetch_access_token(code)
51
+ end
52
+ contacts = fetch_contacts_using_access_token(access_token, token_type)
53
+ session[refresh_token_prop_name(code)] = refresh_token if refresh_token
54
+ contacts
55
+ else
56
+ raise AuthorizationError.new("User did not grant access to contacts list")
57
+ end
58
+ end
59
+
60
+ def refresh_token_prop_name code
61
+ "#{base_prop_name}.#{code}.refresh_token"
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../lib/omnicontacts', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'omnicontacts'
6
+ gem.description = %q{A generalized Rack middleware for importing contacts from major email providers.}
7
+ gem.authors = ['Diego Castorina']
8
+ gem.email = ['diegocastorina@gmail.com']
9
+
10
+ gem.add_runtime_dependency 'rack'
11
+ gem.add_runtime_dependency 'json'
12
+
13
+ gem.add_development_dependency 'simplecov'
14
+ gem.add_development_dependency 'rake'
15
+ gem.add_development_dependency 'rack-test'
16
+ gem.add_development_dependency 'rspec'
17
+
18
+ gem.version = OmniContacts::VERSION
19
+ gem.files = `git ls-files`.split("\n")
20
+ gem.homepage = 'http://github.com/Diego81/omnicontacts'
21
+ gem.require_paths = ['lib']
22
+ gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6') if gem.respond_to? :required_rubygems_version=
23
+ gem.summary = gem.description
24
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
25
+ end
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+ require "omnicontacts/authorization/oauth1"
3
+
4
+ describe OmniContacts::Authorization::OAuth1 do
5
+
6
+ before(:all) do
7
+ OAuth1TestClass= Struct.new(:consumer_key, :consumer_secret, :auth_host, :auth_token_path, :auth_path, :access_token_path, :callback)
8
+ class OAuth1TestClass
9
+ include OmniContacts::Authorization::OAuth1
10
+ end
11
+ end
12
+
13
+ let(:test_target) do
14
+ OAuth1TestClass.new("consumer_key", "secret1", "auth_host", "auth_token_path", "auth_path", "access_token_path", "callback")
15
+ end
16
+
17
+ describe "fetch_authorization_token" do
18
+
19
+ it "should request the token providing all mandatory parameters" do
20
+ test_target.should_receive(:https_post) do |host, path, params|
21
+ host.should eq(test_target.auth_host)
22
+ path.should eq(test_target.auth_token_path)
23
+ params[:oauth_consumer_key].should eq(test_target.consumer_key)
24
+ params[:oauth_nonce].should_not be_nil
25
+ params[:oauth_signature_method].should eq("PLAINTEXT")
26
+ params[:oauth_signature].should eq(test_target.consumer_secret + "%26")
27
+ params[:oauth_timestamp].should_not be_nil
28
+ params[:oauth_version].should eq("1.0")
29
+ params[:oauth_callback].should eq(test_target.callback)
30
+ "oauth_token=token&oauth_token_secret=token_secret"
31
+ end
32
+ test_target.fetch_authorization_token
33
+ end
34
+
35
+ it "should successfully parse the result" do
36
+ test_target.should_receive(:https_post).and_return("oauth_token=token&oauth_token_secret=token_secret")
37
+ test_target.fetch_authorization_token.should eq(["token", "token_secret"])
38
+ end
39
+
40
+ it "should raise an error if request is invalid" do
41
+ test_target.should_receive(:https_post).and_return("invalid_request")
42
+ expect{test_target.fetch_authorization_token}.should raise_error
43
+ end
44
+
45
+ end
46
+
47
+ describe "authorization_url" do
48
+ subject{test_target.authorization_url("token")}
49
+ it{should eq("https://#{test_target.auth_host}#{test_target.auth_path}?oauth_token=token")}
50
+ end
51
+
52
+ describe "fetch_access_token" do
53
+ it "should request the access token using all required parameters" do
54
+ auth_token = "token"
55
+ auth_token_secret = "token_secret"
56
+ auth_verifier = "verifier"
57
+ test_target.should_receive(:https_post) do |host, path, params|
58
+ host.should eq(test_target.auth_host)
59
+ path.should eq(test_target.access_token_path)
60
+ params[:oauth_consumer_key].should eq(test_target.consumer_key)
61
+ params[:oauth_nonce].should_not be_nil
62
+ params[:oauth_signature_method].should eq("PLAINTEXT")
63
+ params[:oauth_version].should eq("1.0")
64
+ params[:oauth_signature].should eq("#{test_target.consumer_secret}%26#{auth_token_secret}")
65
+ params[:oauth_token].should eq(auth_token)
66
+ params[:oauth_verifier].should eq(auth_verifier)
67
+ "oauth_token=access_token&oauth_token_secret=access_token_secret&other_param=other_value"
68
+ end
69
+ test_target.fetch_access_token auth_token, auth_token_secret, auth_verifier, ["other_param"]
70
+ end
71
+
72
+ it "should successfully extract access_token and the other fields" do
73
+ test_target.should_receive(:https_post).and_return("oauth_token=access_token&oauth_token_secret=access_token_secret&other_param=other_value")
74
+ test_target.fetch_access_token("token","token_scret","verified",["other_param"]).should eq(["access_token", "access_token_secret", "other_value"])
75
+ end
76
+ end
77
+
78
+ describe "oauth_signature" do
79
+ subject{ test_target.oauth_signature("GET", "http://social.yahooapis.com/v1/user", {:name => "diego", :surname => "castorina"}, "secret2")}
80
+ it{ should eq("ZqWoQISWcuz%2FSDnDxWihtsFDKwc%3D")}
81
+ end
82
+ end