omnicontacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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