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