gmail-contacts 0.0.4

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