gmail_contacts 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +3 -0
- data/History.txt +6 -0
- data/Manifest.txt +9 -0
- data/README.txt +51 -0
- data/Rakefile +15 -0
- data/lib/gmail_contacts/test_stub.rb +209 -0
- data/lib/gmail_contacts.rb +228 -0
- data/sample/authsub.rb +57 -0
- data/test/test_gmail_contacts.rb +112 -0
- data.tar.gz.sig +0 -0
- metadata +120 -0
- metadata.gz.sig +0 -0
data/.document
ADDED
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
= gmail_contacts
|
2
|
+
|
3
|
+
* http://seattlerb.rubyforge.org/gmail_contacts
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Simple Gmail contacts extraction using GData.
|
8
|
+
|
9
|
+
gmail_contacts development was sponsored by AT&T Interactive.
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
12
|
+
|
13
|
+
* Lets you extract contacts from GMail
|
14
|
+
|
15
|
+
== SYNOPSIS:
|
16
|
+
|
17
|
+
See sample/authsub.rb
|
18
|
+
|
19
|
+
== REQUIREMENTS:
|
20
|
+
|
21
|
+
* gdata gem
|
22
|
+
* Gmail account
|
23
|
+
|
24
|
+
== INSTALL:
|
25
|
+
|
26
|
+
* sudo gem install gmail_contacts
|
27
|
+
|
28
|
+
== LICENSE:
|
29
|
+
|
30
|
+
(The MIT License)
|
31
|
+
|
32
|
+
Copyright (c) 2009 Eric Hodel
|
33
|
+
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
35
|
+
a copy of this software and associated documentation files (the
|
36
|
+
'Software'), to deal in the Software without restriction, including
|
37
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
38
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
39
|
+
permit persons to whom the Software is furnished to do so, subject to
|
40
|
+
the following conditions:
|
41
|
+
|
42
|
+
The above copyright notice and this permission notice shall be
|
43
|
+
included in all copies or substantial portions of the Software.
|
44
|
+
|
45
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
46
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
47
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
48
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
49
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
50
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
51
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/gmail_contacts.rb'
|
6
|
+
|
7
|
+
Hoe.new('gmail_contacts', GmailContacts::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'seattlerb' # if different than lowercase project name
|
9
|
+
p.developer 'Eric Hodel', 'drbrain@segment7.net'
|
10
|
+
|
11
|
+
p.extra_deps << ['gdata', '~> 1.0']
|
12
|
+
p.extra_deps << ['nokogiri', '~> 1.2']
|
13
|
+
end
|
14
|
+
|
15
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'gmail_contacts'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Handy data files and GData modifications that allow GmailContacts to be
|
5
|
+
# more easily tested
|
6
|
+
#
|
7
|
+
# To setup:
|
8
|
+
#
|
9
|
+
# require 'gmail_contacts'
|
10
|
+
# require 'gmail_contacts/test_stub'
|
11
|
+
#
|
12
|
+
# class TestMyClass < Test::Unit::TestCase
|
13
|
+
# def test_something
|
14
|
+
# GmailContacts.stub
|
15
|
+
#
|
16
|
+
# # ... your code using gc
|
17
|
+
#
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# If you set the authsub token to 'recycled_authsub_token', the test stub will
|
22
|
+
# raise a GData::Client::AuthorizationError.
|
23
|
+
|
24
|
+
class GmailContacts::TestStub
|
25
|
+
|
26
|
+
##
|
27
|
+
# First page of contacts
|
28
|
+
|
29
|
+
CONTACTS = <<-ATOM
|
30
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
31
|
+
<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/"CEMBRHY6fSp7ImA9WxVUGEk."">
|
32
|
+
<id>eric@example.com</id>
|
33
|
+
<updated>2009-03-23T21:07:35.815Z</updated>
|
34
|
+
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
|
35
|
+
<title>drbrain's Contacts</title>
|
36
|
+
<link rel="alternate" type="text/html" href="http://www.google.com/"/>
|
37
|
+
<link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full"/>
|
38
|
+
<link rel="http://schemas.google.com/g/2005#post" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full"/>
|
39
|
+
<link rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/batch"/>
|
40
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full?max-results=2"/>
|
41
|
+
<link rel="next" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full?start-index=3&max-results=2"/>
|
42
|
+
<author>
|
43
|
+
<name>drbrain</name>
|
44
|
+
<email>eric@example.com</email>
|
45
|
+
</author>
|
46
|
+
<generator version="1.0" uri="http://www.google.com/m8/feeds">Contacts</generator>
|
47
|
+
<openSearch:totalResults>3</openSearch:totalResults>
|
48
|
+
<openSearch:startIndex>1</openSearch:startIndex>
|
49
|
+
<openSearch:itemsPerPage>2</openSearch:itemsPerPage>
|
50
|
+
<entry gd:etag=""SHg-cTVSLip7ImA9WB5WGUUIQgc."">
|
51
|
+
<id>http://www.google.com/m8/feeds/contacts/eric%40example.com/base/0</id>
|
52
|
+
<updated>2007-08-01T15:35:39.659Z</updated>
|
53
|
+
<app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T15:35:39.659Z</app:edited>
|
54
|
+
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
|
55
|
+
<title>Sean</title>
|
56
|
+
<link rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*" href="http://www.google.com/m8/feeds/photos/media/eric%40example.com/0"/>
|
57
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/0"/>
|
58
|
+
<link rel="edit" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/0"/>
|
59
|
+
<gd:email rel="http://schemas.google.com/g/2005#other" address="sean@example.com" primary="true"/>
|
60
|
+
</entry>
|
61
|
+
<entry gd:etag=""QXk4fjVSLyp7ImA9WxVUGUwDRgE."">
|
62
|
+
<id>http://www.google.com/m8/feeds/contacts/eric%40example.com/base/18</id>
|
63
|
+
<updated>2009-03-24T18:25:50.736Z</updated>
|
64
|
+
<app:edited xmlns:app="http://www.w3.org/2007/app">2009-03-24T18:25:50.736Z</app:edited>
|
65
|
+
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
|
66
|
+
<title>Eric</title>
|
67
|
+
<link rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*" href="http://www.google.com/m8/feeds/photos/media/eric%40example.com/18" gd:etag=""UD9rbkUqSip7ImBkJkcZdVBoHxkeNFMKV1E.""/>
|
68
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/18"/>
|
69
|
+
<link rel="edit" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/18"/>
|
70
|
+
<gd:email rel="http://schemas.google.com/g/2005#other" address="eric@example.com" primary="true"/>
|
71
|
+
<gd:email rel="http://schemas.google.com/g/2005#other" address="eric@example.net"/>
|
72
|
+
<gd:im address="example" protocol="http://schemas.google.com/g/2005#AIM" rel="http://schemas.google.com/g/2005#other"/>
|
73
|
+
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile">999 555 1212</gd:phoneNumber>
|
74
|
+
<gd:postalAddress rel="http://schemas.google.com/g/2005#home">123 Any Street
|
75
|
+
AnyTown, ZZ 99999</gd:postalAddress>
|
76
|
+
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/eric%40example.com/base/6"/>
|
77
|
+
</entry>
|
78
|
+
</feed>
|
79
|
+
ATOM
|
80
|
+
|
81
|
+
##
|
82
|
+
# Second page of contacts
|
83
|
+
|
84
|
+
CONTACTS2 = <<-ATOM
|
85
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
86
|
+
<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/"CEYFQXkzfCp7ImA9WxVUGEg."">
|
87
|
+
<id>eric@example.com</id>
|
88
|
+
<updated>2009-03-23T23:48:30.784Z</updated>
|
89
|
+
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
|
90
|
+
<title>drbrain's Contacts</title>
|
91
|
+
<link rel="alternate" type="text/html" href="http://www.google.com/"/>
|
92
|
+
<link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full"/>
|
93
|
+
<link rel="http://schemas.google.com/g/2005#post" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full"/>
|
94
|
+
<link rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/batch"/>
|
95
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full?start-index=3&max-results=2"/>
|
96
|
+
<link rel="previous" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full?start-index=1&max-results=2"/>
|
97
|
+
<author>
|
98
|
+
<name>drbrain</name>
|
99
|
+
<email>eric@example.com</email>
|
100
|
+
</author>
|
101
|
+
<generator version="1.0" uri="http://www.google.com/m8/feeds">Contacts</generator>
|
102
|
+
<openSearch:totalResults>3</openSearch:totalResults>
|
103
|
+
<openSearch:startIndex>3</openSearch:startIndex>
|
104
|
+
<openSearch:itemsPerPage>2</openSearch:itemsPerPage>
|
105
|
+
<entry gd:etag=""QXk_fTVSLyp7ImA9WxVUFUQITgA."">
|
106
|
+
<id>http://www.google.com/m8/feeds/contacts/eric%40example.com/base/5834fb5d0b47bfd7</id>
|
107
|
+
<updated>2009-03-20T23:49:00.745Z</updated>
|
108
|
+
<app:edited xmlns:app="http://www.w3.org/2007/app">2009-03-20T23:49:00.745Z</app:edited>
|
109
|
+
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
|
110
|
+
<title>Coby</title>
|
111
|
+
<link rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*" href="http://www.google.com/m8/feeds/photos/media/eric%40example.com/5834fb5d0b47bfd7" gd:etag=""eRJhPnolbCp7ImBjG0U0GBtuHmVAdnsJYzM.""/>
|
112
|
+
<link rel="self" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/5834fb5d0b47bfd7"/>
|
113
|
+
<link rel="edit" type="application/atom+xml" href="http://www.google.com/m8/feeds/contacts/eric%40example.com/full/5834fb5d0b47bfd7"/>
|
114
|
+
<gd:email rel="http://schemas.google.com/g/2005#other" address="coby@example.com" primary="true"/>
|
115
|
+
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/eric%40example.com/base/6"/>
|
116
|
+
</entry>
|
117
|
+
</feed>
|
118
|
+
ATOM
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
# :stopdoc:
|
123
|
+
class GmailContacts
|
124
|
+
|
125
|
+
@stubbed = false
|
126
|
+
|
127
|
+
def self.stub
|
128
|
+
return if @stubbed
|
129
|
+
@stubbed = true
|
130
|
+
|
131
|
+
alias old_get_token get_token
|
132
|
+
|
133
|
+
def get_token
|
134
|
+
if @authsub_token == 'recycled_authsub_token' then
|
135
|
+
raise GData::Client::AuthorizationError, 'recycled token'
|
136
|
+
end
|
137
|
+
old_get_token
|
138
|
+
@contact_api.stub_reset
|
139
|
+
@contact_api.stub_data << GmailContacts::TestStub::CONTACTS
|
140
|
+
@contact_api.stub_data << GmailContacts::TestStub::CONTACTS2
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
# :startdoc:
|
146
|
+
|
147
|
+
##
|
148
|
+
# Extension that provides a stub for testing GmailContacts
|
149
|
+
#
|
150
|
+
# See GmailContacts::TestStub for usage details
|
151
|
+
|
152
|
+
class GData::Client::Contacts
|
153
|
+
|
154
|
+
##
|
155
|
+
# Accessor for data the stub will return
|
156
|
+
|
157
|
+
attr_accessor :stub_data
|
158
|
+
|
159
|
+
##
|
160
|
+
# Accessor for the stub AuthSub token
|
161
|
+
|
162
|
+
attr_accessor :stub_token
|
163
|
+
|
164
|
+
##
|
165
|
+
# Accessor for URLs the stub accessed
|
166
|
+
|
167
|
+
attr_accessor :stub_urls
|
168
|
+
|
169
|
+
##
|
170
|
+
# Sets the authsub token to +token+, but does no HTTP requests
|
171
|
+
|
172
|
+
def authsub_token=(token)
|
173
|
+
@stub_token = token
|
174
|
+
auth_handler = Object.new
|
175
|
+
def auth_handler.upgrade() @upgraded = true end
|
176
|
+
def auth_handler.upgraded?() @upgraded end
|
177
|
+
def auth_handler.revoke() @revoked = true end
|
178
|
+
def auth_handler.revoked?() @revoked end
|
179
|
+
self.auth_handler = auth_handler
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Fetches +url+, records in in stub_urls, and returns data if there's still
|
184
|
+
# data in stub_data, or raises an exception
|
185
|
+
|
186
|
+
def get(url)
|
187
|
+
@stub_urls << url
|
188
|
+
raise 'stub data empty' if @stub_data.empty?
|
189
|
+
|
190
|
+
data = @stub_data.shift
|
191
|
+
|
192
|
+
return data.call if Proc === data
|
193
|
+
|
194
|
+
res = Object.new
|
195
|
+
def res.body() @data end
|
196
|
+
res.instance_variable_set :@data, data
|
197
|
+
res
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Resets the stub to empty
|
202
|
+
|
203
|
+
def stub_reset
|
204
|
+
@stub_urls = []
|
205
|
+
@stub_data = []
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'gdata'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
##
|
6
|
+
# GmailContacts sits atop GData and turns the contact feed into
|
7
|
+
# GmailContacts::Contact objects for friendly consumption.
|
8
|
+
#
|
9
|
+
# GmailContacts was sponsored by AT&T Interactive.
|
10
|
+
|
11
|
+
class GmailContacts
|
12
|
+
|
13
|
+
VERSION = '1.0'
|
14
|
+
|
15
|
+
Contact = Struct.new :title, :emails, :ims, :phone_numbers, :addresses
|
16
|
+
|
17
|
+
##
|
18
|
+
# Struct containing title, emails, ims, phone_numbers and addresses
|
19
|
+
|
20
|
+
class Contact
|
21
|
+
|
22
|
+
def pretty_print(q) # :nodoc:
|
23
|
+
q.text "#{title}"
|
24
|
+
q.breakable
|
25
|
+
|
26
|
+
q.group 2 do
|
27
|
+
q.breakable
|
28
|
+
q.group 2, 'emails: ' do
|
29
|
+
emails.each_with_index do |email, i|
|
30
|
+
unless i == 0 then
|
31
|
+
q.text ','
|
32
|
+
q.breakable
|
33
|
+
end
|
34
|
+
|
35
|
+
email += " (Primary)" if i == 0
|
36
|
+
q.text email
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
q.breakable
|
41
|
+
q.group 2, 'ims: ' do
|
42
|
+
ims.each_with_index do |(address, type), i|
|
43
|
+
unless i == 0 then
|
44
|
+
q.text ','
|
45
|
+
q.breakable
|
46
|
+
end
|
47
|
+
|
48
|
+
q.text "#{address} (#{type.sub(/.*#/, '')})"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
q.breakable
|
53
|
+
q.group 2, 'phone numbers: ' do
|
54
|
+
phone_numbers.each_with_index do |(number, type), i|
|
55
|
+
unless i == 0 then
|
56
|
+
q.text ','
|
57
|
+
q.breakable
|
58
|
+
end
|
59
|
+
|
60
|
+
q.text "#{number} (#{type.sub(/.*#/, '')})"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
q.breakable
|
65
|
+
addresses.each do |address, type|
|
66
|
+
q.group 2, "#{type.sub(/.*#/, '')} address:\n" do
|
67
|
+
address = address.split("\n").each do |line|
|
68
|
+
q.text "#{' ' * q.indent}#{line}\n"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Returns the user's primary email address
|
77
|
+
|
78
|
+
def primary_email
|
79
|
+
emails.first
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Contact list author's email
|
86
|
+
|
87
|
+
attr_reader :author_email
|
88
|
+
|
89
|
+
##
|
90
|
+
# Contact list author's name
|
91
|
+
|
92
|
+
attr_reader :author_name
|
93
|
+
|
94
|
+
##
|
95
|
+
# GData::Client::Contacts object accessor for testing
|
96
|
+
|
97
|
+
attr_accessor :contact_api # :nodoc:
|
98
|
+
|
99
|
+
##
|
100
|
+
# Contact data
|
101
|
+
#
|
102
|
+
# An Array with contact title, primary email and alternate emails
|
103
|
+
|
104
|
+
attr_reader :contacts
|
105
|
+
|
106
|
+
##
|
107
|
+
# Contacts list identifier
|
108
|
+
|
109
|
+
attr_reader :id
|
110
|
+
|
111
|
+
##
|
112
|
+
# Contacts list title
|
113
|
+
|
114
|
+
attr_reader :title
|
115
|
+
|
116
|
+
##
|
117
|
+
# Creates a new GmailContacts using +authsub_token+. If you don't yet have
|
118
|
+
# an AuthSub token, call <tt>contact_api.auth_url</tt> providing your return
|
119
|
+
# endpoint.
|
120
|
+
#
|
121
|
+
# See GData::Client::Base in the gdata gem and
|
122
|
+
# http://code.google.com/apis/accounts/docs/AuthSub.html for more details.
|
123
|
+
|
124
|
+
def initialize(authsub_token = nil)
|
125
|
+
@authsub_token = authsub_token
|
126
|
+
@session_token = false
|
127
|
+
|
128
|
+
@id = nil
|
129
|
+
@title = nil
|
130
|
+
@author_email = nil
|
131
|
+
@author_name = nil
|
132
|
+
@contacts ||= []
|
133
|
+
|
134
|
+
@contact_api = GData::Client::Contacts.new
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Fetches contacts from google for +email+.
|
139
|
+
|
140
|
+
def fetch(email)
|
141
|
+
get_token
|
142
|
+
|
143
|
+
uri = "http://www.google.com/m8/feeds/contacts/#{email}/full"
|
144
|
+
|
145
|
+
loop do
|
146
|
+
res = @contact_api.get uri
|
147
|
+
|
148
|
+
xml = Nokogiri::XML res.body
|
149
|
+
|
150
|
+
parse xml
|
151
|
+
|
152
|
+
next_uri = xml.xpath('//xmlns:feed/xmlns:link[@rel="next"]').first
|
153
|
+
break unless next_uri
|
154
|
+
|
155
|
+
uri = next_uri['href']
|
156
|
+
end
|
157
|
+
ensure
|
158
|
+
revoke_token if token?
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Fetches an AuthSub session token
|
163
|
+
|
164
|
+
def get_token
|
165
|
+
@contact_api.authsub_token = @authsub_token
|
166
|
+
@contact_api.auth_handler.upgrade
|
167
|
+
@session_token = true
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Extracts contact information from +xml+, appending it to the current
|
172
|
+
# contact information
|
173
|
+
|
174
|
+
def parse(xml)
|
175
|
+
@id = xml.xpath('//xmlns:feed/xmlns:id').first.text
|
176
|
+
@title = xml.xpath('//xmlns:feed/xmlns:title').first.text
|
177
|
+
|
178
|
+
@author_email =
|
179
|
+
xml.xpath('//xmlns:feed/xmlns:author/xmlns:email').first.text
|
180
|
+
@author_name = xml.xpath('//xmlns:feed/xmlns:author/xmlns:name').first.text
|
181
|
+
|
182
|
+
xml.xpath('//xmlns:feed/xmlns:entry').each do |entry|
|
183
|
+
title = entry.xpath('.//xmlns:title').first.text
|
184
|
+
emails = []
|
185
|
+
emails << entry.xpath('.//gd:email[@primary]').first['address']
|
186
|
+
alternates = entry.xpath('.//gd:email[not(@primary)]')
|
187
|
+
|
188
|
+
emails.push(*alternates.map { |e| e['address'] })
|
189
|
+
|
190
|
+
ims = []
|
191
|
+
entry.xpath('.//gd:im').each do |im|
|
192
|
+
ims << [im['address'], im['protocol']]
|
193
|
+
end
|
194
|
+
|
195
|
+
phones = []
|
196
|
+
entry.xpath('.//gd:phoneNumber').each do |phone|
|
197
|
+
phones << [phone.text, phone['rel']]
|
198
|
+
end
|
199
|
+
|
200
|
+
addresses = []
|
201
|
+
entry.xpath('.//gd:postalAddress').each do |address|
|
202
|
+
addresses << [address.text, address['rel']]
|
203
|
+
end
|
204
|
+
|
205
|
+
contact = Contact.new title, emails, ims, phones, addresses
|
206
|
+
|
207
|
+
@contacts << contact
|
208
|
+
end
|
209
|
+
|
210
|
+
self
|
211
|
+
end
|
212
|
+
|
213
|
+
##
|
214
|
+
# Revokes our AuthSub token
|
215
|
+
|
216
|
+
def revoke_token
|
217
|
+
@contact_api.auth_handler.revoke
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Do we have an AuthSub session token?
|
222
|
+
|
223
|
+
def token?
|
224
|
+
@session_token
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
data/sample/authsub.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'webrick'
|
3
|
+
require 'gmail_contacts'
|
4
|
+
|
5
|
+
webrick = WEBrick::HTTPServer.new :Port => 3000
|
6
|
+
|
7
|
+
webrick.mount_proc '/' do |req, res|
|
8
|
+
res.content_type = 'text/html'
|
9
|
+
|
10
|
+
if req.path == '/' then
|
11
|
+
res.body = <<-BODY
|
12
|
+
<form action="http://#{Socket.gethostname}:3000/go">
|
13
|
+
<input name="email">
|
14
|
+
<input type="submit" value="go">
|
15
|
+
</form>
|
16
|
+
BODY
|
17
|
+
|
18
|
+
elsif req.path == '/go' then
|
19
|
+
gmail_contacts = GmailContacts.new
|
20
|
+
url = gmail_contacts.contact_api.authsub_url \
|
21
|
+
"http://#{Socket.gethostname}:3000/return?email=#{req.query['email']}"
|
22
|
+
res.set_redirect WEBrick::HTTPStatus::SeeOther, url
|
23
|
+
|
24
|
+
elsif req.path == '/return' then
|
25
|
+
res.body = "<h1>contacts</h1>\n"
|
26
|
+
|
27
|
+
begin
|
28
|
+
gmail_contacts = GmailContacts.new req.query['token'].to_s
|
29
|
+
|
30
|
+
email = req.query['email']
|
31
|
+
|
32
|
+
gmail_contacts.fetch email
|
33
|
+
|
34
|
+
res.body << "<h1>contacts</h1>\n\n<ul>\n"
|
35
|
+
|
36
|
+
gmail_contacts.contacts.each do |contact|
|
37
|
+
res.body << "<li>#{contact.title} - #{contact.primary_email}\n"
|
38
|
+
end
|
39
|
+
|
40
|
+
res.body << "<\ul>\n"
|
41
|
+
rescue => e
|
42
|
+
res.body << <<-BODY
|
43
|
+
<h1>error</h1>
|
44
|
+
|
45
|
+
<p>#{e.message}
|
46
|
+
|
47
|
+
<pre>#{e.backtrace.join "\n"}</pre>
|
48
|
+
BODY
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
trap 'INT' do webrick.shutdown end
|
54
|
+
trap 'TERM' do webrick.shutdown end
|
55
|
+
|
56
|
+
webrick.start
|
57
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'gmail_contacts'
|
3
|
+
require 'gmail_contacts/test_stub'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
class TestGmailContacts < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@gc = GmailContacts.new 'token'
|
10
|
+
@api = @gc.contact_api
|
11
|
+
@api.stub_reset
|
12
|
+
|
13
|
+
@eric =
|
14
|
+
GmailContacts::Contact.new('Eric', %w[eric@example.com eric@example.net],
|
15
|
+
[%w[example http://schemas.google.com/g/2005#AIM]],
|
16
|
+
[%w[999\ 555\ 1212 http://schemas.google.com/g/2005#mobile]],
|
17
|
+
[["123 Any Street\nAnyTown, ZZ 99999",
|
18
|
+
"http://schemas.google.com/g/2005#home"]])
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_contact_pretty_print
|
22
|
+
str = ''
|
23
|
+
PP.pp @eric, str
|
24
|
+
|
25
|
+
expected = <<-EXPECTED
|
26
|
+
Eric
|
27
|
+
|
28
|
+
emails: eric@example.com (Primary), eric@example.net
|
29
|
+
ims: example (AIM)
|
30
|
+
phone numbers: 999 555 1212 (mobile)
|
31
|
+
home address:
|
32
|
+
123 Any Street
|
33
|
+
AnyTown, ZZ 99999
|
34
|
+
|
35
|
+
EXPECTED
|
36
|
+
|
37
|
+
assert_equal expected, str
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_fetch
|
41
|
+
@api.stub_data << GmailContacts::TestStub::CONTACTS
|
42
|
+
@api.stub_data << GmailContacts::TestStub::CONTACTS2
|
43
|
+
|
44
|
+
@gc.fetch 'eric@example.com'
|
45
|
+
|
46
|
+
assert_equal 3, @gc.contacts.length
|
47
|
+
|
48
|
+
assert_equal 2, @api.stub_urls.length
|
49
|
+
assert_equal 'http://www.google.com/m8/feeds/contacts/eric@example.com/full',
|
50
|
+
@api.stub_urls.shift
|
51
|
+
assert_equal 'http://www.google.com/m8/feeds/contacts/eric%40example.com/full?start-index=3&max-results=2',
|
52
|
+
@api.stub_urls.shift
|
53
|
+
|
54
|
+
assert @api.auth_handler.upgraded?
|
55
|
+
assert @api.auth_handler.revoked?
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_fetch_forbidden
|
59
|
+
@api.stub_data << proc do
|
60
|
+
raise GData::Client::AuthorizationError
|
61
|
+
end
|
62
|
+
|
63
|
+
assert_raise GData::Client::AuthorizationError do
|
64
|
+
@gc.fetch 'notme@example.com'
|
65
|
+
end
|
66
|
+
|
67
|
+
assert_equal 0, @gc.contacts.length
|
68
|
+
|
69
|
+
assert_equal 1, @api.stub_urls.length
|
70
|
+
assert_equal 'http://www.google.com/m8/feeds/contacts/notme@example.com/full',
|
71
|
+
@api.stub_urls.shift
|
72
|
+
|
73
|
+
assert @api.auth_handler.upgraded?
|
74
|
+
assert @api.auth_handler.revoked?
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_parse
|
78
|
+
@gc.parse Nokogiri::XML(GmailContacts::TestStub::CONTACTS)
|
79
|
+
|
80
|
+
assert_equal 'eric@example.com', @gc.id
|
81
|
+
assert_equal 'drbrain\'s Contacts', @gc.title
|
82
|
+
assert_equal 'drbrain', @gc.author_name
|
83
|
+
assert_equal 'eric@example.com', @gc.author_email
|
84
|
+
|
85
|
+
expected = [
|
86
|
+
GmailContacts::Contact.new('Sean', %w[sean@example.com], [], [], []),
|
87
|
+
@eric
|
88
|
+
]
|
89
|
+
|
90
|
+
assert_equal expected, @gc.contacts
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_parse_two_pages
|
94
|
+
@gc.parse Nokogiri::XML(GmailContacts::TestStub::CONTACTS)
|
95
|
+
@gc.parse Nokogiri::XML(GmailContacts::TestStub::CONTACTS2)
|
96
|
+
|
97
|
+
assert_equal 'eric@example.com', @gc.id
|
98
|
+
assert_equal 'drbrain\'s Contacts', @gc.title
|
99
|
+
assert_equal 'drbrain', @gc.author_name
|
100
|
+
assert_equal 'eric@example.com', @gc.author_email
|
101
|
+
|
102
|
+
expected = [
|
103
|
+
GmailContacts::Contact.new('Sean', %w[sean@example.com], [], [], []),
|
104
|
+
@eric,
|
105
|
+
GmailContacts::Contact.new('Coby', %w[coby@example.com], [], [], []),
|
106
|
+
]
|
107
|
+
|
108
|
+
assert_equal expected, @gc.contacts
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
data.tar.gz.sig
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gmail_contacts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.0"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Hodel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMRAwDgYDVQQDDAdkcmJy
|
14
|
+
YWluMRgwFgYKCZImiZPyLGQBGRYIc2VnbWVudDcxEzARBgoJkiaJk/IsZAEZFgNu
|
15
|
+
ZXQwHhcNMDcxMjIxMDIwNDE0WhcNMDgxMjIwMDIwNDE0WjBBMRAwDgYDVQQDDAdk
|
16
|
+
cmJyYWluMRgwFgYKCZImiZPyLGQBGRYIc2VnbWVudDcxEzARBgoJkiaJk/IsZAEZ
|
17
|
+
FgNuZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbbgLrGLGIDE76
|
18
|
+
LV/cvxdEzCuYuS3oG9PrSZnuDweySUfdp/so0cDq+j8bqy6OzZSw07gdjwFMSd6J
|
19
|
+
U5ddZCVywn5nnAQ+Ui7jMW54CYt5/H6f2US6U0hQOjJR6cpfiymgxGdfyTiVcvTm
|
20
|
+
Gj/okWrQl0NjYOYBpDi+9PPmaH2RmLJu0dB/NylsDnW5j6yN1BEI8MfJRR+HRKZY
|
21
|
+
mUtgzBwF1V4KIZQ8EuL6I/nHVu07i6IkrpAgxpXUfdJQJi0oZAqXurAV3yTxkFwd
|
22
|
+
g62YrrW26mDe+pZBzR6bpLE+PmXCzz7UxUq3AE0gPHbiMXie3EFE0oxnsU3lIduh
|
23
|
+
sCANiQ8BAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
24
|
+
BBS5k4Z75VSpdM0AclG2UvzFA/VW5DANBgkqhkiG9w0BAQUFAAOCAQEAHagT4lfX
|
25
|
+
kP/hDaiwGct7XPuVGbrOsKRVD59FF5kETBxEc9UQ1clKWngf8JoVuEoKD774dW19
|
26
|
+
bU0GOVWO+J6FMmT/Cp7nuFJ79egMf/gy4gfUfQMuvfcr6DvZUPIs9P/TlK59iMYF
|
27
|
+
DIOQ3DxdF3rMzztNUCizN4taVscEsjCcgW6WkUJnGdqlu3OHWpQxZBJkBTjPCoc6
|
28
|
+
UW6on70SFPmAy/5Cq0OJNGEWBfgD9q7rrs/X8GGwUWqXb85RXnUVi/P8Up75E0ag
|
29
|
+
14jEc90kN+C7oI/AGCBN0j6JnEtYIEJZibjjDJTSMWlUKKkj30kq7hlUC2CepJ4v
|
30
|
+
x52qPcexcYZR7w==
|
31
|
+
-----END CERTIFICATE-----
|
32
|
+
|
33
|
+
date: 2009-04-08 00:00:00 -07:00
|
34
|
+
default_executable:
|
35
|
+
dependencies:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: gdata
|
38
|
+
type: :runtime
|
39
|
+
version_requirement:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "1.0"
|
45
|
+
version:
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: nokogiri
|
48
|
+
type: :runtime
|
49
|
+
version_requirement:
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "1.2"
|
55
|
+
version:
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: hoe
|
58
|
+
type: :development
|
59
|
+
version_requirement:
|
60
|
+
version_requirements: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 1.12.1
|
65
|
+
version:
|
66
|
+
description: |-
|
67
|
+
Simple Gmail contacts extraction using GData.
|
68
|
+
|
69
|
+
gmail_contacts development was sponsored by AT&T Interactive.
|
70
|
+
email:
|
71
|
+
- drbrain@segment7.net
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files:
|
77
|
+
- History.txt
|
78
|
+
- Manifest.txt
|
79
|
+
- README.txt
|
80
|
+
files:
|
81
|
+
- .document
|
82
|
+
- History.txt
|
83
|
+
- Manifest.txt
|
84
|
+
- README.txt
|
85
|
+
- Rakefile
|
86
|
+
- lib/gmail_contacts.rb
|
87
|
+
- lib/gmail_contacts/test_stub.rb
|
88
|
+
- sample/authsub.rb
|
89
|
+
- test/test_gmail_contacts.rb
|
90
|
+
has_rdoc: true
|
91
|
+
homepage: http://seattlerb.rubyforge.org/gmail_contacts
|
92
|
+
licenses: []
|
93
|
+
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options:
|
96
|
+
- --main
|
97
|
+
- README.txt
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: "0"
|
105
|
+
version:
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: "0"
|
111
|
+
version:
|
112
|
+
requirements: []
|
113
|
+
|
114
|
+
rubyforge_project: seattlerb
|
115
|
+
rubygems_version: 1.3.1.2162
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Simple Gmail contacts extraction using GData
|
119
|
+
test_files:
|
120
|
+
- test/test_gmail_contacts.rb
|
metadata.gz.sig
ADDED
Binary file
|