Repository: Diego81/omnicontacts Branch: master Commit: 105b40f79cac Files: 37 Total size: 131.9 KB Directory structure: gitextract_m1xfdr8k/ ├── .gitignore ├── Gemfile ├── README.md ├── Rakefile ├── lib/ │ ├── omnicontacts/ │ │ ├── authorization/ │ │ │ ├── oauth1.rb │ │ │ └── oauth2.rb │ │ ├── builder.rb │ │ ├── http_utils.rb │ │ ├── importer/ │ │ │ ├── facebook.rb │ │ │ ├── gmail.rb │ │ │ ├── hotmail.rb │ │ │ ├── linkedin.rb │ │ │ ├── outlook.rb │ │ │ └── yahoo.rb │ │ ├── importer.rb │ │ ├── integration_test.rb │ │ ├── middleware/ │ │ │ ├── base_oauth.rb │ │ │ ├── oauth1.rb │ │ │ └── oauth2.rb │ │ └── parse_utils.rb │ └── omnicontacts.rb ├── omnicontacts.gemspec └── spec/ ├── omnicontacts/ │ ├── authorization/ │ │ ├── oauth1_spec.rb │ │ └── oauth2_spec.rb │ ├── http_utils_spec.rb │ ├── importer/ │ │ ├── facebook_spec.rb │ │ ├── gmail_spec.rb │ │ ├── hotmail_spec.rb │ │ ├── linkedin_spec.rb │ │ ├── outlook_spec.rb │ │ └── yahoo_spec.rb │ ├── integration_test_spec.rb │ ├── middleware/ │ │ ├── base_oauth_spec.rb │ │ ├── oauth1_spec.rb │ │ └── oauth2_spec.rb │ └── parse_utils_spec.rb └── spec_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ coverage *.iml .idea .rvmrc .DS_Store nbproject ================================================ FILE: Gemfile ================================================ source 'http://rubygems.org' gemspec ================================================ FILE: README.md ================================================ # OmniContacts Inspired by the popular OmniAuth, OmniContacts is a library that enables users of an application to import contacts from their email or Facebook accounts. The email providers currently supported are Gmail, Yahoo and Hotmail. OmniContacts is a Rack middleware, therefore you can use it with Rails, Sinatra and any other Rack-based framework. OmniContacts uses the OAuth protocol to communicate with the contacts provider. Yahoo still uses OAuth 1.0, while Facebook, Gmail and Hotmail support OAuth 2.0. In order to use OmniContacts, it is therefore necessary to first register your application with the provider and to obtain client_id and client_secret. ## Contribute! Me (rubytastic) and the orginal author Diego don't actively use this code at the moment, anyone interested in maintaining and contributing to this codebase please write me up in a personal message ( rubytastic ) I try to merge pull requests in every once and a while but this code would benefit from someone actively use and contribute to it. ## Gem build updates There is now a new gem build out which should address many issues people had when posting on the issue tracker. Please update to the latest GEM version if you have problems before posting new issues. ## Usage Add OmniContacts as a dependency: ```ruby gem "omnicontacts" ``` As for OmniAuth, there is a Builder facilitating the usage of multiple contacts importers. In the case of a Rails application, the following code could be placed at `config/initializers/omnicontacts.rb`: ```ruby require "omnicontacts" Rails.application.middleware.use OmniContacts::Builder do importer :gmail, "client_id", "client_secret", {:redirect_path => "/oauth2callback", :ssl_ca_file => "/etc/ssl/certs/curl-ca-bundle.crt"} importer :yahoo, "consumer_id", "consumer_secret", {:callback_path => "/callback"} importer :linkedin, "consumer_id", "consumer_secret", {:redirect_path => "/oauth2callback", :state => ''} importer :hotmail, "client_id", "client_secret" importer :outlook, "app_id", "app_secret" importer :facebook, "client_id", "client_secret" end ``` Every importer expects `client_id` and `client_secret` as mandatory, while `:redirect_path` and `:ssl_ca_file` are optional (except linkedin - `state` arg mandatory). Since Yahoo implements the version 1.0 of the OAuth protocol, naming is slightly different. Instead of `:redirect_path` you should use `:callback_path` as key in the hash providing the optional parameters. While `:ssl_ca_file` is optional, it is highly recommended to set it on production environments for obvious security reasons. On the other hand it makes things much easier to leave the default value for `:redirect_path` and `:callback path`, the reason of which will be clear after reading the following section. ## Register your application * For Gmail : [Google API Console](https://code.google.com/apis/console/) * For Yahoo : [Yahoo Developer Network](https://developer.yahoo.com/social/contacts/) * For Hotmail : [Microsoft Developer Network](https://account.live.com/developers/applications/index) * For Outlook : [Microsoft Application Registration Portal](https://apps.dev.microsoft.com/) * For Facebook : [Facebook Developers](https://developers.facebook.com/apps) * For Linkedin : [Linkedin Developer Network](https://www.linkedin.com/secure/developer) ##### Note: Please go through [MSDN](http://msdn.microsoft.com/en-us/library/cc287659.aspx) if above Hotmail link will not work. Outlook is a newer Microsoft API which allows to retrieve real email address instead of `email_hashes` when using Hotmail, it also works with all kinds of MS accounts (Office 365, Hotmail.com, Live.com, MSN.com, Outlook.com, and Passport.com). ## Integrating with your Application To use the Gem you first need to redirect your users to `/contacts/:importer`, where `:importer` can be facebook, gmail, yahoo or hotmail. No changes to `config/routes.rb` are needed for this step since OmniContacts will be listening on that path and redirect the user to the email provider's website in order to authorize your app to access his contact list. Once that is done the user will be redirected back to your application, to the path specified in `:redirect_path` (or `:callback_path` for yahoo). If nothing is specified the default value is `/contacts/:importer/callback` (e.g. `/contacts/yahoo/callback`). This makes things simpler and you can just add the following line to `config/routes.rb`: ```ruby match "/contacts/:importer/callback" => "your_controller#callback" ``` The list of contacts can be accessed via the `omnicontacts.contacts` key in the environment hash and it consists of a simple array of hashes. The following table shows which fields are supported by which provider:
Provider :email :id :profile_picture :name :first_name :last_name :address_1 :address_2 :city :region :postcode :country :phone_number :birthday :gender :relation
Gmail X X X X X X X X X X X X X X X
Facebook X X X X X X X X
Yahoo X X X X X X X X X X X
Hotmail X X X X X X X X
Outlook X X X X X X X X X X X
Linkedin X X X X X
Obviously it may happen that some fields are blank even if supported by the provider in the case that the contact did not provide any information about them. The information for the logged in user can also be accessed via 'omnicontacts.user' key in the environment hash. It consists of a simple hash which includes the same fields as above. The following snippet shows how to simply print name and email of each contact, and also the the name of logged in user: ```ruby def contacts_callback @contacts = request.env['omnicontacts.contacts'] @user = request.env['omnicontacts.user'] puts "List of contacts of #{@user[:name]} obtained from #{params[:importer]}:" @contacts.each do |contact| puts "Contact found: name => #{contact[:name]}, email => #{contact[:email]}" end end ``` If the user does not authorize your application to access his/her contacts list, or any other inconvenience occurs, he/she is redirected to `/contacts/failure`. The query string will contain a parameter named `error_message` which specifies why the list of contacts could not be retrieved. `error_message` can have one of the following values: `not_authorized`, `timeout` and `internal_error`. ## Tips and tricks OmniContacts supports OAuth 1.0 and OAuth 2.0 token refresh, but for both it needs to persist data between requests. OmniContacts stores access tokens in the session. If you hit the 4KB cookie storage limit you better opt for the Memcache or the Active Record storage. Gmail requires you to register the redirect_path on their website along with your application. Make sure to use the same value present in the configuration file, or `/contacts/gmail/callback` if using the default. Also make sure that your full url is used including "www" if your site redirects from the root domain. To configure the max number of contacts to download from Gmail, just add a max results parameter in your initializer: ```ruby importer :gmail, "xxx", "yyy", :max_results => 1000 ``` Yahoo requires you to configure the Permissions your application requires. Make sure to go the Yahoo website and to select Read permission for Contacts. Hotmail presents a "peculiar" feature. Their API returns a Contact object which does not contain an e-mail field! However, if the contact has either name, family name or both set to null, than there is a field called name which does contain the e-mail address. This means that it may happen that an Hotmail contact does not contain the email field. ## Integration Testing You can enable test mode like this: ```ruby OmniContacts.integration_test.enabled = true ``` In this way all requests to `/omnicontacts/provider` will be redirected automatically to `/omnicontacts/provider/callback`. The `mock` method allows to configure per-provider the result to return: ```ruby OmniContacts.integration_test.mock(:provider_name, :email => "user@example.com") ``` You can either pass a single hash or an array of hashes. If you pass a string, an error will be triggered with subsequent redirect to `/contacts/failure?error_message=internal_error` You can also pass a user to fill `omnicontacts.user` (optional) ```ruby OmniContacts.integration_test.mock(:provider_name, {:email => "contact@example.com"}, {:email => "user@example.com"}) ``` Follows a full example of an integration test: ```ruby OmniContacts.integration_test.enabled = true OmniContacts.integration_test.mock(:gmail, :email => "user@example.com") visit '/contacts/gmail' page.should have_content("user@example.com") ``` ## Working on localhost Since Hotmail and Facebook do not allow the usage of `localhost` as redirect path for the authorization step, a workaround is to use `ngrok`. This is really useful when you need someone, the contacts provider in this case, to access your locally running application using a unique url. Install ngrok, download from: https://ngrok.com/ https://github.com/inconshreveable/ngrok Unzip the file ```bash unzip /place/this/is/ngrok.zip ``` Start your application ```bash $ rails server => Booting WEBrick => Rails 4.0.4 application starting in development on http://0.0.0.0:3000 ``` In a new terminal window, start the tunnel and pass the port where your application is running: ```bash ./ngrok 3000 ``` Check the output to see something like ```bash ngrok (Ctrl+C to quit) Tunnel Status online Version 1.6/1.5 Forwarding http://274101c1e.ngrok.com -> 127.0.0.1:3000 Forwarding https://274101c1e.ngrok.com -> 127.0.0.1:3000 Web Interface 127.0.0.1:4040 # Conn 0 Avg Conn Time 0.00ms ``` This window will show all network transaction that your locally hosted application is processing. Ngrok will process all of the requests and responses on your localhost. Visit: ```bash http://123456789.ngrok.com # replace 123456789 with your instance ``` ## Example application Thanks to @sonianand11, you can find a full example of a Rails application using OmniContacts at: https://github.com/sonianand11/omnicontacts_example ## Thanks As already mentioned above, a special thanks goes to @sonianand11 for implementing an example app. Thanks also to @asmatameem for her huge contribution. She indeed added support for Facebook and for many fields which were missing before. ## License Copyright (c) 2012-2013 Diego81 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Rakefile ================================================ require 'bundler' require 'rspec/core/rake_task' Bundler::GemHelper.install_tasks RSpec::Core::RakeTask.new(:spec) task :default => :spec task :test => :spec ================================================ FILE: lib/omnicontacts/authorization/oauth1.rb ================================================ require "omnicontacts/http_utils" require "base64" # This module represent a OAuth 1.0 Client. # # Classes including the module must implement # the following methods: # * auth_host -> the host of the authorization server # * auth_token_path -> the path to query to obtain a request token # * consumer_key -> the registered consumer key of the client # * consumer_secret -> the registered consumer secret of the client # * callback -> the callback to include during the redirection step # * auth_path -> the path on the authorization server to redirect the user to # * access_token_path -> the path to query in order to obtain the access token module OmniContacts module Authorization module OAuth1 include HTTPUtils OAUTH_VERSION = "1.0" # Obtain an authorization token from the server. # The token is returned in an array along with the relative authorization token secret. def fetch_authorization_token request_token_response = https_post(auth_host, auth_token_path, request_token_req_params) values_from_query_string(request_token_response, ["oauth_token", "oauth_token_secret"]) end private def request_token_req_params { :oauth_consumer_key => consumer_key, :oauth_nonce => encode(random_string), :oauth_signature_method => "PLAINTEXT", :oauth_signature => encode(consumer_secret + "&"), :oauth_timestamp => timestamp, :oauth_version => OAUTH_VERSION, :oauth_callback => callback } end def random_string (0...50).map { ('a'..'z').to_a[rand(26)] }.join end def timestamp Time.now.to_i.to_s end def values_from_query_string query_string, keys_to_extract map = query_string_to_map(query_string) keys_to_extract.collect do |key| if map.has_key?(key) map[key] else raise "No value found for #{key} in #{query_string}" end end end public # Returns the url the user has to be redirected to do in order grant permission to the client application. def authorization_url auth_token "https://" + auth_host + auth_path + "?oauth_token=" + auth_token end # Fetches the access token from the authorization server. # The method expects the authorization token, the authorization token secret and the authorization verifier. # The result comprises the access token, the access token secret and a list of additional fields extracted from the server's response. # The list of additional fields to extract is specified as last parameter def fetch_access_token auth_token, auth_token_secret, auth_verifier, additional_fields_to_extract = [] access_token_resp = https_post(auth_host, access_token_path, access_token_req_params(auth_token, auth_token_secret, auth_verifier)) values_from_query_string(access_token_resp, (["oauth_token", "oauth_token_secret"] + additional_fields_to_extract)) end private def access_token_req_params auth_token, auth_token_secret, auth_verifier { :oauth_consumer_key => consumer_key, :oauth_nonce => encode(random_string), :oauth_signature_method => "PLAINTEXT", :oauth_signature => encode(consumer_secret + "&" + auth_token_secret), :oauth_version => OAUTH_VERSION, :oauth_timestamp => timestamp, :oauth_token => auth_token, :oauth_verifier => auth_verifier } end public # Calculates a signature using HMAC-SHA1 according to the OAuth 1.0 specifications. # # The base string is given is a RFC 3986 encoded concatenation of: # * Uppercase HTTP method # * An '&' # * A url without any parameters # * An '&' # * All parameters to use in the request encoded themselves and sorted by key. # # The signature key is given by the concatenation of: # * RFC 3986 encoded consumer secret # * An '&' # * RFC 3986 encoded token secret def oauth_signature method, url, params, secret encoded_method = encode(method.upcase) encoded_url = encode(url) # params must be in alphabetical order encoded_params = encode(to_query_string(params.sort { |x, y| x.to_s <=> y.to_s })) base_string = encoded_method + '&' + encoded_url + '&' + encoded_params key = encode(consumer_secret) + '&' + secret hmac_sha1 = OpenSSL::HMAC.digest('sha1', key, base_string) # base64 encode results must be stripped encode(Base64.encode64(hmac_sha1).strip) end end end end ================================================ FILE: lib/omnicontacts/authorization/oauth2.rb ================================================ require "omnicontacts/http_utils" require "json" # This module represents an OAuth 2.0 client. # # Classes including the module must implement # the following methods: # * auth_host -> the host of the authorization server # * authorize_path -> the path on the authorization server the redirect the use to # * client_id -> the registered client id of the client # * client_secret -> the registered client secret of the client # * redirect_path -> the path the authorization server has to redirect the user back after authorization # * auth_token_path -> the path to query once the user has granted permission to the application # * scope -> the scope necessary to acquire the contacts list. module OmniContacts module Authorization module OAuth2 include HTTPUtils # Calculates the URL the user has to be redirected to in order to authorize # the application to access his contacts list. def authorization_url "https://" + auth_host + authorize_path + "?" + authorize_url_params end private def authorize_url_params to_query_string({ :client_id => client_id, :scope => encode(scope), :response_type => "code", :access_type => "online", :approval_prompt => "auto", :redirect_uri => encode(redirect_uri) }) end public # Fetches the access token from the authorization server using the given authorization code. def fetch_access_token code access_token_from_response https_post(auth_host, auth_token_path, token_req_params(code)) end private def token_req_params code { :client_id => client_id, :client_secret => client_secret, :code => code, :redirect_uri => encode(redirect_uri), :grant_type => "authorization_code" } end def access_token_from_response response if auth_host == "graph.facebook.com" response = query_string_to_map(response).to_json end json = JSON.parse(response) raise json["error"] if json["error"] [json["access_token"], json["token_type"], json["refresh_token"]] end public # Refreshes the access token using the provided refresh_token. def refresh_access_token refresh_token access_token_from_response https_post(auth_host, auth_token_path, refresh_token_req_params(refresh_token)) end private def refresh_token_req_params refresh_token { :client_id => client_id, :client_secret => client_secret, :refresh_token => refresh_token, :grant_type => "refresh_token" } end end end end ================================================ FILE: lib/omnicontacts/builder.rb ================================================ require "omnicontacts" module OmniContacts class Builder < Rack::Builder def initialize(app, &block) if rack14? super else @app = app super(&block) end end def rack14? v = Rack.release.split('.') v[0].to_i >= 1 || v[1].to_i >= 4 end def importer importer, *args middleware = OmniContacts::Importer.const_get(importer.to_s.capitalize) use middleware, *args rescue NameError raise LoadError, "Could not find importer #{importer}." end def call env @ins << @app unless rack14? || @ins.include?(@app) to_app.call(env) end end end ================================================ FILE: lib/omnicontacts/http_utils.rb ================================================ require "net/http" require "net/https" require "cgi" require "openssl" # This module contains a set of utility methods related to the HTTP protocol. module OmniContacts module HTTPUtils SSL_PORT = 443 module_function def query_string_to_map query_string query_string.split('&').reduce({}) do |memo, key_value| (key, value) = key_value.split('=') memo[key]= value memo end end def to_query_string map map.collect do |key, value| key.to_s + "=" + value.to_s end.join("&") end # Encodes the given input according to RFC 3986 def encode to_encode CGI.escape(to_encode) end # Calculates the url of the host from a Rack environment. # The result is in the form scheme://host:port # If port is 80 the result is scheme://host # According to Rack specification the HTTP_HOST variable is preferred over SERVER_NAME. def host_url_from_rack_env env port = ((env["SERVER_PORT"] == 80) && "") || ":#{env['SERVER_PORT']}" host = (env["HTTP_HOST"]) || (env["SERVER_NAME"] + port) "#{scheme(env)}://#{host}" end def scheme env if env['HTTPS'] == 'on' 'https' elsif env['HTTP_X_FORWARDED_SSL'] == 'on' 'https' elsif env['HTTP_X_FORWARDED_PROTO'] env['HTTP_X_FORWARDED_PROTO'].split(',').first else env["rack.url_scheme"] end end # Classes including the module must respond to the ssl_ca_file message in order to use the following methods. # The response will be the path to the CA file to use when making https requests. # If the result of ssl_ca_file is nil no file is used. In this case a warn message is logged. private # Executes an HTTP GET request. # It raises a RuntimeError if the response code is not equal to 200 def http_get host, path, params connection = Net::HTTP.new(host) process_http_response connection.request_get(path + "?" + to_query_string(params)) end # Executes an HTTP POST request over SSL # It raises a RuntimeError if the response code is not equal to 200 def https_post host, path, params https_connection host do |connection| connection.request_post(path, to_query_string(params)) end end # Executes an HTTP GET request over SSL # It raises a RuntimeError if the response code is not equal to 200 def https_get host, path, params, headers = nil https_connection host do |connection| connection.request_get(path + "?" + to_query_string(params), headers) end end def https_connection (host) connection = Net::HTTP.new(host, SSL_PORT) connection.use_ssl = true if ssl_ca_file connection.ca_file = ssl_ca_file else logger << "No SSL ca file provided. It is highly reccomended to use one in production envinronments" if respond_to?(:logger) && logger connection.verify_mode = OpenSSL::SSL::VERIFY_NONE end process_http_response(yield(connection)) end def process_http_response response raise response.body if response.code != "200" response.body end end end ================================================ FILE: lib/omnicontacts/importer/facebook.rb ================================================ require "omnicontacts/parse_utils" require "omnicontacts/middleware/oauth2" require "json" module OmniContacts module Importer class Facebook < Middleware::OAuth2 include ParseUtils attr_reader :auth_host, :authorize_path, :auth_token_path, :scope def initialize *args super *args @auth_host = 'graph.facebook.com' @authorize_path = '/oauth/authorize' @scope = 'email,user_relationships,user_birthday,user_friends' @auth_token_path = '/oauth/access_token' @contacts_host = 'graph.facebook.com' @friends_path = '/v2.5/me/friends' @family_path = '/v2.5/me/family' @self_path = '/v2.5/me' end def fetch_contacts_using_access_token access_token, access_token_secret self_response = fetch_current_user access_token user = current_user self_response set_current_user user spouse_id = extract_spouse_id self_response spouse_response = nil if spouse_id spouse_path = "/#{spouse_id}" spouse_response = https_get(@contacts_host, spouse_path, {:access_token => access_token, :fields => 'first_name,last_name,name,id,gender,birthday,picture'}) end family_response = https_get(@contacts_host, @family_path, {:access_token => access_token, :fields => 'first_name,last_name,name,id,gender,birthday,picture,relationship'}) friends_response = https_get(@contacts_host, @friends_path, {:access_token => access_token, :fields => 'first_name,last_name,name,id,gender,birthday,picture'}) contacts_from_response(spouse_response, family_response, friends_response) end def fetch_current_user access_token self_response = https_get(@contacts_host, @self_path, {:access_token => access_token, :fields => 'first_name,last_name,name,id,gender,birthday,picture,relationship_status,significant_other,email'}) self_response = JSON.parse(self_response) if self_response self_response end private def extract_spouse_id response return nil if response.nil? id = nil if response['significant_other'] && response['relationship_status'] == 'Married' id = response['significant_other']['id'] end id end def contacts_from_response(spouse_response, family_response, friends_response) contacts = [] family_ids = Set.new if spouse_response spouse_contact = create_contact_element(JSON.parse(spouse_response)) spouse_contact[:relation] = 'spouse' contacts << spouse_contact family_ids.add(spouse_contact[:id]) end if family_response family_response = JSON.parse(family_response) family_response['data'].each do |family_contact| contacts << create_contact_element(family_contact) family_ids.add(family_contact['id']) end end if friends_response friends_response = JSON.parse(friends_response) friends_response['data'].each do |friends_contact| contacts << create_contact_element(friends_contact) unless family_ids.include?(friends_contact['id']) end end contacts end def create_contact_element contact_info # creating nil fields to keep the fields consistent across other networks contact = {:id => nil, :first_name => nil, :last_name => nil, :name => nil, :email => nil, :gender => nil, :birthday => nil, :profile_picture=> nil, :relation => nil} contact[:id] = contact_info['id'] contact[:first_name] = normalize_name(contact_info['first_name']) contact[:last_name] = normalize_name(contact_info['last_name']) contact[:name] = contact_info['name'] contact[:email] = contact_info['email'] contact[:gender] = contact_info['gender'] contact[:birthday] = birthday(contact_info['birthday']) contact[:profile_picture] = image_url(contact_info['id']) contact[:relation] = contact_info['relationship'] contact end def image_url fb_id return "https://graph.facebook.com/" + fb_id + "/picture" if fb_id end def escape_windows_format value value.gsub(/[\r\s]/, '') end def birthday dob return nil if dob.nil? birthday = dob.split('/') return birthday_format(birthday[0],birthday[1],birthday[2]) end def current_user me return nil if me.nil? user = {:id => me['id'], :email => me['email'], :name => me['name'], :first_name => normalize_name(me['first_name']), :last_name => normalize_name(me['last_name']), :birthday => birthday(me['birthday']), :gender => me['gender'], :profile_picture => image_url(me['id']) } user end end end end ================================================ FILE: lib/omnicontacts/importer/gmail.rb ================================================ require "omnicontacts/parse_utils" require "omnicontacts/middleware/oauth2" module OmniContacts module Importer class Gmail < Middleware::OAuth2 include ParseUtils attr_reader :auth_host, :authorize_path, :auth_token_path, :scope def initialize *args super *args @auth_host = "accounts.google.com" @authorize_path = "/o/oauth2/auth" @auth_token_path = "/o/oauth2/token" @scope = (args[3] && args[3][:scope]) || "https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile" @contacts_host = "www.google.com" @contacts_path = "/m8/feeds/contacts/default/full" @max_results = (args[3] && args[3][:max_results]) || 100 @self_host = "www.googleapis.com" @profile_path = "/oauth2/v3/userinfo" end def fetch_contacts_using_access_token access_token, token_type fetch_current_user(access_token, token_type) contacts_response = https_get(@contacts_host, @contacts_path, contacts_req_params, contacts_req_headers(access_token, token_type)) contacts_from_response(contacts_response, access_token) end def fetch_current_user access_token, token_type self_response = https_get(@self_host, @profile_path, contacts_req_params, contacts_req_headers(access_token, token_type)) user = current_user(self_response, access_token, token_type) set_current_user user end private def contacts_req_params {'max-results' => @max_results.to_s, 'alt' => 'json'} end def contacts_req_headers token, token_type {"GData-Version" => "3.0", "Authorization" => "#{token_type} #{token}"} end def contacts_from_response(response_as_json, access_token) response = JSON.parse(response_as_json) return [] if response['feed'].nil? || response['feed']['entry'].nil? contacts = [] return contacts if response.nil? response['feed']['entry'].each do |entry| # creating nil fields to keep the fields consistent across other networks contact = { :id => nil, :first_name => nil, :last_name => nil, :name => nil, :emails => nil, :gender => nil, :birthday => nil, :profile_picture=> nil, :relation => nil, :addresses => nil, :phone_numbers => nil, :dates => nil, :company => nil, :position => nil } contact[:id] = entry['id']['$t'] if entry['id'] if entry['gd$name'] gd_name = entry['gd$name'] contact[:first_name] = normalize_name(entry['gd$name']['gd$givenName']['$t']) if gd_name['gd$givenName'] contact[:last_name] = normalize_name(entry['gd$name']['gd$familyName']['$t']) if gd_name['gd$familyName'] contact[:name] = normalize_name(entry['gd$name']['gd$fullName']['$t']) if gd_name['gd$fullName'] contact[:name] = full_name(contact[:first_name],contact[:last_name]) if contact[:name].nil? end contact[:emails] = [] entry['gd$email'].each do |email| if email['rel'] split_index = email['rel'].index('#') contact[:emails] << {:name => email['rel'][split_index + 1, email['rel'].length - 1], :email => email['address']} elsif email['label'] contact[:emails] << {:name => email['label'], :email => email['address']} end end if entry['gd$email'] # Support older versions of the gem by keeping singular entries around contact[:email] = contact[:emails][0][:email] if contact[:emails][0] contact[:first_name], contact[:last_name], contact[:name] = email_to_name(contact[:name]) if !contact[:name].nil? && contact[:name].include?('@') contact[:first_name], contact[:last_name], contact[:name] = email_to_name(contact[:emails][0][:email]) if (contact[:name].nil? && contact[:emails][0] && contact[:emails][0][:email]) #format - year-month-date contact[:birthday] = birthday(entry['gContact$birthday']['when']) if entry['gContact$birthday'] # value is either "male" or "female" contact[:gender] = entry['gContact$gender']['value'] if entry['gContact$gender'] if entry['gContact$relation'] if entry['gContact$relation'].is_a?(Hash) contact[:relation] = entry['gContact$relation']['rel'] elsif entry['gContact$relation'].is_a?(Array) contact[:relation] = entry['gContact$relation'].first['rel'] end end contact[:addresses] = [] entry['gd$structuredPostalAddress'].each do |address| if address['rel'] split_index = address['rel'].index('#') new_address = {:name => address['rel'][split_index + 1, address['rel'].length - 1]} elsif address['label'] new_address = {:name => address['label']} end new_address[:address_1] = address['gd$street']['$t'] if address['gd$street'] new_address[:address_1] = address['gd$formattedAddress']['$t'] if new_address[:address_1].nil? && address['gd$formattedAddress'] if !new_address[:address_1].nil? && new_address[:address_1].index("\n") parts = new_address[:address_1].split("\n") new_address[:address_1] = parts.first # this may contain city/state/zip if user jammed it all into one string.... :-( new_address[:address_2] = parts[1..-1].join(', ') end new_address[:city] = address['gd$city']['$t'] if address['gd$city'] new_address[:region] = address['gd$region']['$t'] if address['gd$region'] # like state or province new_address[:country] = address['gd$country']['code'] if address['gd$country'] new_address[:postcode] = address['gd$postcode']['$t'] if address['gd$postcode'] contact[:addresses] << new_address end if entry['gd$structuredPostalAddress'] # Support older versions of the gem by keeping singular entries around if contact[:addresses][0] contact[:address_1] = contact[:addresses][0][:address_1] contact[:address_2] = contact[:addresses][0][:address_2] contact[:city] = contact[:addresses][0][:city] contact[:region] = contact[:addresses][0][:region] contact[:country] = contact[:addresses][0][:country] contact[:postcode] = contact[:addresses][0][:postcode] end contact[:phone_numbers] = [] entry['gd$phoneNumber'].each do |phone_number| if phone_number['rel'] split_index = phone_number['rel'].index('#') contact[:phone_numbers] << {:name => phone_number['rel'][split_index + 1, phone_number['rel'].length - 1], :number => phone_number['$t']} elsif phone_number['label'] contact[:phone_numbers] << {:name => phone_number['label'], :number => phone_number['$t']} end end if entry['gd$phoneNumber'] # Support older versions of the gem by keeping singular entries around contact[:phone_number] = contact[:phone_numbers][0][:number] if contact[:phone_numbers][0] if entry["link"] && entry["link"].is_a?(Array) entry["link"].each do |link| if link["type"] == 'image/*' && link["gd$etag"] contact[:profile_picture] = link["href"] + "?&access_token=" + access_token break end end end if entry['gContact$event'] contact[:dates] = [] entry['gContact$event'].each do |event| if event['rel'] contact[:dates] << {:name => event['rel'], :date => birthday(event['gd$when']['startTime'])} elsif event['label'] contact[:dates] << {:name => event['label'], :date => birthday(event['gd$when']['startTime'])} end end end if entry['gd$organization'] contact[:company] = entry['gd$organization'][0]['gd$orgName']['$t'] if entry['gd$organization'][0]['gd$orgName'] contact[:position] = entry['gd$organization'][0]['gd$orgTitle']['$t'] if entry['gd$organization'][0]['gd$orgTitle'] end contacts << contact if contact[:name] end contacts.uniq! {|c| c[:email] || c[:profile_picture] || c[:name]} contacts end def current_user me, access_token, token_type return nil if me.nil? me = JSON.parse(me) user = {:id => me['id'], :email => me['email'], :name => me['name'], :first_name => me['given_name'], :last_name => me['family_name'], :gender => me['gender'], :birthday => birthday(me['birthday']), :profile_picture => me["picture"], :access_token => access_token, :token_type => token_type } user end def birthday dob return nil if dob.nil? birthday = dob.split('-') return birthday_format(birthday[2], birthday[3], nil) if birthday.size == 4 return birthday_format(birthday[1], birthday[2], birthday[0]) if birthday.size == 3 end def contact_id(profile_url) id = (profile_url.present?) ? File.basename(profile_url) : nil id end end end end ================================================ FILE: lib/omnicontacts/importer/hotmail.rb ================================================ require "omnicontacts/middleware/oauth2" require "omnicontacts/parse_utils" require "json" module OmniContacts module Importer class Hotmail < Middleware::OAuth2 include ParseUtils attr_reader :auth_host, :authorize_path, :auth_token_path, :scope def initialize app, client_id, client_secret, options ={} super app, client_id, client_secret, options @auth_host = "login.live.com" @authorize_path = "/oauth20_authorize.srf" @scope = options[:permissions] || "wl.signin, wl.basic, wl.birthday , wl.emails ,wl.contacts_birthday , wl.contacts_photos, wl.contacts_emails" @auth_token_path = "/oauth20_token.srf" @contacts_host = "apis.live.net" @contacts_path = "/v5.0/me/contacts" @self_path = "/v5.0/me" end def fetch_contacts_using_access_token access_token, access_token_secret fetch_current_user(access_token) contacts_response = https_get(@contacts_host, @contacts_path, :access_token => access_token) contacts_from_response contacts_response end def fetch_current_user access_token self_response = https_get(@contacts_host, @self_path, :access_token => access_token) user = current_user self_response set_current_user user end private def contacts_from_response response_as_json response = JSON.parse(response_as_json) contacts = [] response['data'].each do |entry| # creating nil fields to keep the fields consistent across other networks contact = {:id => nil, :first_name => nil, :last_name => nil, :name => nil, :email => nil, :gender => nil, :birthday => nil, :profile_picture=> nil, :relation => nil, :email_hashes => []} contact[:id] = entry['user_id'] ? entry['user_id'] : entry['id'] contact[:email] = parse_email(entry['emails']) if valid_email? parse_email(entry['emails']) contact[:first_name] = normalize_name(entry['first_name']) contact[:last_name] = normalize_name(entry['last_name']) contact[:name] = normalize_name(entry['name']) contact[:birthday] = birthday_format(entry['birth_month'], entry['birth_day'], entry['birth_year']) contact[:gender] = entry['gender'] contact[:profile_picture] = image_url(entry['user_id']) contact[:email_hashes] = entry['email_hashes'] contacts << contact if contact[:name] || contact[:first_name] end contacts end def parse_email(emails) return nil if emails.nil? emails['account'] || emails['preferred'] || emails['personal'] || emails['business'] || emails['other'] end def current_user me return nil if me.nil? me = JSON.parse(me) email = parse_email(me['emails']) user = {:id => me['id'], :email => email, :name => me['name'], :first_name => me['first_name'], :last_name => me['last_name'], :gender => me['gender'], :profile_picture => image_url(me['id']), :birthday => birthday_format(me['birth_month'], me['birth_day'], me['birth_year']) } user end def image_url hotmail_id return 'https://apis.live.net/v5.0/' + hotmail_id + '/picture' if hotmail_id end def escape_windows_format value value.gsub(/[\r\s]/, '') end def valid_email? value /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/.match(value) end end end end ================================================ FILE: lib/omnicontacts/importer/linkedin.rb ================================================ require "omnicontacts/parse_utils" require "omnicontacts/middleware/oauth2" module OmniContacts module Importer class Linkedin < Middleware::OAuth2 include ParseUtils attr_reader :auth_host, :authorize_path, :auth_token_path, :scope, :state def initialize *args super *args @auth_host = "www.linkedin.com" @authorize_path = "/uas/oauth2/authorization" @auth_token_path = "/uas/oauth2/accessToken" @scope = (args[3] && args[3][:scope]) || "r_network" @contacts_host = "api.linkedin.com" @contacts_path = "/v1/people/~/connections:(id,first-name,last-name,picture-url)" @self_host = "www.linkedin.com" @profile_path = "/oauth2/v1/userinfo" @state = (args[3] && args[3][:state]) end def fetch_contacts_using_access_token access_token, token_type token_type = "Bearer" if token_type.nil? contacts_response = https_get(@contacts_host, @contacts_path, contacts_req_params, contacts_req_headers(access_token, token_type)) contacts_from_response contacts_response end private def contacts_req_params {'format' => 'json'} end def contacts_req_headers token, token_type {"Authorization" => "#{token_type} #{token}"} end def contacts_from_response response_as_json response = JSON.parse(response_as_json) return [] if response['values'].nil? contacts = [] return contacts if response.nil? response['values'].map do |entry| { id: entry['id'], first_name: normalize_name(entry['firstName']), last_name: normalize_name(entry['lastName']), name: full_name(entry['firstName'],entry['lastName']), profile_picture: entry['pictureUrl'] } end end def authorize_url_params # merge state param required by LinkedIn _params = super _params += "&" + to_query_string(state: @state) end end end end ================================================ FILE: lib/omnicontacts/importer/outlook.rb ================================================ require "omnicontacts/middleware/oauth2" require "omnicontacts/parse_utils" require "json" # API Docs: https://msdn.microsoft.com/en-us/office/office365/api/api-catalog#Outlookcontacts module OmniContacts module Importer class Outlook < Middleware::OAuth2 include ParseUtils attr_reader :auth_host, :authorize_path, :auth_token_path, :scope def initialize app, client_id, client_secret, options ={} super app, client_id, client_secret, options @auth_host = "login.microsoftonline.com" @authorize_path = "/common/oauth2/v2.0/authorize" @scope = options[:permissions] || "https://outlook.office.com/contacts.read" @auth_token_path = "/common/oauth2/v2.0/token" @contacts_host = "outlook.office.com" @contacts_path = "/api/v2.0/me/contacts" @self_path = "/api/v2.0/me" end def fetch_contacts_using_access_token access_token, token_type fetch_current_user(access_token, token_type) contacts_response = https_get(@contacts_host, @contacts_path, {}, contacts_req_headers(access_token, token_type)) contacts_from_response contacts_response end def fetch_current_user access_token, token_type self_response = https_get(@contacts_host, @self_path, {}, contacts_req_headers(access_token, token_type)) user = current_user self_response set_current_user user end private def contacts_req_headers token, token_type { "Authorization" => "#{token_type} #{token}" } end def current_user me return nil if me.nil? me = JSON.parse(me) name_splitted = me["DisplayName"].split(" ") first_name = name_splitted.first last_name = name_splitted.last if name_splitted.size > 1 user = empty_contact user[:id] = me["Id"] user[:email] = me["EmailAddress"] user[:name] = me["DisplayName"] user[:first_name] = normalize_name(first_name) user[:last_name] = normalize_name(last_name) user end def contacts_from_response response_as_json response = JSON.parse(response_as_json) contacts = [] response["value"].each do |entry| contact = empty_contact # Full fields reference: # https://msdn.microsoft.com/office/office365/api/complex-types-for-mail-contacts-calendar#RESTAPIResourcesContact contact[:id] = entry["Id"] contact[:first_name] = entry["GivenName"] contact[:last_name] = entry["Surname"] contact[:name] = entry["DisplayName"] contact[:email] = parse_email(entry["EmailAddresses"]) contact[:birthday] = birthday(entry["Birthday"]) address = [entry["HomeAddress"], entry["BusinessAddress"], entry["OtherAddress"]].reject(&:empty?).first if address contact[:address_1] = address["Street"] contact[:city] = address["City"] contact[:region] = address["State"] contact[:postcode] = address["PostalCode"] contact[:country] = address["CountryOrRegion"] end contacts << contact if contact[:name] || contact[:first_name] end contacts end def empty_contact { :id => nil, :first_name => nil, :last_name => nil, :name => nil, :email => nil, :gender => nil, :birthday => nil, :profile_picture => nil, :address_1 => nil, :address_2 => nil, :city => nil, :region => nil, :postcode => nil, :relation => nil } end def parse_email emails return nil if emails.nil? emails.map! { |email| email["Address"] } emails.select! { |email| valid_email? email } emails.first end def birthday dob return nil if dob.nil? birthday = dob[0..9].split("-") birthday[0] = nil if birthday[0].to_i < 1900 # if year is not set it returns 1604 return birthday_format(birthday[1], birthday[2], birthday[0]) end def valid_email? value /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/.match(value) end end end end ================================================ FILE: lib/omnicontacts/importer/yahoo.rb ================================================ require "omnicontacts/parse_utils" require "omnicontacts/middleware/oauth1" require "json" module OmniContacts module Importer class Yahoo < Middleware::OAuth1 include ParseUtils attr_reader :auth_host, :auth_token_path, :auth_path, :access_token_path def initialize *args super *args @auth_host = 'api.login.yahoo.com' @auth_token_path = '/oauth/v2/get_request_token' @auth_path = '/oauth/v2/request_auth' @access_token_path = '/oauth/v2/get_token' @contacts_host = 'social.yahooapis.com' end def fetch_contacts_from_token_and_verifier auth_token, auth_token_secret, auth_verifier (access_token, access_token_secret, guid) = fetch_access_token(auth_token, auth_token_secret, auth_verifier, ['xoauth_yahoo_guid']) fetch_current_user(access_token, access_token_secret, guid) contacts_path = "/v1/user/#{guid}/contacts" contacts_response = https_get(@contacts_host, contacts_path, contacts_req_params(access_token, access_token_secret, contacts_path)) contacts_from_response contacts_response end def fetch_current_user access_token, access_token_secret, guid self_path = "/v1/user/#{guid}/profile" self_response = https_get(@contacts_host, self_path, contacts_req_params(access_token, access_token_secret, self_path)) user = current_user self_response set_current_user user end private def contacts_req_params access_token, access_token_secret, contacts_path params = { :format => 'json', :oauth_consumer_key => consumer_key, :oauth_nonce => encode(random_string), :oauth_signature_method => 'HMAC-SHA1', :oauth_timestamp => timestamp, :oauth_token => access_token, :oauth_version => OmniContacts::Authorization::OAuth1::OAUTH_VERSION } contacts_url = "https://#{@contacts_host}#{contacts_path}" params['oauth_signature'] = oauth_signature('GET', contacts_url, params, access_token_secret) params end def contacts_from_response response_as_json response = JSON.parse(response_as_json) contacts = [] return contacts unless response['contacts']['contact'] response['contacts']['contact'].each do |entry| # creating nil fields to keep the fields consistent across other networks contact = { :id => nil, :first_name => nil, :last_name => nil, :name => nil, :email => nil, :gender => nil, :birthday => nil, :profile_picture=> nil, :address_1 => nil, :address_2 => nil, :city => nil, :region => nil, :postcode => nil, :relation => nil } yahoo_id = nil contact[:id] = entry['id'].to_s entry['fields'].each do |field| case field['type'] when 'name' contact[:first_name] = normalize_name(field['value']['givenName']) contact[:last_name] = normalize_name(field['value']['familyName']) contact[:name] = full_name(contact[:first_name],contact[:last_name]) when 'email' contact[:email] = field['value'] if field['type'] == 'email' when 'yahooid' yahoo_id = field['value'] when 'address' value = field['value'] contact[:address_1] = street = value['street'] if street.index("\n") parts = street.split("\n") contact[:address_1] = parts.first contact[:address_2] = parts[1..-1].join(', ') end contact[:city] = value['city'] contact[:region] = value['stateOrProvince'] contact[:postcode] = value['postalCode'] when 'birthday' contact[:birthday] = birthday_format(field['value']['month'], field['value']['day'],field['value']['year']) end contact[:first_name], contact[:last_name], contact[:name] = email_to_name(contact[:email]) if contact[:name].nil? && contact[:email] # contact[:first_name], contact[:last_name], contact[:name] = email_to_name(yahoo_id) if (yahoo_id && contact[:name].nil? && contact[:email].nil?) if yahoo_id contact[:profile_picture] = image_url(yahoo_id) else contact[:profile_picture] = image_url_from_email(contact[:email]) end end contacts << contact if contact[:name] end contacts.uniq! {|c| c[:email] || c[:profile_picture] || c[:name]} contacts end def image_url yahoo_id return 'https://img.msg.yahoo.com/avatar.php?yids=' + yahoo_id if yahoo_id end def parse_email(emails) return nil if emails.nil? email = nil if emails.is_a?(Hash) if emails.has_key?("primary") email = emails['handle'] end elsif emails.is_a?(Array) emails.each do |e| if e.has_key?('primary') && e['primary'] email = e['handle'] break end end end email end def birthday dob return nil if dob.nil? birthday = dob.split('/') return birthday_format(birthday[0], birthday[1], birthday[3]) if birthday.size == 3 return birthday_format(birthday[0], birthday[1], nil) if birthday.size == 2 end def gender g return "female" if g == "F" return "male" if g == "M" end def my_image img return nil if img.nil? return img['imageUrl'] end def current_user me return nil if me.nil? me = JSON.parse(me) me = me['profile'] email = parse_email(me['emails']) user = {:id => me["guid"], :email => email, :name => full_name(me['givenName'],me['familyName']), :first_name => normalize_name(me['givenName']), :last_name => normalize_name(me['familyName']), :gender => gender(me['gender']), :birthday => birthday(me['birthdate']), :profile_picture => my_image(me['image']) } user end #def profile_image_url(guid, access_token, access_token_secret) # image_path = "/v1/user/#{guid}/profile/image/48x48" # response = https_get(@contacts_host, image_path, contacts_req_params(access_token, access_token_secret, image_path)) # image_data = JSON.parse(response) # return image_data['image']['imageUrl'] if image_data['image'] # return nil #end end end end ================================================ FILE: lib/omnicontacts/importer.rb ================================================ module OmniContacts module Importer autoload :Gmail, "omnicontacts/importer/gmail" autoload :Yahoo, "omnicontacts/importer/yahoo" autoload :Hotmail, "omnicontacts/importer/hotmail" autoload :Outlook, "omnicontacts/importer/outlook" autoload :Facebook, "omnicontacts/importer/facebook" autoload :Linkedin, "omnicontacts/importer/linkedin" end end ================================================ FILE: lib/omnicontacts/integration_test.rb ================================================ require 'singleton' class IntegrationTest include Singleton attr_accessor :enabled def initialize enabled = false clear_mocks end def clear_mocks @user_mocks = {} @contact_mocks = {} end def mock provider, contacts, user = {} @contact_mocks[provider.to_sym] = contacts @user_mocks[provider.to_sym] = user end def mock_authorization_from_user provider [302, {"Content-Type" => "application/x-www-form-urlencoded", "location" => provider.redirect_path}, []] end def mock_fetch_contacts provider result = @contact_mocks[provider.class_name.to_sym] || [] if result.is_a? Array result elsif result.is_a? Hash [result] else raise result.to_s end end def mock_fetch_user provider @user_mocks[provider.class_name.to_sym] || {} end end ================================================ FILE: lib/omnicontacts/middleware/base_oauth.rb ================================================ # This class contains the common behavior for middlewares # implementing either versions of OAuth. # # Extending classes are required to implement # the following methods: # * request_authorization_from_user # * fetch_contatcs module OmniContacts module Middleware class BaseOAuth attr_reader :ssl_ca_file def initialize app, options @app = app @listening_path = MOUNT_PATH + class_name @ssl_ca_file = options[:ssl_ca_file] end def class_name self.class.name.split('::').last.downcase end # Rack callback. It handles three cases: # * user visit middleware entry point. # In this case request_authorization_from_user is called # * user is redirected back to the application # from the authorization site. In this case the list # of contacts is fetched and stored in the variables # omnicontacts.contacts within the Rack env variable. # Once that is done the next middleware component is called. # * user visits any other resource. In this case the request # is simply forwarded to the next middleware component. def call env @env = env if env["PATH_INFO"] =~ /^#{@listening_path}\/?$/ session['omnicontacts.params'] = Rack::Request.new(env).params handle_initial_request elsif env["PATH_INFO"] =~ /^#{redirect_path}/ env['omnicontacts.params'] = session.delete('omnicontacts.params') handle_callback else @app.call(env) end end private def test_mode? IntegrationTest.instance.enabled end def handle_initial_request execute_and_rescue_exceptions do if test_mode? IntegrationTest.instance.mock_authorization_from_user(self) else request_authorization_from_user end end end def handle_callback execute_and_rescue_exceptions do @env["omnicontacts.contacts"] = if test_mode? IntegrationTest.instance.mock_fetch_contacts(self) else fetch_contacts end set_current_user IntegrationTest.instance.mock_fetch_user(self) if test_mode? @app.call(@env) end end def set_current_user user @env["omnicontacts.user"] = user end # This method rescues executes a block of code and # rescue all exceptions. In case of an exception the # user is redirected to the failure endpoint. def execute_and_rescue_exceptions yield rescue AuthorizationError => e handle_error :not_authorized, e rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e handle_error :timeout, e rescue ::RuntimeError => e handle_error :internal_error, e end def handle_error error_type, exception logger.puts("Error #{error_type} while processing #{@env["PATH_INFO"]}: #{exception.message}") if logger failure_url = "#{ MOUNT_PATH }failure?error_message=#{error_type}&importer=#{class_name}" params_url = append_request_params(failure_url) target_url = append_state_query(params_url) [302, {"Content-Type" => "text/html", "location" => target_url}, []] end def session raise "You must provide a session to use OmniContacts" unless @env["rack.session"] @env["rack.session"] end def logger @env["rack.errors"] if @env end def base_prop_name "omnicontacts." + class_name end def append_request_params(target_url) return target_url unless @env['omnicontacts.params'] params = Rack::Utils.build_query(@env['omnicontacts.params']) unless params.nil? or params.empty? target_url = target_url + (target_url.include?("?")?"&":"?") + params end return target_url end def append_state_query(target_url) state = Rack::Utils.parse_query(@env['QUERY_STRING'])['state'] unless state.nil? target_url = target_url + (target_url.include?("?")?"&":"?") + 'state=' + state end return target_url end end end end ================================================ FILE: lib/omnicontacts/middleware/oauth1.rb ================================================ require "omnicontacts/authorization/oauth1" require "omnicontacts/middleware/base_oauth" # This class is an OAuth 1.0 Rack middleware. # # Extending classes are required to # implement the following methods: # * fetch_token_from_token_and_verifier -> this method has to # fetch the list of contacts from the authorization server. module OmniContacts module Middleware class OAuth1 < BaseOAuth include Authorization::OAuth1 attr_reader :consumer_key, :consumer_secret, :callback_path def initialize app, consumer_key, consumer_secret, options = {} super app, options @consumer_key = consumer_key @consumer_secret = consumer_secret @callback_path = options[:callback_path] || "#{ MOUNT_PATH }#{class_name}/callback" @token_prop_name = "#{base_prop_name}.oauth_token" end def callback host_url_from_rack_env(@env) + callback_path end alias :redirect_path :callback_path # Obtains an authorization token from the server, # stores it and the session and redirect the user # to the authorization website. def request_authorization_from_user (auth_token, auth_token_secret) = fetch_authorization_token session[@token_prop_name] = auth_token session[token_secret_prop_name(auth_token)] = auth_token_secret redirect_to_authorization_site(auth_token) end def token_secret_prop_name oauth_token "#{base_prop_name}.#{oauth_token}.oauth_token_secret" end def redirect_to_authorization_site auth_token authorization_url = authorization_url(auth_token) target_url = append_state_query(authorization_url) [302, {"Content-Type" => "application/x-www-form-urlencoded", "location" => target_url}, []] end # Parses the authorization token from the query string and # obtain the relative secret from the session. # Finally it calls fetch_contacts_from_token_and_verifier. # If token is found in the query string an AuhorizationError # is raised. def fetch_contacts params = query_string_to_map(@env["QUERY_STRING"]) oauth_token = params["oauth_token"] oauth_verifier = params["oauth_verifier"] oauth_token_secret = session[token_secret_prop_name(oauth_token)] if oauth_token && oauth_verifier && oauth_token_secret fetch_contacts_from_token_and_verifier(oauth_token, oauth_token_secret, oauth_verifier) else raise AuthorizationError.new("User did not grant access to contacts list") end end end end end ================================================ FILE: lib/omnicontacts/middleware/oauth2.rb ================================================ require "omnicontacts/authorization/oauth2" require "omnicontacts/middleware/base_oauth" # This class is a OAuth 2 Rack middleware. # # Extending class are required to implement # the following methods: # * fetch_contacts_using_access_token -> it # fetches the list of contacts from the authorization # server. module OmniContacts module Middleware class OAuth2 < BaseOAuth include Authorization::OAuth2 attr_reader :client_id, :client_secret, :redirect_path def initialize app, client_id, client_secret, options ={} super app, options @client_id = client_id @client_secret = client_secret @redirect_path = options[:redirect_path] || "#{ MOUNT_PATH }#{class_name}/callback" @ssl_ca_file = options[:ssl_ca_file] end def request_authorization_from_user target_url = append_state_query(authorization_url) [302, {"Content-Type" => "application/x-www-form-urlencoded", "location" => target_url}, []] end def redirect_uri host_url_from_rack_env(@env) + redirect_path end # It extract the authorization code from the query string. # It uses it to obtain an access token. # If the authorization code has a refresh token associated # with it in the session, it uses the obtain an access token. # It fetches the list of contacts and stores the refresh token # associated with the access token in the session. # Finally it returns the list of contacts. # If no authorization code is found in the query string an # AuthoriazationError is raised. def fetch_contacts code = query_string_to_map(@env["QUERY_STRING"])["code"] if code refresh_token = session[refresh_token_prop_name(code)] (access_token, token_type, refresh_token) = if refresh_token refresh_access_token(refresh_token) else fetch_access_token(code) end contacts = fetch_contacts_using_access_token(access_token, token_type) session[refresh_token_prop_name(code)] = refresh_token if refresh_token contacts else raise AuthorizationError.new("User did not grant access to contacts list") end end def refresh_token_prop_name code "#{base_prop_name}.#{code}.refresh_token" end end end end ================================================ FILE: lib/omnicontacts/parse_utils.rb ================================================ module OmniContacts module ParseUtils # return has of birthday day, month and year def birthday_format month, day, year return {:day => day.to_i, :month => month.to_i, :year => year.to_i}if year && month && day return {:day => day.to_i, :month => month.to_i, :year => nil} if !year && month && day return nil if (!year && !month) || (!year && !day) end # normalize the name def normalize_name name return nil if name.nil? name.chomp! name.squeeze!(' ') name.strip! return name end # create a full name given the individual first and last name def full_name first_name, last_name return "#{first_name} #{last_name}" if first_name && last_name return "#{first_name}" if first_name && !last_name return "#{last_name}" if !first_name && last_name return nil end # create a username/name from a given email def email_to_name username_or_email username_or_email = username_or_email.split('@').first if username_or_email.include?('@') if group = (/(?[a-z|A-Z]+)[\.|_](?[a-z|A-Z]+)/).match(username_or_email) first_name = normalize_name(group[:first]) last_name = normalize_name(group[:last]) return first_name, last_name, "#{first_name} #{last_name}" end username = normalize_name(username_or_email) return username, nil, username end # create an image_url from a gmail or yahoo email id. def image_url_from_email email return nil if email.nil? || !email.include?('@') username, domain = *(email.split('@')) return nil if username.nil? or domain.nil? gmail_base_url = "https://profiles.google.com/s2/photos/profile/" yahoo_base_url = "https://img.msg.yahoo.com/avatar.php?yids=" if domain.include?('gmail') image_url = gmail_base_url + username elsif domain.include?('yahoo') image_url = yahoo_base_url + username end image_url end end end ================================================ FILE: lib/omnicontacts.rb ================================================ module OmniContacts VERSION = "0.3.10" MOUNT_PATH = "/contacts/" autoload :Builder, "omnicontacts/builder" autoload :Importer, "omnicontacts/importer" autoload :IntegrationTest, "omnicontacts/integration_test" class AuthorizationError < RuntimeError end def self.integration_test IntegrationTest.instance end end ================================================ FILE: omnicontacts.gemspec ================================================ # encoding: utf-8 require File.expand_path('../lib/omnicontacts', __FILE__) Gem::Specification.new do |gem| gem.name = 'omnicontacts' gem.description = %q{A generalized Rack middleware for importing contacts from major email providers.} gem.authors = ['Diego Castorina', 'Jordan Lance', 'Asma Tameem', 'Randy Villanueva'] gem.email = ['diegocastorina@gmail.com', 'voorruby@gmail.com'] gem.add_runtime_dependency 'rack' gem.add_runtime_dependency 'json' gem.add_development_dependency 'simplecov' gem.add_development_dependency 'rake' gem.add_development_dependency 'rack-test' gem.add_development_dependency 'rspec' gem.version = OmniContacts::VERSION gem.files = `git ls-files`.split("\n") gem.homepage = 'http://github.com/Diego81/omnicontacts' gem.require_paths = ['lib'] gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6') if gem.respond_to? :required_rubygems_version= gem.summary = gem.description gem.test_files = `git ls-files -- {spec}/*`.split("\n") end ================================================ FILE: spec/omnicontacts/authorization/oauth1_spec.rb ================================================ require "spec_helper" require "omnicontacts/authorization/oauth1" describe OmniContacts::Authorization::OAuth1 do before(:all) do OAuth1TestClass= Struct.new(:consumer_key, :consumer_secret, :auth_host, :auth_token_path, :auth_path, :access_token_path, :callback) class OAuth1TestClass include OmniContacts::Authorization::OAuth1 end end let(:test_target) do OAuth1TestClass.new("consumer_key", "secret1", "auth_host", "auth_token_path", "auth_path", "access_token_path", "callback") end describe "fetch_authorization_token" do it "should request the token providing all mandatory parameters" do test_target.should_receive(:https_post) do |host, path, params| host.should eq(test_target.auth_host) path.should eq(test_target.auth_token_path) params[:oauth_consumer_key].should eq(test_target.consumer_key) params[:oauth_nonce].should_not be_nil params[:oauth_signature_method].should eq("PLAINTEXT") params[:oauth_signature].should eq(test_target.consumer_secret + "%26") params[:oauth_timestamp].should_not be_nil params[:oauth_version].should eq("1.0") params[:oauth_callback].should eq(test_target.callback) "oauth_token=token&oauth_token_secret=token_secret" end test_target.fetch_authorization_token end it "should successfully parse the result" do test_target.should_receive(:https_post).and_return("oauth_token=token&oauth_token_secret=token_secret") test_target.fetch_authorization_token.should eq(["token", "token_secret"]) end it "should raise an error if request is invalid" do test_target.should_receive(:https_post).and_return("invalid_request") expect { test_target.fetch_authorization_token }.to raise_error end end describe "authorization_url" do subject { test_target.authorization_url("token") } it { should eq("https://#{test_target.auth_host}#{test_target.auth_path}?oauth_token=token") } end describe "fetch_access_token" do it "should request the access token using all required parameters" do auth_token = "token" auth_token_secret = "token_secret" auth_verifier = "verifier" test_target.should_receive(:https_post) do |host, path, params| host.should eq(test_target.auth_host) path.should eq(test_target.access_token_path) params[:oauth_consumer_key].should eq(test_target.consumer_key) params[:oauth_nonce].should_not be_nil params[:oauth_signature_method].should eq("PLAINTEXT") params[:oauth_version].should eq("1.0") params[:oauth_signature].should eq("#{test_target.consumer_secret}%26#{auth_token_secret}") params[:oauth_token].should eq(auth_token) params[:oauth_verifier].should eq(auth_verifier) "oauth_token=access_token&oauth_token_secret=access_token_secret&other_param=other_value" end test_target.fetch_access_token auth_token, auth_token_secret, auth_verifier, ["other_param"] end it "should successfully extract access_token and the other fields" do test_target.should_receive(:https_post).and_return("oauth_token=access_token&oauth_token_secret=access_token_secret&other_param=other_value") test_target.fetch_access_token("token", "token_scret", "verified", ["other_param"]).should eq(["access_token", "access_token_secret", "other_value"]) end end describe "oauth_signature" do subject { test_target.oauth_signature("GET", "https://social.yahooapis.com/v1/user", {:name => "diego", :surname => "castorina"}, "secret2") } it { should eq("xfumZoyVYUbHXSAafdha3HZUqQg%3D") } end end ================================================ FILE: spec/omnicontacts/authorization/oauth2_spec.rb ================================================ require "spec_helper" require "omnicontacts/authorization/oauth2" describe OmniContacts::Authorization::OAuth2 do before(:all) do OAuth2TestClass= Struct.new(:auth_host, :authorize_path, :client_id, :client_secret, :scope, :redirect_uri, :auth_token_path) class OAuth2TestClass include OmniContacts::Authorization::OAuth2 end end let(:test_target) do OAuth2TestClass.new("auth_host", "authorize_path", "client_id", "client_secret", "scope", "redirect_uri", "auth_token_path") end describe "authorization_url" do subject { test_target.authorization_url } it { should include("https://#{test_target.auth_host}#{test_target.authorize_path}") } it { should include("client_id=#{test_target.client_id}") } it { should include("scope=#{test_target.scope}") } it { should include("redirect_uri=#{test_target.redirect_uri}") } it { should include("access_type=online") } it { should include("response_type=code") } end let(:access_token_response) { %[{"access_token": "access_token", "token_type":"token_type", "refresh_token":"refresh_token"}] } describe "fetch_access_token" do it "should provide all mandatory parameters in a https post request" do code = "code" test_target.should_receive(:https_post) do |host, path, params| host.should eq(test_target.auth_host) path.should eq(test_target.auth_token_path) params[:code].should eq(code) params[:client_id].should eq(test_target.client_id) params[:client_secret].should eq(test_target.client_secret) params[:redirect_uri].should eq(test_target.redirect_uri) params[:grant_type].should eq("authorization_code") access_token_response end test_target.fetch_access_token code end it "should successfully parse the token from the JSON response" do test_target.should_receive(:https_post).and_return(access_token_response) (access_token, token_type, refresh_token) = test_target.fetch_access_token "code" access_token.should eq("access_token") token_type.should eq("token_type") refresh_token.should eq("refresh_token") end it "should raise if the http request fails" do test_target.should_receive(:https_post).and_raise("Invalid code") expect { test_target.fetch_access_token("code") }.to raise_error end it "should raise an error if the JSON response contains an error field" do test_target.should_receive(:https_post).and_return(%[{"error": "error_message"}]) expect { test_target.fetch_access_token("code") }.to raise_error end end describe "refresh_access_token" do it "should provide all mandatory fields in a https post request" do refresh_token = "refresh_token" test_target.should_receive(:https_post) do |host, path, params| host.should eq(test_target.auth_host) path.should eq(test_target.auth_token_path) params[:client_id].should eq(test_target.client_id) params[:client_secret].should eq(test_target.client_secret) params[:refresh_token].should eq(refresh_token) params[:grant_type].should eq("refresh_token") access_token_response end test_target.refresh_access_token refresh_token end it "should successfully parse the token from the JSON response" do test_target.should_receive(:https_post).and_return(access_token_response) (access_token, token_type, refresh_token) = test_target.refresh_access_token "refresh_token" access_token.should eq("access_token") token_type.should eq("token_type") refresh_token.should eq("refresh_token") end end end ================================================ FILE: spec/omnicontacts/http_utils_spec.rb ================================================ require "spec_helper" require "omnicontacts/http_utils" describe OmniContacts::HTTPUtils do describe "to_query_string" do it "should create a query string from a map" do result = OmniContacts::HTTPUtils.to_query_string(:name => "john", :surname => "doe") if result.match(/^name/) result.should eq("name=john&surname=doe") else result.should eq("surname=doe&name=john") end end it "should work for integer values in the map" do result = OmniContacts::HTTPUtils.to_query_string(:client_id => 1234, :secret => "1234HJL8") result.should eq("client_id=1234&secret=1234HJL8") end end describe "encode" do it "should encode the space" do OmniContacts::HTTPUtils.encode("name=\"john\"").should eq("name%3D%22john%22") end end describe "query_string_to_map" do it "should split a query string into a map" do query_string = "name=john&surname=doe" result = OmniContacts::HTTPUtils.query_string_to_map(query_string) result["name"].should eq("john") result["surname"].should eq("doe") end end describe "host_url_from_rack_env" do it "should calculate the host url using the HTTP_HOST variable" do env = {"rack.url_scheme" => "http", "HTTP_HOST" => "localhost:8080", "SERVER_NAME" => "localhost", "SERVER_PORT" => 8080} OmniContacts::HTTPUtils.host_url_from_rack_env(env).should eq("http://localhost:8080") end it "should calculate the host url using SERVER_NAME and SERVER_PORT variables" do env = {"rack.url_scheme" => "http", "SERVER_NAME" => "localhost", "SERVER_PORT" => 8080} OmniContacts::HTTPUtils.host_url_from_rack_env(env).should eq("http://localhost:8080") end end describe "https_post" do before(:each) do @connection = double Net::HTTP.should_receive(:new).and_return(@connection) @connection.should_receive(:use_ssl=).with(true) @test_target = Object.new @test_target.extend OmniContacts::HTTPUtils @response = double end it "should execute a request with success" do @test_target.should_receive(:ssl_ca_file).and_return(nil) @connection.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) @connection.should_receive(:request_post).and_return(@response) @response.should_receive(:code).and_return("200") @response.should_receive(:body).and_return("some content") @test_target.send(:https_post, "host", "path", {}) end it "should raise an exception with response code != 200" do @test_target.should_receive(:ssl_ca_file).and_return(nil) @connection.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) @connection.should_receive(:request_get).and_return(@response) @response.should_receive(:code).and_return("500") @response.should_receive(:body).and_return("some error message") expect { @test_target.send(:https_get, "host", "path", {}) }.to raise_error end end end ================================================ FILE: spec/omnicontacts/importer/facebook_spec.rb ================================================ require "spec_helper" require "omnicontacts/importer/facebook" describe OmniContacts::Importer::Facebook do let(:facebook) { OmniContacts::Importer::Facebook.new({}, "client_id", "client_secret") } let(:self_response) { '{ "first_name":"Chris", "last_name":"Johnson", "name":"Chris Johnson", "id":"543216789", "gender":"male", "birthday":"06/21/1982", "significant_other":{"id": "243435322"}, "relationship_status": "Married", "picture":{"data":{"url":"http://profile.ak.fbcdn.net/hprofile-ak-snc6/186364_543216789_2089044200_q.jpg","is_silhouette":false}}, "email": "chrisjohnson@gmail.com" }' } let(:spouse_response) { '{ "first_name":"Mary", "last_name":"Johnson", "name":"Mary Johnson", "id":"243435322", "gender":"female", "birthday":"01/21", "picture":{"data":{"url":"http://profile.ak.fbcdn.net/hprofile-ak-snc6/186364_243435322_2089044200_q.jpg","is_silhouette":false}} }' } let(:contacts_as_json) { '{"data":[ { "first_name":"John", "last_name":"Smith", "name":"John Smith", "id":"608061886", "gender":"male", "birthday":"06/21", "relationship":"cousin", "picture":{"data":{"url":"http://profile.ak.fbcdn.net/hprofile-ak-snc6/186364_608061886_2089044200_q.jpg","is_silhouette":false}} } ] }' } describe "fetch_contacts_using_access_token" do let(:token) { "token" } let(:token_type) { "token_type" } before(:each) do facebook.instance_variable_set(:@env, {}) end it "should request the contacts by providing the token in the url" do facebook.should_receive(:https_get) do |host, self_path, params, headers| params[:access_token].should eq(token) params[:fields].should eq('first_name,last_name,name,id,gender,birthday,picture,relationship_status,significant_other,email') self_response end facebook.should_receive(:https_get) do |host, spouse_path, params, headers| params[:access_token].should eq(token) params[:fields].should eq('first_name,last_name,name,id,gender,birthday,picture') spouse_response end facebook.should_receive(:https_get) do |host, path, params, headers| params[:access_token].should eq(token) params[:fields].should eq('first_name,last_name,name,id,gender,birthday,picture,relationship') contacts_as_json end.exactly(1).times facebook.should_receive(:https_get) do |host, path, params, headers| params[:access_token].should eq(token) params[:fields].should eq('first_name,last_name,name,id,gender,birthday,picture') contacts_as_json end.exactly(1).times facebook.fetch_contacts_using_access_token token, token_type end it "should correctly parse id, name,email,gender, birthday, profile picture and relation" do 1.times { facebook.should_receive(:https_get).and_return(self_response) } 1.times { facebook.should_receive(:https_get) } 2.times { facebook.should_receive(:https_get).and_return(contacts_as_json) } result = facebook.fetch_contacts_using_access_token token, token_type result.size.should be(1) result.first[:id].should eq('608061886') result.first[:first_name].should eq('John') result.first[:last_name].should eq('Smith') result.first[:name].should eq('John Smith') result.first[:email].should be_nil result.first[:gender].should eq('male') result.first[:birthday].should eq({:day=>21, :month=>06, :year=>nil}) result.first[:profile_picture].should eq('https://graph.facebook.com/608061886/picture') result.first[:relation].should eq('cousin') end it "should correctly parse and set logged in user information" do 1.times { facebook.should_receive(:https_get).and_return(self_response) } 1.times { facebook.should_receive(:https_get) } 2.times { facebook.should_receive(:https_get).and_return(contacts_as_json) } facebook.fetch_contacts_using_access_token token, token_type user = facebook.instance_variable_get(:@env)["omnicontacts.user"] user.should_not be_nil user[:id].should eq("543216789") user[:first_name].should eq("Chris") user[:last_name].should eq("Johnson") user[:name].should eq("Chris Johnson") user[:email].should eq("chrisjohnson@gmail.com") user[:gender].should eq("male") user[:birthday].should eq({:day=>21, :month=>06, :year=>1982}) user[:profile_picture].should eq("https://graph.facebook.com/543216789/picture") end end end ================================================ FILE: spec/omnicontacts/importer/gmail_spec.rb ================================================ require "spec_helper" require "omnicontacts/importer/gmail" describe OmniContacts::Importer::Gmail do let(:gmail) { OmniContacts::Importer::Gmail.new({}, "client_id", "client_secret") } let(:gmail_with_scope_args) { OmniContacts::Importer::Gmail.new( {}, "client_id", "client_secret", { scope: %w( https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile ).join(" ") } ) } let(:self_response) { '{ "id":"16482944006464829443", "email":"chrisjohnson@gmail.com", "name":"Chris Johnson", "given_name":"Chris", "family_name":"Johnson", "picture":"https://lh3.googleusercontent.com/-b8aFbTBM/AAAAAAI/IWA/vsek/photo.jpg", "gender":"male", "birthday":"1982-06-21" }' } let(:contacts_as_json) { '{"version":"1.0","encoding":"UTF-8", "feed":{ "xmlns":"http://www.w3.org/2005/Atom", "xmlns$openSearch":"http://a9.com/-/spec/opensearch/1.1/", "xmlns$gContact":"http://schemas.google.com/contact/2008", "xmlns$batch":"http://schemas.google.com/gdata/batch", "xmlns$gd":"http://schemas.google.com/g/2005", "gd$etag":"W/\"C0YHRno7fSt7I2A9WhBSQ0Q.\"", "id":{"$t":"logged_in_user@gmail.com"}, "updated":{"$t":"2013-02-20T20:12:17.405Z"}, "category":[{ "scheme":"http://schemas.google.com/g/2005#kind", "term":"http://schemas.google.com/contact/2008#contact" }], "title":{"$t":"Users\'s Contacts"}, "link":[ {"rel":"http://schemas.google.com/contacts/2008/rel#photo","type":"image/*", "href":"https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/6b41d030b05abc","gd$etag":"\"VSxuN0cISit7I2A1UVUSdy12KHwgBFkE333.\""}, {"rel":"alternate","type":"text/html","href":"http://www.google.com/"}, {"rel":"http://schemas.google.com/g/2005#feed","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full"}, {"rel":"http://schemas.google.com/g/2005#post","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full"}, {"rel":"http://schemas.google.com/g/2005#batch","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/batch"}, {"rel":"self","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full?alt\u003djson\u0026max-results\u003d1"}, {"rel":"next","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full?alt\u003djson\u0026start-index\u003d2\u0026max-results\u003d1"} ], "author":[{"name":{"$t":"Edward"},"email":{"$t":"logged_in_user@gmail.com"}}], "generator":{"version":"1.0","uri":"http://www.google.com/m8/feeds","$t":"Contacts"}, "openSearch$totalResults":{"$t":"1007"}, "openSearch$startIndex":{"$t":"1"}, "openSearch$itemsPerPage":{"$t":"1"}, "entry":[ { "gd$etag":"\"R3oyfDVSLyt7I2A9WhBTSEULRA0.\"", "id":{"$t":"http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1"}, "updated":{"$t":"2013-02-14T22:36:36.494Z"}, "app$edited":{"xmlns$app":"http://www.w3.org/2007/app","$t":"2013-02-14T22:36:36.494Z"}, "category":[{"scheme":"http://schemas.google.com/g/2005#kind","term":"http://schemas.google.com/contact/2008#contact"}], "title":{"$t":"Edward Bennet"}, "link":[ {"rel":"http://schemas.google.com/contacts/2008/rel#photo","type":"image/*", "href":"https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/6b41d030b05abc", "gd$etag":"\"VSxuN0cISit7I2A1UVUSdy12KHwgBFkE333.\""}, {"rel":"self","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"}, {"rel":"edit","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"} ], "gd$name":{ "gd$fullName":{"$t":"Edward Bennet"}, "gd$givenName":{"$t":"Edward"}, "gd$familyName":{"$t":"Bennet"} }, "gd$organization":[{"rel":"http://schemas.google.com/g/2005#other","gd$orgName":{"$t":"Google"},"gd$orgTitle":{"$t":"Master Developer"}}], "gContact$birthday":{"when":"1954-07-02"}, "gContact$relation":{"rel":"father"}, "gContact$gender":{"value":"male"}, "gContact$event":[{"rel":"anniversary","gd$when":{"startTime":"1983-04-21"}},{"label":"New Job","gd$when":{"startTime":"2014-12-01"}}], "gd$email":[{"rel":"http://schemas.google.com/g/2005#other","address":"bennet@gmail.com","primary":"true"}], "gContact$groupMembershipInfo":[{"deleted":"false","href":"http://www.google.com/m8/feeds/groups/logged_in_user%40gmail.com/base/6"}], "gd$structuredPostalAddress":[{"rel":"http://schemas.google.com/g/2005#home","gd$formattedAddress":{"$t":"1313 Trashview Court\nApt. 13\nNowheresville, OK 66666"},"gd$street":{"$t":"1313 Trashview Court\nApt. 13"},"gd$postcode":{"$t":"66666"},"gd$country":{"code":"VA","$t":"Valoran"},"gd$city":{"$t":"Nowheresville"},"gd$region":{"$t":"OK"}}], "gd$phoneNumber":[{"rel":"http://schemas.google.com/g/2005#mobile","uri":"tel:+34-653-15-76-88","$t":"653157688"}] }, { "gd$etag":"\"R3oyfDVSLyt7I2A9WhBTSEULRA0.\"", "id":{"$t":"http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1"}, "updated":{"$t":"2013-02-15T22:36:36.494Z"}, "app$edited":{"xmlns$app":"http://www.w3.org/2007/app","$t":"2013-02-15T22:36:36.494Z"}, "category":[{"scheme":"http://schemas.google.com/g/2005#kind","term":"http://schemas.google.com/contact/2008#contact"}], "title":{"$t":"Emilia Fox"}, "link":[ {"rel":"http://schemas.google.com/contacts/2008/rel#photo","type":"image/*","href":"https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/1"}, {"rel":"self","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"}, {"rel":"edit","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"} ], "gd$name":{ "gd$fullName":{"$t":"Emilia Fox"}, "gd$givenName":{"$t":"Emilia"}, "gd$familyName":{"$t":"Fox"} }, "gContact$birthday":{"when":"1974-02-10"}, "gContact$relation":[{"rel":"spouse"}], "gContact$gender":{"value":"female"}, "gd$email":[{"rel":"http://schemas.google.com/g/2005#other","address":"emilia.fox@gmail.com","primary":"true"}], "gContact$groupMembershipInfo":[{"deleted":"false","href":"http://www.google.com/m8/feeds/groups/logged_in_user%40gmail.com/base/6"}] }] } }' } describe "fetch_contacts_using_access_token" do let(:token) { "token" } let(:token_type) { "token_type" } before(:each) do gmail.instance_variable_set(:@env, {}) gmail_with_scope_args.instance_variable_set(:@env, {}) end it "should request the contacts by specifying version and code in the http headers" do gmail.should_receive(:https_get) do |host, path, params, headers| headers["GData-Version"].should eq("3.0") headers["Authorization"].should eq("#{token_type} #{token}") self_response end gmail.should_receive(:https_get) do |host, path, params, headers| headers["GData-Version"].should eq("3.0") headers["Authorization"].should eq("#{token_type} #{token}") contacts_as_json end gmail.fetch_contacts_using_access_token token, token_type gmail.scope.should eq "https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile" gmail_with_scope_args.scope.should eq "https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/userinfo#email https://www.googleapis.com/auth/userinfo.profile" end it "should correctly parse id, name, email, gender, birthday, profile picture and relation for 1st contact" do gmail.should_receive(:https_get) gmail.should_receive(:https_get).and_return(contacts_as_json) result = gmail.fetch_contacts_using_access_token token, token_type result.size.should be(2) result.first[:id].should eq('http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1') result.first[:first_name].should eq('Edward') result.first[:last_name].should eq('Bennet') result.first[:name].should eq("Edward Bennet") result.first[:email].should eq("bennet@gmail.com") result.first[:gender].should eq("male") result.first[:birthday].should eq({ :day => 02, :month => 07, :year => 1954 }) result.first[:relation].should eq('father') result.first[:profile_picture].should eq("https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/6b41d030b05abc?&access_token=token") result.first[:dates][0][:name].should eq("anniversary") end it "should correctly parse id, name, email, gender, birthday, profile picture, snailmail address, phone and relation for 2nd contact" do gmail.should_receive(:https_get) gmail.should_receive(:https_get).and_return(contacts_as_json) result = gmail.fetch_contacts_using_access_token token, token_type result.size.should be(2) result.last[:id].should eq('http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1') result.last[:first_name].should eq('Emilia') result.last[:last_name].should eq('Fox') result.last[:name].should eq("Emilia Fox") result.last[:email].should eq("emilia.fox@gmail.com") result.last[:gender].should eq("female") result.last[:birthday].should eq({ :day => 10, :month => 02, :year => 1974 }) result.last[:profile_picture].should be_nil result.last[:relation].should eq('spouse') result.first[:address_1].should eq('1313 Trashview Court') result.first[:address_2].should eq('Apt. 13') result.first[:city].should eq('Nowheresville') result.first[:region].should eq('OK') result.first[:country].should eq('VA') result.first[:postcode].should eq('66666') result.first[:phone_number].should eq('653157688') end it "should correctly parse and set logged in user information" do gmail.should_receive(:https_get).and_return(self_response) gmail.should_receive(:https_get).and_return(contacts_as_json) gmail.fetch_contacts_using_access_token token, token_type user = gmail.instance_variable_get(:@env)["omnicontacts.user"] user.should_not be_nil user[:id].should eq("16482944006464829443") user[:first_name].should eq("Chris") user[:last_name].should eq("Johnson") user[:name].should eq("Chris Johnson") user[:email].should eq("chrisjohnson@gmail.com") user[:gender].should eq("male") user[:birthday].should eq({ :day => 21, :month => 06, :year => 1982 }) user[:profile_picture].should eq("https://lh3.googleusercontent.com/-b8aFbTBM/AAAAAAI/IWA/vsek/photo.jpg") end context "when address_1 is nil" do let(:contacts_as_json) { '{"version":"1.0","encoding":"UTF-8", "feed":{ "xmlns":"http://www.w3.org/2005/Atom", "xmlns$openSearch":"http://a9.com/-/spec/opensearch/1.1/", "xmlns$gContact":"http://schemas.google.com/contact/2008", "xmlns$batch":"http://schemas.google.com/gdata/batch", "xmlns$gd":"http://schemas.google.com/g/2005", "gd$etag":"W/\"C0YHRno7fSt7I2A9WhBSQ0Q.\"", "id":{"$t":"logged_in_user@gmail.com"}, "updated":{"$t":"2013-02-20T20:12:17.405Z"}, "category":[{ "scheme":"http://schemas.google.com/g/2005#kind", "term":"http://schemas.google.com/contact/2008#contact" }], "title":{"$t":"Users\'s Contacts"}, "link":[ {"rel":"http://schemas.google.com/contacts/2008/rel#photo","type":"image/*", "href":"https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/6b41d030b05abc","gd$etag":"\"VSxuN0cISit7I2A1UVUSdy12KHwgBFkE333.\""}, {"rel":"alternate","type":"text/html","href":"http://www.google.com/"}, {"rel":"http://schemas.google.com/g/2005#feed","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full"}, {"rel":"http://schemas.google.com/g/2005#post","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full"}, {"rel":"http://schemas.google.com/g/2005#batch","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/batch"}, {"rel":"self","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full?alt\u003djson\u0026max-results\u003d1"}, {"rel":"next","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full?alt\u003djson\u0026start-index\u003d2\u0026max-results\u003d1"} ], "author":[{"name":{"$t":"Edward"},"email":{"$t":"logged_in_user@gmail.com"}}], "generator":{"version":"1.0","uri":"http://www.google.com/m8/feeds","$t":"Contacts"}, "openSearch$totalResults":{"$t":"1007"}, "openSearch$startIndex":{"$t":"1"}, "openSearch$itemsPerPage":{"$t":"1"}, "entry":[ { "gd$etag":"\"R3oyfDVSLyt7I2A9WhBTSEULRA0.\"", "id":{"$t":"http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1"}, "updated":{"$t":"2013-02-14T22:36:36.494Z"}, "app$edited":{"xmlns$app":"http://www.w3.org/2007/app","$t":"2013-02-14T22:36:36.494Z"}, "category":[{"scheme":"http://schemas.google.com/g/2005#kind","term":"http://schemas.google.com/contact/2008#contact"}], "title":{"$t":"Edward Bennet"}, "link":[ {"rel":"http://schemas.google.com/contacts/2008/rel#photo","type":"image/*", "href":"https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/6b41d030b05abc", "gd$etag":"\"VSxuN0cISit7I2A1UVUSdy12KHwgBFkE333.\""}, {"rel":"self","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"}, {"rel":"edit","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"} ], "gd$name":{ "gd$fullName":{"$t":"Edward Bennet"}, "gd$givenName":{"$t":"Edward"}, "gd$familyName":{"$t":"Bennet"} }, "gd$organization":[{"rel":"http://schemas.google.com/g/2005#other","gd$orgName":{"$t":"Google"},"gd$orgTitle":{"$t":"Master Developer"}}], "gContact$birthday":{"when":"1954-07-02"}, "gContact$relation":{"rel":"father"}, "gContact$gender":{"value":"male"}, "gContact$event":[{"rel":"anniversary","gd$when":{"startTime":"1983-04-21"}},{"label":"New Job","gd$when":{"startTime":"2014-12-01"}}], "gd$email":[{"rel":"http://schemas.google.com/g/2005#other","address":"bennet@gmail.com","primary":"true"}], "gContact$groupMembershipInfo":[{"deleted":"false","href":"http://www.google.com/m8/feeds/groups/logged_in_user%40gmail.com/base/6"}], "gd$structuredPostalAddress":[{"rel":"http://schemas.google.com/g/2005#home","gd$formattedAddress":{},"gd$street":{},"gd$postcode":{"$t":"66666"},"gd$country":{"code":"VA","$t":"Valoran"},"gd$city":{"$t":"Nowheresville"},"gd$region":{"$t":"OK"}}], "gd$phoneNumber":[{"rel":"http://schemas.google.com/g/2005#mobile","uri":"tel:+34-653-15-76-88","$t":"653157688"}] }, { "gd$etag":"\"R3oyfDVSLyt7I2A9WhBTSEULRA0.\"", "id":{"$t":"http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1"}, "updated":{"$t":"2013-02-15T22:36:36.494Z"}, "app$edited":{"xmlns$app":"http://www.w3.org/2007/app","$t":"2013-02-15T22:36:36.494Z"}, "category":[{"scheme":"http://schemas.google.com/g/2005#kind","term":"http://schemas.google.com/contact/2008#contact"}], "title":{"$t":"Emilia Fox"}, "link":[ {"rel":"http://schemas.google.com/contacts/2008/rel#photo","type":"image/*","href":"https://www.google.com/m8/feeds/photos/media/logged_in_user%40gmail.com/1"}, {"rel":"self","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"}, {"rel":"edit","type":"application/atom+xml","href":"https://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/full/1"} ], "gd$name":{ "gd$fullName":{"$t":"Emilia Fox"}, "gd$givenName":{"$t":"Emilia"}, "gd$familyName":{"$t":"Fox"} }, "gContact$birthday":{"when":"1974-02-10"}, "gContact$relation":[{"rel":"spouse"}], "gContact$gender":{"value":"female"}, "gd$email":[{"rel":"http://schemas.google.com/g/2005#other","address":"emilia.fox@gmail.com","primary":"true"}], "gContact$groupMembershipInfo":[{"deleted":"false","href":"http://www.google.com/m8/feeds/groups/logged_in_user%40gmail.com/base/6"}] }] } }' } it "should correctly parse id, name, email, gender, birthday, profile picture, snailmail address, phone and relation for 2nd contact" do gmail.should_receive(:https_get) gmail.should_receive(:https_get).and_return(contacts_as_json) result = gmail.fetch_contacts_using_access_token token, token_type result.size.should be(2) result.last[:id].should eq('http://www.google.com/m8/feeds/contacts/logged_in_user%40gmail.com/base/1') result.last[:first_name].should eq('Emilia') result.last[:last_name].should eq('Fox') result.last[:name].should eq("Emilia Fox") result.last[:email].should eq("emilia.fox@gmail.com") result.last[:gender].should eq("female") result.last[:birthday].should eq({ :day => 10, :month => 02, :year => 1974 }) result.last[:profile_picture].should be_nil result.last[:relation].should eq('spouse') result.first[:address_1].should eq(nil) result.first[:address_2].should eq(nil) result.first[:city].should eq('Nowheresville') result.first[:region].should eq('OK') result.first[:country].should eq('VA') result.first[:postcode].should eq('66666') result.first[:phone_number].should eq('653157688') end end end end ================================================ FILE: spec/omnicontacts/importer/hotmail_spec.rb ================================================ require "spec_helper" require "omnicontacts/importer/hotmail" describe OmniContacts::Importer::Hotmail do let(:permissions) { "perm1, perm2" } let(:hotmail) { OmniContacts::Importer::Hotmail.new({}, "client_id", "client_secret", {:permissions => permissions}) } let(:self_response) { '{ "id": "4502de12390223d0", "name": "Chris Johnson", "first_name": "Chris", "last_name": "Johnson", "birth_day": 21, "birth_month": 6, "birth_year": 1982, "gender": null, "emails": {"preferred":"chrisjohnson@gmail.com", "account":"chrisjohn@gmail.com", "personal":null, "business":null} }' } let(:contacts_as_json) { '{ "data": [ { "id": "contact.7fac34bb000000000000000000000000", "first_name": "John", "last_name": "Smith", "name": "John Smith", "gender": null, "user_id": "123456", "is_friend": false, "is_favorite": false, "birth_day": 5, "birth_month": 6, "birth_year":1952, "email_hashes":["1234567890"] } ]}' } describe "fetch_contacts_using_access_token" do let(:token) { "token" } let(:token_type) { "token_type" } before(:each) do hotmail.instance_variable_set(:@env, {"HTTP_HOST" => "http://example.com"}) end it "should request the contacts by providing the token in the url" do hotmail.should_receive(:https_get) do |host, path, params, headers| params[:access_token].should eq(token) self_response end hotmail.should_receive(:https_get) do |host, path, params, headers| params[:access_token].should eq(token) contacts_as_json end hotmail.fetch_contacts_using_access_token token, token_type end it "should set requested permissions in the authorization url" do hotmail.authorization_url.should match(/scope=#{Regexp.quote(CGI.escape(permissions))}/) end it "should correctly parse id, name, email, gender, birthday, profile picture, relation and email hashes" do hotmail.should_receive(:https_get).and_return(self_response) hotmail.should_receive(:https_get).and_return(contacts_as_json) result = hotmail.fetch_contacts_using_access_token token, token_type result.size.should be(1) result.first[:id].should eq('123456') result.first[:first_name].should eq("John") result.first[:last_name].should eq('Smith') result.first[:name].should eq("John Smith") result.first[:email].should be_nil result.first[:gender].should be_nil result.first[:birthday].should eq({:day=>5, :month=>6, :year=>1952}) result.first[:profile_picture].should eq('https://apis.live.net/v5.0/123456/picture') result.first[:relation].should be_nil result.first[:email_hashes].should eq(["1234567890"]) end it "should correctly parse and set logged in user information" do hotmail.should_receive(:https_get).and_return(self_response) hotmail.should_receive(:https_get).and_return(contacts_as_json) hotmail.fetch_contacts_using_access_token token, token_type user = hotmail.instance_variable_get(:@env)["omnicontacts.user"] user.should_not be_nil user[:id].should eq('4502de12390223d0') user[:first_name].should eq('Chris') user[:last_name].should eq('Johnson') user[:name].should eq('Chris Johnson') user[:gender].should be_nil user[:birthday].should eq({:day=>21, :month=>06, :year=>1982}) user[:email].should eq('chrisjohn@gmail.com') user[:profile_picture].should eq('https://apis.live.net/v5.0/4502de12390223d0/picture') end end end ================================================ FILE: spec/omnicontacts/importer/linkedin_spec.rb ================================================ require "spec_helper" require "omnicontacts/importer/linkedin" describe OmniContacts::Importer::Linkedin do let(:linkedin) { OmniContacts::Importer::Linkedin.new({}, "client_id", "client_secret", state: "ipsaeumeaque") } let(:contacts_as_json) do "{ \n \"_total\": 2, \n \"values\": [ \n { \n \"firstName\": \"Adolf\", \n \"id\": \"k71S5q6MKe\", \n \"lastName\": \"Witting\", \n \"pictureUrl\": \"https://media.licdn.com/mpr/mprx/0_mLnj-7szw130pFRLB8Op7-p1Sxoyv53U3B47Scp1Sxoyv53U3B47Scp1Sxoyv53U3B47Sc\"\n }, \n { \n \"firstName\": \"Emmet\", \n \"id\": \"ms5r3lI3J2\", \n \"lastName\": \"Little\", \n \"pictureUrl\": \"https://media.licdn.com/mpr/mprx/0_iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6\"\n } ]\n }" end describe "fetch_contacts_using_access_token" do let(:token) { "token" } let(:token_type) { nil } before(:each) do linkedin.instance_variable_set(:@env, {}) end it "should request the contacts by specifying code in the http headers" do linkedin.should_receive(:https_get) do |host, path, params, headers| headers["Authorization"].should eq("Bearer #{token}") contacts_as_json end linkedin.fetch_contacts_using_access_token token, token_type end it "should correctly parse id, name, and profile picture for 1st contact" do linkedin.should_receive(:https_get).and_return(contacts_as_json) result = linkedin.fetch_contacts_using_access_token token, token_type result.size.should be(2) result.first[:id].should eq('k71S5q6MKe') result.first[:first_name].should eq('Adolf') result.first[:last_name].should eq('Witting') result.first[:name].should eq("Adolf Witting") result.first[:profile_picture].should eq("https://media.licdn.com/mpr/mprx/0_mLnj-7szw130pFRLB8Op7-p1Sxoyv53U3B47Scp1Sxoyv53U3B47Scp1Sxoyv53U3B47Sc") end it "should correctly parse id, name, and profile picture for 2nd contact" do linkedin.should_receive(:https_get).and_return(contacts_as_json) result = linkedin.fetch_contacts_using_access_token token, token_type result.size.should be(2) result.last[:id].should eq('ms5r3lI3J2') result.last[:first_name].should eq('Emmet') result.last[:last_name].should eq('Little') result.last[:name].should eq("Emmet Little") result.last[:profile_picture].should eq("https://media.licdn.com/mpr/mprx/0_iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6iH9m158zCdISt1X6") end end end ================================================ FILE: spec/omnicontacts/importer/outlook_spec.rb ================================================ require "spec_helper" require "omnicontacts/importer/outlook" describe OmniContacts::Importer::Outlook do let(:permissions) { "Contacts.Read" } let(:outlook) { OmniContacts::Importer::Outlook.new({}, "app_id", "app_secret", {:permissions => permissions}) } let(:self_response) { '{ "@odata.context": "https://outlook.office.com/api/v2.0/$metadata#Me", "@odata.id": "https://outlook.office.com/api/v2.0/Users(\'00034001-df52-d3d5-0000-000000000000@84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa\')", "Id": "00034001-df52-d3d5-0000-000000000000@84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa", "EmailAddress": "test.user@outlook.com", "DisplayName": "Test User", "Alias": "puid-00034001DF52D3D5", "MailboxGuid": "00034001-df52-d3d5-0000-000000000000" }' } let(:contacts_as_json) { '{ "@odata.context": "https://outlook.office.com/api/v2.0/$metadata#Me/Contacts", "value": [{ "@odata.id": "https://outlook.office.com/api/v2.0/Users(\'00034001-df52-d3d5-0000-000000000000@84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa\')/Contacts(\'AQMkADAwATM0MDAAMS1kZjUyLWQzZDUtMDACLTAwCgBGAAADQ1hAWLJpwk6DZYyOhnclvgcAxCL3G7jnpkiRUVmiNrhjJgAAAgEOAAAAxCL3G7jnpkiRUVmiNrhjJgAAAixsAAAA\')", "@odata.etag": "W/\"EQAAABYAAADEIvcbuOemSJFRWaI2uGMmAAAAlo4t\"", "Id": "AQMkADAwATM0MDAAMS1kZjUyLWQzZDUtMDACLTAwCgBGAAADQ1hAWLJpwk6DZYyOhnclvgcAxCL3G7jnpkiRUVmiNrhjJgAAAgEOAAAAxCL3G7jnpkiRUVmiNrhjJgAAAixsAAAA", "CreatedDateTime": "2016-04-13T21:25:24Z", "LastModifiedDateTime": "2016-04-14T19:36:55Z", "ChangeKey": "EQAAABYAAADEIvcbuOemSJFRWaI2uGMmAAAAlo4t", "Categories": [], "ParentFolderId": "AQMkADAwATM0MDAAMS1kZjUyLWQzZDUtMDACLTAwCgAuAAADQ1hAWLJpwk6DZYyOhnclvgEAxCL3G7jnpkiRUVmiNrhjJgAAAgEOAAAA", "Birthday": "1604-08-14T00:00:00Z", "FileAs": "Contact, First", "DisplayName": "First Contact", "GivenName": "First", "Initials": null, "MiddleName": null, "NickName": null, "Surname": "Contact", "Title": null, "YomiGivenName": null, "YomiSurname": null, "YomiCompanyName": null, "Generation": null, "EmailAddresses": [{ "Name": "contact.first@email.com", "Address": "contact.first@email.com" }, { "Name": "contact.second@email.com", "Address": "contact.second@email.com" }], "ImAddresses": [], "JobTitle": null, "CompanyName": null, "Department": null, "OfficeLocation": null, "Profession": null, "BusinessHomePage": null, "AssistantName": null, "Manager": null, "HomePhones": [], "MobilePhone1": null, "BusinessPhones": [], "HomeAddress": { "Street": "address1", "City": "city", "State": "state", "CountryOrRegion": "US", "PostalCode": "89111" }, "BusinessAddress": {}, "OtherAddress": {}, "SpouseName": null, "PersonalNotes": null, "Children": [] }] }' } describe "fetch_contacts_using_access_token" do let(:access_token) { "access_token" } let(:token_type) { "token_type" } before(:each) do outlook.instance_variable_set(:@env, {"HTTP_HOST" => "http://example.com"}) end it "should request the contacts by providing the authorization header with token_type and access_token" do outlook.should_receive(:https_get) do |host, path, params, headers| params.should eq({}) headers["Authorization"].should eq("token_type access_token") self_response end outlook.should_receive(:https_get) do |host, path, params, headers| params.should eq({}) headers["Authorization"].should eq("token_type access_token") contacts_as_json end outlook.fetch_contacts_using_access_token access_token, token_type end it "should set requested permissions in the authorization url" do outlook.authorization_url.should match(/scope=#{Regexp.quote(CGI.escape(permissions))}/) end it "should correctly parse id, name and email" do outlook.should_receive(:https_get).and_return(self_response) outlook.should_receive(:https_get).and_return(contacts_as_json) result = outlook.fetch_contacts_using_access_token access_token, token_type result.size.should be(1) result.first[:id].should eq('AQMkADAwATM0MDAAMS1kZjUyLWQzZDUtMDACLTAwCgBGAAADQ1hAWLJpwk6DZYyOhnclvgcAxCL3G7jnpkiRUVmiNrhjJgAAAgEOAAAAxCL3G7jnpkiRUVmiNrhjJgAAAixsAAAA') result.first[:first_name].should eq("First") result.first[:last_name].should eq("Contact") result.first[:name].should eq("First Contact") result.first[:email].should eq("contact.first@email.com") result.first[:birthday].should eq({ :day => 14, :month => 8, :year => nil }) result.first[:address_1].should eq("address1") result.first[:address_2].should be_nil result.first[:city].should eq("city") result.first[:region].should eq("state") result.first[:postcode].should eq("89111") result.first[:country].should eq("US") result.first[:gender].should be_nil result.first[:profile_picture].should be_nil result.first[:relation].should be_nil end it "should correctly parse and set logged in user information" do outlook.should_receive(:https_get).and_return(self_response) outlook.should_receive(:https_get).and_return(contacts_as_json) outlook.fetch_contacts_using_access_token access_token, token_type user = outlook.instance_variable_get(:@env)["omnicontacts.user"] user.should_not be_nil user[:id].should eq('00034001-df52-d3d5-0000-000000000000@84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa') user[:first_name].should eq("Test") user[:last_name].should eq("User") user[:name].should eq("Test User") user[:email].should eq("test.user@outlook.com") user[:gender].should be_nil user[:birthday].should be_nil end end end ================================================ FILE: spec/omnicontacts/importer/yahoo_spec.rb ================================================ require "spec_helper" require "omnicontacts/importer/yahoo" describe OmniContacts::Importer::Yahoo do describe "fetch_contacts_from_token_and_verifier" do let(:self_response) { '{"profile":{ "guid":"PCLASP523T3E2R5TFMHDW9KWQQ", "birthdate": "06/21", "emails":[{"handle":"chrisjohnson@gmail.com", "id":10, "primary":true, "type":"HOME"}, {"handle":"xyz@xyz.com", "id":11, "type":"HOME"}], "familyName": "Johnson", "gender":"M", "givenName":"Chris", "image":{"imageUrl":"https://avatars.zenfs.com/users/23T3E2R5TFMHDW-AFE-I7lUpIsGQ==.large.png"} } }' } let(:contacts_as_json) { '{ "contacts": { "start":1, "count":1, "contact":[ { "id":10, "fields":[ {"id":819, "type":"email", "value":"johnny@yahoo.com"}, {"id":806,"type":"name","value":{"givenName":"John","middleName":"","familyName":"Smith"},"editedBy":"OWNER","categories":[]}, {"id":33555343,"type":"guid","value":"7ET6MYV2UQ6VR6CBSNMCLFJIVI"}, {"id":946,"type":"birthday","value":{"day":"22","month":"2","year":"1952"},"editedBy":"OWNER","categories":[]}, {"id":21, "type":"address", "value":{"street":"1313 Trashview Court\nApt. 13", "city":"Nowheresville", "stateOrProvince":"OK", "postalCode":"66666", "country":"", "countryCode":""}, "editedBy":"OWNER", "flags":["HOME"], "categories":[]} ] } ] } }' } let(:yahoo) { OmniContacts::Importer::Yahoo.new({}, "consumer_key", "consumer_secret") } before(:each) do yahoo.instance_variable_set(:@env, {}) end it "should request the contacts by specifying all required parameters" do yahoo.should_receive(:fetch_access_token).and_return(["access_token", "access_token_secret", "guid"]) yahoo.should_receive(:https_get) do |host, path, params| params[:format].should eq("json") params[:oauth_consumer_key].should eq("consumer_key") params[:oauth_nonce].should_not be_nil params[:oauth_signature_method].should eq("HMAC-SHA1") params[:oauth_timestamp].should_not be_nil params[:oauth_token].should eq("access_token") params[:oauth_version].should eq("1.0") self_response end yahoo.should_receive(:https_get) do |host, path, params| params[:format].should eq("json") params[:oauth_consumer_key].should eq("consumer_key") params[:oauth_nonce].should_not be_nil params[:oauth_signature_method].should eq("HMAC-SHA1") params[:oauth_timestamp].should_not be_nil params[:oauth_token].should eq("access_token") params[:oauth_version].should eq("1.0") contacts_as_json end yahoo.fetch_contacts_from_token_and_verifier "auth_token", "auth_token_secret", "oauth_verifier" end it "should correctly parse id, name,email,gender, birthday, snailmail address, image source and relation for contact and logged in user" do yahoo.should_receive(:fetch_access_token).and_return(["access_token", "access_token_secret", "guid"]) yahoo.should_receive(:https_get).and_return(self_response) yahoo.should_receive(:https_get).and_return(contacts_as_json) result = yahoo.fetch_contacts_from_token_and_verifier "auth_token", "auth_token_secret", "oauth_verifier" result.size.should be(1) result.first[:id].should eq('10') result.first[:first_name].should eq('John') result.first[:last_name].should eq('Smith') result.first[:name].should eq("John Smith") result.first[:email].should eq("johnny@yahoo.com") result.first[:gender].should be_nil result.first[:birthday].should eq({:day=>22, :month=>2, :year=>1952}) result.first[:address_1].should eq('1313 Trashview Court') result.first[:address_2].should eq('Apt. 13') result.first[:city].should eq('Nowheresville') result.first[:region].should eq('OK') result.first[:postcode].should eq('66666') result.first[:relation].should be_nil end it "should return an empty list of contacts" do empty_contacts_list = '{"contacts": {"start":0, "count":0}}' yahoo.should_receive(:fetch_access_token).and_return(["access_token", "access_token_secret", "guid"]) yahoo.should_receive(:https_get).and_return(self_response) yahoo.should_receive(:https_get).and_return(empty_contacts_list) result = yahoo.fetch_contacts_from_token_and_verifier "auth_token", "auth_token_secret", "oauth_verifier" result.should be_empty end it "should correctly parse and set logged in user information" do yahoo.should_receive(:fetch_access_token).and_return(["access_token", "access_token_secret", "guid"]) yahoo.should_receive(:https_get).and_return(self_response) yahoo.should_receive(:https_get).and_return(contacts_as_json) yahoo.fetch_contacts_from_token_and_verifier "auth_token", "auth_token_secret", "oauth_verifier" user = yahoo.instance_variable_get(:@env)["omnicontacts.user"] user.should_not be_nil user[:id].should eq('PCLASP523T3E2R5TFMHDW9KWQQ') user[:first_name].should eq('Chris') user[:last_name].should eq('Johnson') user[:name].should eq('Chris Johnson') user[:gender].should eq('male') user[:birthday].should eq({:day=>21, :month=>06, :year=>nil}) user[:email].should eq('chrisjohnson@gmail.com') user[:profile_picture].should eq('https://avatars.zenfs.com/users/23T3E2R5TFMHDW-AFE-I7lUpIsGQ==.large.png') end end end ================================================ FILE: spec/omnicontacts/integration_test_spec.rb ================================================ require "spec_helper" require "omnicontacts/integration_test" describe IntegrationTest do context "mock_initial_request" do it "should redirect to the provider's redirect_path" do provider = mock redirect_path = "/redirect_path" provider.stub(:redirect_path => redirect_path) IntegrationTest.instance.mock_authorization_from_user(provider)[1]["location"].should eq(redirect_path) end end context "mock_callback" do before(:each) { @env = {} @provider = self.mock @provider.stub(:class_name => "test") IntegrationTest.instance.clear_mocks } it "should return an empty contacts list" do IntegrationTest.instance.mock_fetch_contacts(@provider).should be_empty end it "should return a configured list of contacts " do contacts = [:name => 'John Doe', :email => 'john@doe.com'] IntegrationTest.instance.mock('test', contacts) result = IntegrationTest.instance.mock_fetch_contacts(@provider) result.size.should be(1) result.first[:email].should eq(contacts.first[:email]) result.first[:name].should eq(contacts.first[:name]) end it "should return a single element list of contacts " do contact = {:name => 'John Doe', :email => 'john@doe.com'} IntegrationTest.instance.mock('test', contact) result = IntegrationTest.instance.mock_fetch_contacts(@provider) result.size.should be(1) result.first[:email].should eq(contact[:email]) result.first[:name].should eq(contact[:name]) end it "should return a user" do contact = {:name => 'John Doe', :email => 'john@doe.com'} user = {:name => 'Mary Smith', :email => 'mary@smith.com'} IntegrationTest.instance.mock('test', contact, user) result = IntegrationTest.instance.mock_fetch_user(@provider) result[:email].should eq(user[:email]) result[:name].should eq(user[:name]) end it "should throw an exception" do IntegrationTest.instance.mock('test', :some_error) expect {IntegrationTest.instance.mock_fetch_contacts(@provider)}.to raise_error end end end ================================================ FILE: spec/omnicontacts/middleware/base_oauth_spec.rb ================================================ require "spec_helper" require "omnicontacts" require "omnicontacts/middleware/base_oauth" describe OmniContacts::Middleware::BaseOAuth do before(:all) do class TestProvider < OmniContacts::Middleware::BaseOAuth def initialize app, consumer_key, consumer_secret, options = {} super app, options end def redirect_path "#{ MOUNT_PATH }testprovider/callback" end def self.mock_session @mock_session ||= {} end def session TestProvider.mock_session end end OmniContacts.integration_test.enabled = true end let(:app) { Rack::Builder.new do |b| b.use TestProvider, "consumer_id", "consumer_secret" b.run lambda { |env| [200, {"Content-Type" => "text/html"}, ["Hello World"]] } end.to_app } it "should return a preconfigured list of contacts" do OmniContacts.integration_test.mock(:testprovider, :email => "user@example.com") get "#{ MOUNT_PATH }testprovider" get "#{ MOUNT_PATH }testprovider/callback" last_request.env["omnicontacts.contacts"].first[:email].should eq("user@example.com") end it "should redirect to failure url" do OmniContacts.integration_test.mock(:testprovider, "some_error" ) get "#{ MOUNT_PATH }testprovider" get "#{MOUNT_PATH }testprovider/callback" last_response.should be_redirect last_response.headers["location"].should eq("#{ MOUNT_PATH }failure?error_message=internal_error&importer=testprovider") end it "should pass through state query params to the failure url" do OmniContacts.integration_test.mock(:testprovider, "some_error" ) get "#{MOUNT_PATH }testprovider/callback?state=/parent/resource/id" last_response.headers["location"].should eq("#{ MOUNT_PATH }failure?error_message=internal_error&importer=testprovider&state=/parent/resource/id") end it "should store request params in session" do OmniContacts.integration_test.mock(:testprovider, :email => "user@example.com") get "#{ MOUNT_PATH }testprovider?foo=bar" app.session['omnicontacts.params'].should eq({'foo' => 'bar'}) end it "should pass the params from session to callback environment " do OmniContacts.integration_test.mock(:testprovider, :email => "user@example.com") app.session.merge!({'omnicontacts.params' => {'foo' => 'bar'}}) get "#{MOUNT_PATH }testprovider/callback?state=/parent/resource/id" last_request.env["omnicontacts.params"].should eq({'foo' => 'bar'}) end it "should pass the params from session on failure" do OmniContacts.integration_test.mock(:testprovider, "some_error" ) get "#{ MOUNT_PATH }testprovider" app.session.merge!({'omnicontacts.params' => {'foo' => 'bar'}}) get "#{MOUNT_PATH }testprovider/callback" last_response.should be_redirect last_response.headers["location"].should be_include("foo=bar") end after(:all) do OmniContacts.integration_test.enabled = false OmniContacts.integration_test.clear_mocks end end ================================================ FILE: spec/omnicontacts/middleware/oauth1_spec.rb ================================================ require "spec_helper" require "omnicontacts/middleware/oauth1" describe OmniContacts::Middleware::OAuth1 do before(:all) do class OAuth1Middleware < OmniContacts::Middleware::OAuth1 def self.mock_auth_token_resp @mock_auth_token_resp ||= Object.new end def fetch_authorization_token OAuth1Middleware.mock_auth_token_resp.body end def authorization_url auth_token "http://www.example.com" end def fetch_contacts_from_token_and_verifier oauth_token, ouath_token_secret, oauth_verifier [{:name => "John Doe", :email => "john@example.com"}] end def self.mock_session @mock_session ||= {} end def session OAuth1Middleware.mock_session end end end let(:app) { Rack::Builder.new do |b| b.use OAuth1Middleware, "consumer_id", "consumer_secret" b.run lambda { |env| [200, {"Content-Type" => "text/html"}, ["Hello World"]] } end.to_app } context "visiting the listening path" do it "should save the authorization token and redirect to the authorization url" do OAuth1Middleware.mock_auth_token_resp.should_receive(:body).and_return(["auth_token", "auth_token_secret"]) get "#{ MOUNT_PATH }oauth1middleware" last_response.should be_redirect last_response.headers['location'].should eq("http://www.example.com") end it "should pass through state query params visiting the listening path" do OAuth1Middleware.mock_auth_token_resp.should_receive(:body).and_return(["auth_token", "auth_token_secret"]) get "#{ MOUNT_PATH }oauth1middleware?state=/parent/resource/id" last_response.headers['location'].should eq("http://www.example.com?state=/parent/resource/id") end it "should redirect to failure url if fetching the request token does not succeed" do OAuth1Middleware.mock_auth_token_resp.should_receive(:body).and_raise("Request failed") get "contacts/oauth1middleware" last_response.should be_redirect last_response.headers["location"].should eq("#{ MOUNT_PATH }failure?error_message=internal_error&importer=oauth1middleware") end end context "visiting the callback url after authorization" do it "should return the list of contacts" do OAuth1Middleware.mock_session.should_receive(:[]).and_return("oauth_token_secret") get "#{ MOUNT_PATH }oauth1middleware/callback?oauth_token=token&oauth_verifier=verifier" last_response.should be_ok last_request.env["omnicontacts.contacts"].size.should be(1) end it "should redirect to failure url if oauth_token_secret is not found in the session" do OAuth1Middleware.mock_session.should_receive(:[]).and_return(nil) get "#{ MOUNT_PATH }oauth1middleware/callback?oauth_token=token&oauth_verifier=verifier" last_response.should be_redirect last_response.headers["location"].should eq("#{ MOUNT_PATH }failure?error_message=not_authorized&importer=oauth1middleware") end end end ================================================ FILE: spec/omnicontacts/middleware/oauth2_spec.rb ================================================ require "spec_helper" require "omnicontacts/middleware/oauth2" describe OmniContacts::Middleware::OAuth2 do before(:all) do class OAuth2Middleware < OmniContacts::Middleware::OAuth2 def authorization_url "http://www.example.com" end def redirect_path "/redirect_path" end def self.mock_session @mock_session ||= {} end def session OAuth2Middleware.mock_session end def fetch_access_token code ["access_token", "token_type", "token_refresh"] end def fetch_contacts_using_access_token token, token_type [{:name => "John Doe", :email => "john@example.com"}] end end end let(:app) { Rack::Builder.new do |b| b.use OAuth2Middleware, "client_id", "client_secret" b.run lambda { |env| [200, {"Content-Type" => "text/html"}, ["Hello World"]] } end.to_app } context "visiting the listening path" do it "should redirect to authorization site when visiting the listening path" do get "#{ MOUNT_PATH }oauth2middleware" last_response.should be_redirect last_response.headers['location'].should eq("http://www.example.com") end it "should pass through state query params visiting the listening path" do get "#{ MOUNT_PATH }oauth2middleware?state=/parent/resource/id" last_response.headers['location'].should eq("http://www.example.com?state=/parent/resource/id") end end context "visiting the callback url after authorization" do it "should fetch the contacts" do get '/redirect_path?code=ABC' last_response.should be_ok last_request.env["omnicontacts.contacts"].size.should be(1) end it "should redirect to failure page because user did not allow access to contacts list" do get '/redirect_path?error=not_authorized' last_response.should be_redirect last_response.headers["location"].should eq("#{ MOUNT_PATH }failure?error_message=not_authorized&importer=oauth2middleware") end end end ================================================ FILE: spec/omnicontacts/parse_utils_spec.rb ================================================ require "spec_helper" require "omnicontacts/parse_utils" include OmniContacts::ParseUtils describe OmniContacts::ParseUtils do describe "normalize_name" do it "should remove trailing spaces" do result = normalize_name("John ") result.should eq("John") end it "should preserve capitalization" do result = normalize_name("John McDonald") result.should eq("John McDonald") end end describe "full_name" do it "should preserve capitalization" do result = full_name("John", "McDonald") result.should eq("John McDonald") end it "returns only first name if no last name present" do result = full_name("John", nil) result.should eq("John") end it "returns only last name if no first name present" do result = full_name(nil, "McDonald") result.should eq("McDonald") end end describe "birthday_format" do it "returns nil if (!year && !month) || (!year && !day)" do result = birthday_format(nil, Date.today, nil) result.should eq(nil) result = birthday_format(Date.today.month, nil, nil) result.should eq(nil) end end describe "email_to_name" do it "create a probable name from email" do username_or_email = "foo.bar@test.com" result = email_to_name(username_or_email) result.should eq ['foo','bar',"foo bar"] end end end ================================================ FILE: spec/spec_helper.rb ================================================ require "simplecov" SimpleCov.start do add_filter "spec/" end require "rspec" require "rack/test" RSpec.configure do |config| config.include Rack::Test::Methods end MOUNT_PATH = "/contacts/"