gmail-contacts 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +55 -0
- data/Rakefile +150 -0
- data/gmail-contacts.gemspec +56 -0
- data/lib/gmail-contacts/google.rb +306 -0
- data/lib/gmail-contacts.rb +48 -0
- metadata +60 -0
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
gmail-contacts
|
2
|
+
==============
|
3
|
+
|
4
|
+
A Ruby gem for getting a list of your Gmail contacts. Most of the code here has been extracted from
|
5
|
+
[mislav's contacts gem](https://github.com/mislav/contacts). It's been updated to work with Ruby 1.9
|
6
|
+
and Rails 3.
|
7
|
+
|
8
|
+
Installation
|
9
|
+
-------
|
10
|
+
|
11
|
+
gem install gmail-contacts
|
12
|
+
|
13
|
+
Usage
|
14
|
+
-----
|
15
|
+
Get a link to the authorization URL:
|
16
|
+
|
17
|
+
GmailContacts::Google.authentication_url("http://mysite.com/invites")
|
18
|
+
|
19
|
+
The user will be redirected to the URL you pass and a `token` parameter will be sent along. Capture
|
20
|
+
that token and then request the contacts:
|
21
|
+
|
22
|
+
GmailContacts::Google.new("some_token").contacts
|
23
|
+
|
24
|
+
Every `Contact` has a name and email fields:
|
25
|
+
|
26
|
+
GmailContacts::Google.new("some_token").contacts.each do |contact|
|
27
|
+
puts "#{contact.name}: #{contact.email}"
|
28
|
+
end
|
29
|
+
|
30
|
+
Usage with Rails
|
31
|
+
----
|
32
|
+
First create the authorization link in one of your views:
|
33
|
+
|
34
|
+
# app/views/invites/new.html.erb
|
35
|
+
<%= link_to "Invite your Gmail contacts", GmailContacts::Google.authentication_url("http://mysite.com/invites") %>
|
36
|
+
|
37
|
+
Then create a controller action that receives the token and fetches the contacts:
|
38
|
+
|
39
|
+
# config/routes.rb
|
40
|
+
match "/invites" => "invites#index"
|
41
|
+
|
42
|
+
# app/controllers/invites_controller.rb
|
43
|
+
class InvitesController
|
44
|
+
def index
|
45
|
+
token = params[:token]
|
46
|
+
@contacts = GmailContacts::Google.new("some_token").contacts
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Finally, iterate through the contacts in your view:
|
51
|
+
|
52
|
+
# app/views/invites/index.html.erb
|
53
|
+
<% @contacts.each do |contact| %>
|
54
|
+
<span><strong><%= contact.name %></strong>: <% contact.email %></span>
|
55
|
+
<% end %>
|
data/Rakefile
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
#############################################################################
|
6
|
+
#
|
7
|
+
# Helper functions
|
8
|
+
#
|
9
|
+
#############################################################################
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
17
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
18
|
+
end
|
19
|
+
|
20
|
+
def date
|
21
|
+
Date.today.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def rubyforge_project
|
25
|
+
name
|
26
|
+
end
|
27
|
+
|
28
|
+
def gemspec_file
|
29
|
+
"#{name}.gemspec"
|
30
|
+
end
|
31
|
+
|
32
|
+
def gem_file
|
33
|
+
"#{name}-#{version}.gem"
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace_header(head, header_name)
|
37
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
38
|
+
end
|
39
|
+
|
40
|
+
#############################################################################
|
41
|
+
#
|
42
|
+
# Standard tasks
|
43
|
+
#
|
44
|
+
#############################################################################
|
45
|
+
|
46
|
+
task :default => :test
|
47
|
+
|
48
|
+
require 'rake/testtask'
|
49
|
+
Rake::TestTask.new(:test) do |test|
|
50
|
+
test.libs << 'lib' << 'test'
|
51
|
+
test.pattern = 'test/**/test_*.rb'
|
52
|
+
test.verbose = true
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Generate RCov test coverage and open in your browser"
|
56
|
+
task :coverage do
|
57
|
+
require 'rcov'
|
58
|
+
sh "rm -fr coverage"
|
59
|
+
sh "rcov test/test_*.rb"
|
60
|
+
sh "open coverage/index.html"
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'rdoc/task'
|
64
|
+
Rake::RDocTask.new do |rdoc|
|
65
|
+
rdoc.rdoc_dir = 'rdoc'
|
66
|
+
rdoc.title = "#{name} #{version}"
|
67
|
+
rdoc.rdoc_files.include('README*')
|
68
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Open an irb session preloaded with this library"
|
72
|
+
task :console do
|
73
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
74
|
+
end
|
75
|
+
|
76
|
+
#############################################################################
|
77
|
+
#
|
78
|
+
# Custom tasks (add your own tasks here)
|
79
|
+
#
|
80
|
+
#############################################################################
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
#############################################################################
|
85
|
+
#
|
86
|
+
# Packaging tasks
|
87
|
+
#
|
88
|
+
#############################################################################
|
89
|
+
|
90
|
+
desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
|
91
|
+
task :release => :build do
|
92
|
+
unless `git branch` =~ /^\* master$/
|
93
|
+
puts "You must be on the master branch to release!"
|
94
|
+
exit!
|
95
|
+
end
|
96
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
97
|
+
sh "git tag v#{version}"
|
98
|
+
sh "git push origin master"
|
99
|
+
sh "git push origin v#{version}"
|
100
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
101
|
+
end
|
102
|
+
|
103
|
+
desc "Build #{gem_file} into the pkg directory"
|
104
|
+
task :build => :gemspec do
|
105
|
+
sh "mkdir -p pkg"
|
106
|
+
sh "gem build #{gemspec_file}"
|
107
|
+
sh "mv #{gem_file} pkg"
|
108
|
+
end
|
109
|
+
|
110
|
+
desc "Generate #{gemspec_file}"
|
111
|
+
task :gemspec => :validate do
|
112
|
+
# read spec file and split out manifest section
|
113
|
+
spec = File.read(gemspec_file)
|
114
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
115
|
+
|
116
|
+
# replace name version and date
|
117
|
+
replace_header(head, :name)
|
118
|
+
replace_header(head, :version)
|
119
|
+
replace_header(head, :date)
|
120
|
+
#comment this out if your rubyforge_project has a different name
|
121
|
+
replace_header(head, :rubyforge_project)
|
122
|
+
|
123
|
+
# determine file list from git ls-files
|
124
|
+
files = `git ls-files`.
|
125
|
+
split("\n").
|
126
|
+
sort.
|
127
|
+
reject { |file| file =~ /^\./ }.
|
128
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
129
|
+
map { |file| " #{file}" }.
|
130
|
+
join("\n")
|
131
|
+
|
132
|
+
# piece file back together and write
|
133
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
134
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
135
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
136
|
+
puts "Updated #{gemspec_file}"
|
137
|
+
end
|
138
|
+
|
139
|
+
desc "Validate #{gemspec_file}"
|
140
|
+
task :validate do
|
141
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
142
|
+
unless libfiles.empty?
|
143
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
144
|
+
exit!
|
145
|
+
end
|
146
|
+
unless Dir['VERSION*'].empty?
|
147
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
148
|
+
exit!
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
10
|
+
s.rubygems_version = '1.3.5'
|
11
|
+
|
12
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
13
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
14
|
+
## the sub! line in the Rakefile
|
15
|
+
s.name = 'gmail-contacts'
|
16
|
+
s.version = '0.0.4'
|
17
|
+
s.date = '2012-06-19'
|
18
|
+
s.rubyforge_project = 'gmail-contacts'
|
19
|
+
|
20
|
+
## Make sure your summary is short. The description may be as long
|
21
|
+
## as you like.
|
22
|
+
s.summary = "A Ruby library for fetching Gmail account contacts"
|
23
|
+
s.description = "A Ruby library for fetching Gmail account contacts"
|
24
|
+
|
25
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
26
|
+
## better to set the email to an email list or something. If you don't have
|
27
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
28
|
+
s.authors = ["Federico Builes"]
|
29
|
+
s.email = 'federico.builes@gmail.com'
|
30
|
+
s.homepage = 'https://github.com/febuiles/gmail-contacts'
|
31
|
+
|
32
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
33
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
34
|
+
s.require_paths = %w[lib]
|
35
|
+
|
36
|
+
## List your runtime dependencies here. Runtime dependencies are those
|
37
|
+
## that are needed for an end user to actually USE your code.
|
38
|
+
s.add_dependency("hpricot")
|
39
|
+
|
40
|
+
## Leave this section as-is. It will be automatically generated from the
|
41
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
42
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
43
|
+
# = MANIFEST =
|
44
|
+
s.files = %w[
|
45
|
+
README.md
|
46
|
+
Rakefile
|
47
|
+
gmail-contacts.gemspec
|
48
|
+
lib/gmail-contacts.rb
|
49
|
+
lib/gmail-contacts/google.rb
|
50
|
+
]
|
51
|
+
# = MANIFEST =
|
52
|
+
|
53
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
54
|
+
## matches what you actually use.
|
55
|
+
s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
|
56
|
+
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
require 'gmail_contacts'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hpricot'
|
5
|
+
require 'cgi'
|
6
|
+
require 'time'
|
7
|
+
require 'zlib'
|
8
|
+
require 'stringio'
|
9
|
+
require 'net/http'
|
10
|
+
require 'net/https'
|
11
|
+
|
12
|
+
module GmailContacts
|
13
|
+
# == Fetching Google Contacts
|
14
|
+
#
|
15
|
+
# First, get the user to follow the following URL:
|
16
|
+
#
|
17
|
+
# GmailContacts::Google.authentication_url('http://mysite.com/invite')
|
18
|
+
#
|
19
|
+
# After he authenticates successfully to Google, it will redirect him back to the target URL
|
20
|
+
# (specified as argument above) and provide the token GET parameter. Use it to create a
|
21
|
+
# new instance of this class and request the contact list:
|
22
|
+
#
|
23
|
+
# gmail = GmailContacts::Google.new(params[:token])
|
24
|
+
# gmail.contacts
|
25
|
+
# # => [#<Contact 1>, #<Contact 2>, ...]
|
26
|
+
#
|
27
|
+
# == Storing a session token
|
28
|
+
#
|
29
|
+
# The basic token that you will get after the user has authenticated on Google is valid
|
30
|
+
# for <b>only one request</b>. However, you can specify that you want a session token which
|
31
|
+
# doesn't expire:
|
32
|
+
#
|
33
|
+
# GmailContacts::Google.authentication_url('http://mysite.com/invite', :session => true)
|
34
|
+
#
|
35
|
+
# When the user authenticates, he will be redirected back with a token that can be exchanged
|
36
|
+
# for a session token with the following method:
|
37
|
+
#
|
38
|
+
# token = GmailContacts::Google.sesion_token(params[:token])
|
39
|
+
#
|
40
|
+
# Now you have a permanent token. Store it with other user data so you can query the API
|
41
|
+
# on his behalf without him having to authenticate on Google each time.
|
42
|
+
class Google
|
43
|
+
DOMAIN = 'www.google.com'
|
44
|
+
AuthSubPath = '/accounts/AuthSub' # all variants go over HTTPS
|
45
|
+
ClientLogin = '/accounts/ClientLogin'
|
46
|
+
FeedsPath = '/m8/feeds/contacts/'
|
47
|
+
|
48
|
+
# default options for #authentication_url
|
49
|
+
def self.authentication_url_options
|
50
|
+
@authentication_url_options ||= {
|
51
|
+
:scope => "http://#{DOMAIN}#{FeedsPath}",
|
52
|
+
:secure => false,
|
53
|
+
:session => false
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# default options for #client_login
|
58
|
+
def self.client_login_options
|
59
|
+
@client_login_options ||= {
|
60
|
+
:accountType => 'GOOGLE',
|
61
|
+
:service => 'cp',
|
62
|
+
:source => 'Contacts-Ruby'
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# URL to Google site where user authenticates. Afterwards, Google redirects to your
|
67
|
+
# site with the URL specified as +target+.
|
68
|
+
#
|
69
|
+
# Options are:
|
70
|
+
# * <tt>:scope</tt> -- the AuthSub scope in which the resulting token is valid
|
71
|
+
# (default: "http://www.google.com/m8/feeds/contacts/")
|
72
|
+
# * <tt>:secure</tt> -- boolean indicating whether the token will be secure. Only available
|
73
|
+
# for registered domains.
|
74
|
+
# (default: false)
|
75
|
+
# * <tt>:session</tt> -- boolean indicating if the token can be exchanged for a session token
|
76
|
+
# (default: false)
|
77
|
+
def self.authentication_url(target, options = {})
|
78
|
+
params = authentication_url_options.merge(options)
|
79
|
+
params[:next] = target
|
80
|
+
query = query_string(params)
|
81
|
+
"https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Makes an HTTPS request to exchange the given token with a session one. Session
|
85
|
+
# tokens never expire, so you can store them in the database alongside user info.
|
86
|
+
#
|
87
|
+
# Returns the new token as string or nil if the parameter couldn't be found in response
|
88
|
+
# body.
|
89
|
+
def self.session_token(token)
|
90
|
+
response = http_start do |google|
|
91
|
+
google.get(AuthSubPath + 'SessionToken', authorization_header(token))
|
92
|
+
end
|
93
|
+
|
94
|
+
pair = response.body.split(/\n/).detect { |p| p.index('Token=') == 0 }
|
95
|
+
pair.split('=').last if pair
|
96
|
+
end
|
97
|
+
|
98
|
+
# Alternative to AuthSub: using email and password.
|
99
|
+
def self.client_login(email, password)
|
100
|
+
response = http_start do |google|
|
101
|
+
query = query_string(client_login_options.merge(:Email => email, :Passwd => password))
|
102
|
+
puts "posting #{query} to #{ClientLogin}" if GmailContacts::verbose?
|
103
|
+
google.post(ClientLogin, query)
|
104
|
+
end
|
105
|
+
|
106
|
+
pair = response.body.split(/\n/).detect { |p| p.index('Auth=') == 0 }
|
107
|
+
pair.split('=').last if pair
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_reader :user, :token, :headers
|
111
|
+
attr_accessor :projection
|
112
|
+
|
113
|
+
# A token is required here. By default, an AuthSub token from
|
114
|
+
# Google is one-time only, which means you can only make a single request with it.
|
115
|
+
def initialize(token, user_id = 'default', client = false)
|
116
|
+
@user = user_id.to_s
|
117
|
+
@token = token.to_s
|
118
|
+
@headers = {
|
119
|
+
'Accept-Encoding' => 'gzip',
|
120
|
+
'User-Agent' => Identifier + ' (gzip)'
|
121
|
+
}.update(self.class.authorization_header(@token, client))
|
122
|
+
@projection = 'thin'
|
123
|
+
end
|
124
|
+
|
125
|
+
def get(params) # :nodoc:
|
126
|
+
self.class.http_start(false) do |google|
|
127
|
+
path = FeedsPath + CGI.escape(@user)
|
128
|
+
google_params = translate_parameters(params)
|
129
|
+
query = self.class.query_string(google_params)
|
130
|
+
google.get("#{path}/#{@projection}?#{query}", @headers)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Timestamp of last update. This value is available only after the XML
|
135
|
+
# document has been parsed; for instance after fetching the contact list.
|
136
|
+
def updated_at
|
137
|
+
@updated_at ||= Time.parse @updated_string if @updated_string
|
138
|
+
end
|
139
|
+
|
140
|
+
# Timestamp of last update as it appeared in the XML document
|
141
|
+
def updated_at_string
|
142
|
+
@updated_string
|
143
|
+
end
|
144
|
+
|
145
|
+
# Fetches, parses and returns the contact list.
|
146
|
+
#
|
147
|
+
# ==== Options
|
148
|
+
# * <tt>:limit</tt> -- use a large number to fetch a bigger contact list (default: 200)
|
149
|
+
# * <tt>:offset</tt> -- 0-based value, can be used for pagination
|
150
|
+
# * <tt>:order</tt> -- currently the only value support by Google is "lastmodified"
|
151
|
+
# * <tt>:descending</tt> -- boolean
|
152
|
+
# * <tt>:updated_after</tt> -- string or time-like object, use to only fetch contacts
|
153
|
+
# that were updated after this date
|
154
|
+
def contacts(options = {})
|
155
|
+
params = { :limit => 200 }.update(options)
|
156
|
+
response = get(params)
|
157
|
+
parse_contacts response_body(response)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Fetches contacts using multiple API calls when necessary
|
161
|
+
def all_contacts(options = {}, chunk_size = 200)
|
162
|
+
in_chunks(options, :contacts, chunk_size)
|
163
|
+
end
|
164
|
+
|
165
|
+
protected
|
166
|
+
|
167
|
+
def in_chunks(options, what, chunk_size)
|
168
|
+
returns = []
|
169
|
+
offset = 0
|
170
|
+
|
171
|
+
begin
|
172
|
+
chunk = send(what, options.merge(:offset => offset, :limit => chunk_size))
|
173
|
+
returns.push(*chunk)
|
174
|
+
offset += chunk_size
|
175
|
+
end while chunk.size == chunk_size
|
176
|
+
|
177
|
+
returns
|
178
|
+
end
|
179
|
+
|
180
|
+
def response_body(response)
|
181
|
+
unless response['Content-Encoding'] == 'gzip'
|
182
|
+
response.body
|
183
|
+
else
|
184
|
+
gzipped = StringIO.new(response.body)
|
185
|
+
Zlib::GzipReader.new(gzipped).read
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def parse_contacts(body)
|
190
|
+
doc = Hpricot::XML body
|
191
|
+
contacts_found = []
|
192
|
+
|
193
|
+
if updated_node = doc.at('/feed/updated')
|
194
|
+
@updated_string = updated_node.inner_text
|
195
|
+
end
|
196
|
+
|
197
|
+
(doc / '/feed/entry').each do |entry|
|
198
|
+
email_nodes = entry / 'gd:email[@address]'
|
199
|
+
|
200
|
+
unless email_nodes.empty?
|
201
|
+
title_node = entry.at('/title')
|
202
|
+
name = title_node ? title_node.inner_text : nil
|
203
|
+
contact = Contact.new(nil, name)
|
204
|
+
contact.emails.concat email_nodes.map { |e| e['address'].to_s }
|
205
|
+
contacts_found << contact
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
contacts_found
|
210
|
+
end
|
211
|
+
|
212
|
+
# Constructs a query string from a Hash object
|
213
|
+
def self.query_string(params)
|
214
|
+
params.inject([]) do |all, pair|
|
215
|
+
key, value = pair
|
216
|
+
unless value.nil?
|
217
|
+
value = case value
|
218
|
+
when TrueClass; '1'
|
219
|
+
when FalseClass; '0'
|
220
|
+
else value
|
221
|
+
end
|
222
|
+
|
223
|
+
all << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
224
|
+
end
|
225
|
+
all
|
226
|
+
end.join('&')
|
227
|
+
end
|
228
|
+
|
229
|
+
def translate_parameters(params)
|
230
|
+
params.inject({}) do |all, pair|
|
231
|
+
key, value = pair
|
232
|
+
unless value.nil?
|
233
|
+
key = case key
|
234
|
+
when :limit
|
235
|
+
'max-results'
|
236
|
+
when :offset
|
237
|
+
value = value.to_i + 1
|
238
|
+
'start-index'
|
239
|
+
when :order
|
240
|
+
all['sortorder'] = 'descending' if params[:descending].nil?
|
241
|
+
'orderby'
|
242
|
+
when :descending
|
243
|
+
value = value ? 'descending' : 'ascending'
|
244
|
+
'sortorder'
|
245
|
+
when :updated_after
|
246
|
+
value = value.strftime("%Y-%m-%dT%H:%M:%S%Z") if value.respond_to? :strftime
|
247
|
+
'updated-min'
|
248
|
+
else key
|
249
|
+
end
|
250
|
+
|
251
|
+
all[key] = value
|
252
|
+
end
|
253
|
+
all
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def self.authorization_header(token, client = false)
|
258
|
+
type = client ? 'GoogleLogin auth' : 'AuthSub token'
|
259
|
+
{ 'Authorization' => %(#{type}="#{token}") }
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.http_start(ssl = true)
|
263
|
+
port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
|
264
|
+
http = Net::HTTP.new(DOMAIN, port)
|
265
|
+
redirects = 0
|
266
|
+
if ssl
|
267
|
+
http.use_ssl = true
|
268
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
269
|
+
end
|
270
|
+
http.start
|
271
|
+
|
272
|
+
begin
|
273
|
+
response = yield(http)
|
274
|
+
|
275
|
+
loop do
|
276
|
+
inspect_response(response) if GmailContacts::verbose?
|
277
|
+
|
278
|
+
case response
|
279
|
+
when Net::HTTPSuccess
|
280
|
+
break response
|
281
|
+
when Net::HTTPRedirection
|
282
|
+
if redirects == TooManyRedirects::MAX_REDIRECTS
|
283
|
+
raise TooManyRedirects.new(response)
|
284
|
+
end
|
285
|
+
location = URI.parse response['Location']
|
286
|
+
puts "Redirected to #{location}"
|
287
|
+
response = http.get(location.path)
|
288
|
+
redirects += 1
|
289
|
+
else
|
290
|
+
response.error!
|
291
|
+
end
|
292
|
+
end
|
293
|
+
ensure
|
294
|
+
http.finish
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def self.inspect_response(response, out = $stderr)
|
299
|
+
out.puts response.inspect
|
300
|
+
for name, value in response
|
301
|
+
out.puts "#{name}: #{value}"
|
302
|
+
end
|
303
|
+
out.puts "----\n#{ response}\n----" unless response.body.empty?
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'gmail_contacts/google'
|
2
|
+
|
3
|
+
module GmailContacts
|
4
|
+
|
5
|
+
VERSION = "0.0.4"
|
6
|
+
|
7
|
+
Identifier = 'GmailContacts v' + VERSION
|
8
|
+
|
9
|
+
# An object that represents a single contact
|
10
|
+
class Contact
|
11
|
+
attr_reader :name, :username, :emails
|
12
|
+
|
13
|
+
def initialize(email, name = nil, username = nil)
|
14
|
+
@emails = []
|
15
|
+
@emails << email if email
|
16
|
+
@name = name
|
17
|
+
@username = username
|
18
|
+
end
|
19
|
+
|
20
|
+
def email
|
21
|
+
@emails.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
%!#<GmailContacts::Contact "#{name}"#{email ? " (#{email})" : ''}>!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.verbose?
|
30
|
+
'irb' == $0
|
31
|
+
end
|
32
|
+
|
33
|
+
class Error < StandardError
|
34
|
+
end
|
35
|
+
|
36
|
+
class TooManyRedirects < Error
|
37
|
+
attr_reader :response, :location
|
38
|
+
|
39
|
+
MAX_REDIRECTS = 2
|
40
|
+
|
41
|
+
def initialize(response)
|
42
|
+
@response = response
|
43
|
+
@location = @response['Location']
|
44
|
+
super "exceeded maximum of #{MAX_REDIRECTS} redirects (Location: #{location})"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gmail-contacts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Federico Builes
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: hpricot
|
16
|
+
requirement: &70217158537480 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70217158537480
|
25
|
+
description: A Ruby library for fetching Gmail account contacts
|
26
|
+
email: federico.builes@gmail.com
|
27
|
+
executables: []
|
28
|
+
extensions: []
|
29
|
+
extra_rdoc_files: []
|
30
|
+
files:
|
31
|
+
- README.md
|
32
|
+
- Rakefile
|
33
|
+
- gmail-contacts.gemspec
|
34
|
+
- lib/gmail-contacts.rb
|
35
|
+
- lib/gmail-contacts/google.rb
|
36
|
+
homepage: https://github.com/febuiles/gmail-contacts
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project: gmail-contacts
|
56
|
+
rubygems_version: 1.8.10
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: A Ruby library for fetching Gmail account contacts
|
60
|
+
test_files: []
|