context-io 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/Gemfile +4 -0
  2. data/LICENSE +21 -0
  3. data/README.md +129 -0
  4. data/Rakefile +121 -0
  5. data/SPEC.md +49 -0
  6. data/context-io.gemspec +96 -0
  7. data/lib/context-io.rb +14 -0
  8. data/lib/context-io/account.rb +254 -0
  9. data/lib/context-io/authentication.rb +23 -0
  10. data/lib/context-io/config.rb +103 -0
  11. data/lib/context-io/connection.rb +45 -0
  12. data/lib/context-io/core_ext/hash.rb +31 -0
  13. data/lib/context-io/error.rb +24 -0
  14. data/lib/context-io/error/bad_request.rb +12 -0
  15. data/lib/context-io/error/client_error.rb +10 -0
  16. data/lib/context-io/error/forbidden.rb +12 -0
  17. data/lib/context-io/error/internal_server_error.rb +10 -0
  18. data/lib/context-io/error/not_found.rb +12 -0
  19. data/lib/context-io/error/payment_required.rb +13 -0
  20. data/lib/context-io/error/server_error.rb +10 -0
  21. data/lib/context-io/error/service_unavailable.rb +13 -0
  22. data/lib/context-io/error/unauthorized.rb +12 -0
  23. data/lib/context-io/file.rb +234 -0
  24. data/lib/context-io/folder.rb +90 -0
  25. data/lib/context-io/message.rb +160 -0
  26. data/lib/context-io/oauth_provider.rb +84 -0
  27. data/lib/context-io/request.rb +70 -0
  28. data/lib/context-io/request/oauth.rb +44 -0
  29. data/lib/context-io/resource.rb +16 -0
  30. data/lib/context-io/response.rb +5 -0
  31. data/lib/context-io/response/parse_json.rb +30 -0
  32. data/lib/context-io/response/raise_client_error.rb +59 -0
  33. data/lib/context-io/response/raise_server_error.rb +32 -0
  34. data/lib/context-io/source.rb +193 -0
  35. data/lib/context-io/version.rb +7 -0
  36. data/spec/account_spec.rb +247 -0
  37. data/spec/contextio_spec.rb +45 -0
  38. data/spec/file_spec.rb +101 -0
  39. data/spec/fixtures/accounts.json +21 -0
  40. data/spec/fixtures/files.json +41 -0
  41. data/spec/fixtures/files_group.json +47 -0
  42. data/spec/fixtures/folders.json +1 -0
  43. data/spec/fixtures/messages.json +1 -0
  44. data/spec/fixtures/oauth_providers.json +12 -0
  45. data/spec/fixtures/sources.json +1 -0
  46. data/spec/folder_spec.rb +48 -0
  47. data/spec/message_spec.rb +294 -0
  48. data/spec/oauth_provider_spec.rb +88 -0
  49. data/spec/source_spec.rb +248 -0
  50. data/spec/spec_helper.rb +4 -0
  51. metadata +214 -0
@@ -0,0 +1,16 @@
1
+ require 'context-io/connection'
2
+ require 'context-io/request'
3
+
4
+ module ContextIO
5
+ # The superclass of all resources provided by the API
6
+ #
7
+ # @api private
8
+ class Resource
9
+ include ContextIO::Connection
10
+ include ContextIO::Request
11
+
12
+ extend ContextIO::Connection
13
+ extend ContextIO::Request
14
+ end
15
+ end
16
+
@@ -0,0 +1,5 @@
1
+ module ContextIO
2
+ # Namespace for response
3
+ module Response
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ require 'faraday'
2
+ require 'multi_json'
3
+
4
+ module ContextIO
5
+ module Response
6
+ # Faraday middleware for parsing JSON in the response body
7
+ #
8
+ # @api private
9
+ class ParseJson < Faraday::Response::Middleware
10
+ # Parse the response body into JSON
11
+ #
12
+ # @param [#to_s] The response body, containing JSON data.
13
+ #
14
+ # @return [Object] The parsed JSON data, probably an Array or a Hash.
15
+ def parse(body)
16
+ case body
17
+ when ''
18
+ nil
19
+ when 'true'
20
+ true
21
+ when 'false'
22
+ false
23
+ else
24
+ ::MultiJson.decode(body)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,59 @@
1
+ require 'faraday'
2
+ require 'context-io/error/bad_request'
3
+ require 'context-io/error/forbidden'
4
+ require 'context-io/error/not_found'
5
+ require 'context-io/error/unauthorized'
6
+ require 'context-io/error/payment_required'
7
+
8
+ module ContextIO
9
+ module Response
10
+ # Faraday middleware for raising errors on 4xx HTTP status codes
11
+ #
12
+ # @api private
13
+ class RaiseClientError < Faraday::Response::Middleware
14
+ # Raise an error if the response has a 4xx status code
15
+ #
16
+ # @raise [ContextIO::Error::ClientError] If the response has a 4xx status
17
+ # code.
18
+ # @raise [ContextIO::Error::BadRequest] If the response has a 400 status
19
+ # code.
20
+ # @raise [ContextIO::Error::Unauthorized] If the response has a 401 status
21
+ # code.
22
+ # @raise [ContextIO::Error::PaymentRequired] If the response has a 402
23
+ # status code.
24
+ # @raise [ContextIO::Error::Forbidden] If the response has a 403 status
25
+ # code.
26
+ # @raise [ContextIO::Error::NotFound] If the response has a 404 status
27
+ # code.
28
+ #
29
+ # @return [void]
30
+ def on_complete(env)
31
+ case env[:status].to_i
32
+ when 400
33
+ raise ContextIO::Error::BadRequest.new(error_body(env[:body]), env[:response_headers])
34
+ when 401
35
+ raise ContextIO::Error::Unauthorized.new(error_body(env[:body]), env[:response_headers])
36
+ when 402
37
+ raise ContextIO::Error::PaymentRequired.new(error_body(env[:body]), env[:response_headers])
38
+ when 403
39
+ raise ContextIO::Error::Forbidden.new(error_body(env[:body]), env[:response_headers])
40
+ when 404
41
+ raise ContextIO::Error::NotFound.new(error_body(env[:body]), env[:response_headers])
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # @return [String] The error message if one is defines, or an empty
48
+ # string.
49
+ def error_body(body)
50
+ if body && body['type'] == 'error' && body['value']
51
+ body['value']
52
+ else
53
+ ''
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,32 @@
1
+ require 'faraday'
2
+ require 'context-io/error/internal_server_error'
3
+ require 'context-io/error/service_unavailable'
4
+
5
+ module ContextIO
6
+ module Response
7
+ # Faraday middleware for raising errors on 5xx status codes
8
+ #
9
+ # @api private
10
+ class RaiseServerError < Faraday::Response::Middleware
11
+ # Raise an error if the response has a 5xx status code
12
+ #
13
+ # @raise [ContextIO::Error::ServerError] If the response has a 5xx status
14
+ # code.
15
+ # @raise [ContextIO::Error::InternalServerError] If the response has a 500
16
+ # status code.
17
+ # @raise [ContextIO::Error::ServiceUnavailable] If the response has a 503
18
+ # status code.
19
+ #
20
+ # @return [void]
21
+ def on_complete(env)
22
+ case env[:status].to_i
23
+ when 500
24
+ raise ContextIO::Error::InternalServerError.new('Something is technically wrong.', env[:response_headers])
25
+ when 503
26
+ raise ContextIO::Error::ServiceUnavailable.new('Could not connect to mail server.', env[:response_headers])
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,193 @@
1
+ require 'context-io/resource'
2
+
3
+ module ContextIO
4
+ # A message source. Create one of these for each mailbox a user has
5
+ #
6
+ # @api public
7
+ class Source < ContextIO::Resource
8
+ attr_accessor :label, :authentication_type, :port, :service_level, :username,
9
+ :server, :source_type, :sync_period, :use_ssl, :status
10
+ attr_reader :account_id, :email
11
+
12
+ # Public: Get all sources for given account.
13
+ #
14
+ # query - An optional Hash (default: {}) containing a query to filter the
15
+ # responses. For possible values see Context.IO API documentation.
16
+ # Returns an Array of Source objects.
17
+ #
18
+ # account - Account object or ID
19
+ def self.all(account, query = {})
20
+ return [] if account.nil?
21
+
22
+ account_id = account.is_a?(Account) ? account.id : account.to_s
23
+ get("/2.0/accounts/#{account_id}/sources", query).map do |msg|
24
+ Source.from_json(account_id, msg)
25
+ end
26
+ end
27
+
28
+ # Find a source for given ID
29
+ #
30
+ # @api public
31
+ #
32
+ # @param [String] label The label of the source to look up.
33
+ #
34
+ # @example Find the source with the labe 'foobar'
35
+ # ContextIO::Source.find('abcdef012345', 'foobar')
36
+ #
37
+ # @return [Source] The source with the given label.
38
+ def self.find(account, label)
39
+ return nil if account.nil? or label.to_s.empty?
40
+ account_id = account.is_a?(Account) ? account.id : account.to_s
41
+
42
+ Source.from_json(account_id, get("/2.0/accounts/#{account_id}/sources/#{label.to_s}"))
43
+ end
44
+
45
+ # Create a Source instance from the JSON returned by the Context.IO server
46
+ #
47
+ # @api private
48
+ #
49
+ # @param [Hash] The parsed JSON object returned by a Context.IO API request.
50
+ # See their documentation for what keys are possible.
51
+ #
52
+ # @return [Source] A source with the given attributes.
53
+ def self.from_json(account_id, json)
54
+ source = new(account_id, json)
55
+ end
56
+
57
+ def initialize(account_id, attributes = {})
58
+ raise ArgumentError if account_id.to_s.empty?
59
+
60
+ @account_id = account_id.to_s
61
+ @email = attributes['email']
62
+ @label = attributes['label'] || ''
63
+ @authentication_type = attributes['authentication_type']
64
+ @port = attributes['port'] || 143
65
+ @service_level = attributes['service_level']
66
+ @username = attributes['username']
67
+ @server = attributes['server']
68
+ @source_type = attributes['type'] || 'IMAP'
69
+ @sync_period = attributes['sync_period']
70
+ @use_ssl = attributes['use_ssl'] || false
71
+ @status = attributes['status']
72
+ @password = attributes['password']
73
+ @provider_token = attributes['provider_token']
74
+ @provider_token_secret = attributes['provider_token_secret']
75
+ @provider_consumer_key = attributes['provider_consumer_key']
76
+ end
77
+
78
+ # Returns all source's folders.
79
+ def folders
80
+ ContextIO::Folder.all(@account_id, @label)
81
+ end
82
+
83
+ # Sends the source data to Context.IO
84
+ #
85
+ # If the source has been sent to Context.IO before, this will update
86
+ # allowed source attributes.
87
+ #
88
+ # @api public
89
+ #
90
+ # @raise [ArgumentError] If required arguments are missing.
91
+ #
92
+ # @example Create a source
93
+ # source = ContextIO::Source.new(@account.id, {'email' => 'me@example.com', 'server' => 'imap@example.com',
94
+ # 'username' => "me", 'use_ssl' => true, 'port' => 143, 'type' => 'IMAP'})
95
+ # source.save
96
+ #
97
+ # @return [true, false] Whether the save succeeded or not.
98
+ def save
99
+ @label.to_s.empty? ? create_record : update_record
100
+ end
101
+
102
+ # Destroys current source object
103
+ def destroy
104
+ return false if @label.to_s.empty?
105
+
106
+ response = delete("/2.0/accounts/#{@account_id}/sources/#{@label}")
107
+ @label = '' if response['success']
108
+
109
+ response['success']
110
+ end
111
+
112
+ # Update attributes on the Source object and then send them to Context.IO
113
+ #
114
+ # @api public
115
+ #
116
+ # @param [Hash] attributes The attributes to update. Allowed
117
+ # attributes are status, sync period, service level, password,
118
+ # provider token, provider token secret and provider consumer key
119
+ #
120
+ # @example Update the Source sync period to one day
121
+ # source.update_attributes('sync_period' => '1d')
122
+ #
123
+ # @return [true, false] Whether the update succeeded or not.
124
+ def update_attributes(attributes = {})
125
+ raise ArgumentError.new("Cannot set attributes on new record") if @label.to_s.empty?
126
+
127
+ attributes.each do |k,v|
128
+ if ["status", "sync_period", "service_level", "password", "provider_token", "provider_token_secret", "provider_consumer_key"].include? k
129
+ send("#{k}=", v)
130
+ end
131
+ end
132
+ update_record
133
+ end
134
+
135
+ private
136
+
137
+ # Create the Source on Context.IO
138
+ #
139
+ # @api private
140
+ #
141
+ # @return [true, false] Whether the creation succeeded or not.
142
+ def create_record
143
+ if @email.to_s.empty? or @server.to_s.empty? or @username.to_s.empty? or @port.to_s.empty?
144
+ raise ArgumentError.new("Mandatory arguments are not set")
145
+ end
146
+
147
+ params = {}
148
+ params['email'] = @email.to_s
149
+ params['server'] = @server.to_s
150
+ params['username'] = @username.to_s
151
+ params['use_ssl'] = @use_ssl.to_s
152
+ params['port'] = @port.to_s
153
+ params['type'] = @source_type.to_s
154
+
155
+ # Optional parameters
156
+ params['service_level'] = @service_level if @service_level
157
+ params['sync_period'] = @sync_period if @sync_period
158
+ params['password'] = @password if @password
159
+ params['provider_token'] = @provider_token if @provider_token
160
+ params['provider_token_secret'] = @provider_token_secret if @provider_token_secret
161
+ params['provider_consumer_key'] = @provider_consumer_key if @provider_consumer_key
162
+
163
+ response = post("/2.0/accounts/#{@account_id}/sources", params)
164
+ response['success']
165
+ end
166
+
167
+ # Update existing Source on Context.IO
168
+ #
169
+ # Only sends the status, sync period, service level, password,
170
+ # provider token, provider token secret and provider consumer key
171
+ # as they are the only attributes the Context.IO API allows to be updated.
172
+ #
173
+ # @api private
174
+ #
175
+ # @return [true, false] Whether the update succeeded or not.
176
+ def update_record
177
+ return false if @label.to_s.empty?
178
+
179
+ atts = {}
180
+
181
+ atts["status"] = @status if @status
182
+ atts["sync_period"] = @sync_period if @sync_period
183
+ atts["service_level"] = @service_level if @service_level
184
+ atts["password"] = @password if @password
185
+ atts["provider_token"] = @provider_token if @provider_token
186
+ atts["provider_token_secret"] = @provider_token_secret if @provider_token_secret
187
+ atts["provider_consumer_key"] = @provider_consumer_key if @provider_consumer_key
188
+
189
+ response = post("/2.0/accounts/#{@account_id}/sources/#{@label}", atts)
190
+ response['success']
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,7 @@
1
+ module ContextIO
2
+ # The ContextIO version. Follows Semantic Versioning.
3
+ #
4
+ # @see http://semver.org
5
+ VERSION = '0.0.1'
6
+ end
7
+
@@ -0,0 +1,247 @@
1
+ require 'spec_helper'
2
+
3
+ describe ContextIO::Account do
4
+ let(:existing_account) do
5
+ stub_request(:post, 'https://api.context.io/2.0/accounts').
6
+ to_return(
7
+ :body => '{
8
+ "success": true,
9
+ "id": "1234567890abcdef",
10
+ "resource_url": "https://api.context.io/2.0/accounts/1234567890abcdef"
11
+ }')
12
+ account = ContextIO::Account.new(:email => 'foo@bar.com')
13
+ account.save
14
+
15
+ account
16
+ end
17
+ describe '.all' do
18
+ before(:each) do
19
+ json_accs = File.read(File.expand_path(File.join(File.dirname(__FILE__), "fixtures", "accounts.json")))
20
+ @stub = stub_request(:get, 'https://api.context.io/2.0/accounts').to_return(:body => json_accs)
21
+ end
22
+
23
+ it 'returns an array of Account objects' do
24
+ accounts = ContextIO::Account.all
25
+ accounts.first.should be_a(ContextIO::Account)
26
+ end
27
+
28
+ it 'calls the API request' do
29
+ ContextIO::Account.all
30
+
31
+ @stub.should have_been_requested
32
+ end
33
+
34
+ it 'sets the attributes on the Account objects' do
35
+ ContextIO::Account.all.first.id.should == 'abcdef0123456789'
36
+ end
37
+
38
+ it 'sends a query if one is given' do
39
+ @stub = @stub.with(:query => {
40
+ :email => 'me@example.com',
41
+ :status => 'OK',
42
+ :status_ok => '1',
43
+ :limit => '1',
44
+ :offset => '0'
45
+ })
46
+
47
+ ContextIO::Account.all(
48
+ :email => 'me@example.com',
49
+ :status => :ok,
50
+ :status_ok => true,
51
+ :limit => 1,
52
+ :offset => 0
53
+ )
54
+
55
+ @stub.should have_been_requested
56
+ end
57
+ end
58
+
59
+ describe '.find' do
60
+ before(:each) do
61
+ @stub = stub_request(:get, 'https://api.context.io/2.0/accounts/abcdef0123456789').to_return(
62
+ :body => '{
63
+ "id": "abcdef0123456789",
64
+ "username": "me.example.com_1234567890abcdef",
65
+ "created": 1234567890,
66
+ "suspended": 0,
67
+ "email_addresses": [ "me@example.com" ],
68
+ "first_name": "John",
69
+ "last_name": "Doe",
70
+ "password_expired": 0,
71
+ "sources": [{
72
+ "server": "mail.example.com",
73
+ "label": "me::mail.example.com",
74
+ "username": "me",
75
+ "port": 993,
76
+ "authentication_type": "password",
77
+ "use_ssl": true,
78
+ "sync_period": "1d",
79
+ "status": "OK",
80
+ "service_level": "pro",
81
+ "type": "imap"
82
+ }]
83
+ }'
84
+ )
85
+ end
86
+
87
+ it 'returns an instance of Account' do
88
+ ContextIO::Account.find('abcdef0123456789').should be_a(ContextIO::Account)
89
+ end
90
+
91
+ it 'calls the API request' do
92
+ ContextIO::Account.find('abcdef0123456789')
93
+
94
+ @stub.should have_been_requested
95
+ end
96
+
97
+ it 'sets the attributes on the Account object' do
98
+ ContextIO::Account.find('abcdef0123456789').id.should == 'abcdef0123456789'
99
+ end
100
+ end
101
+
102
+ describe '.new' do
103
+ it 'returns an instance of Account' do
104
+ ContextIO::Account.new.should be_a(ContextIO::Account)
105
+ end
106
+
107
+ it 'sets the given attributes on the Account object' do
108
+ account = ContextIO::Account.new(:first_name => 'John')
109
+ account.first_name.should == 'John'
110
+ end
111
+ end
112
+
113
+ describe '#save' do
114
+
115
+
116
+ it 'returns true if the save was successful' do
117
+ @stub = stub_request(:post, 'https://api.context.io/2.0/accounts').
118
+ with(:body => { :email => 'me@example.com' }).
119
+ to_return(
120
+ :body => '{
121
+ "success": true,
122
+ "id": "abcdef0123456789",
123
+ "resource_url": "https://api.context.io/2.0/accounts/abcdef0123456789"
124
+ }'
125
+ )
126
+
127
+ account = ContextIO::Account.new(:email => 'me@example.com')
128
+
129
+ account.save.should be_true
130
+ end
131
+
132
+ it 'returns false if the save was unsuccessful' do
133
+ @stub = stub_request(:post, 'https://api.context.io/2.0/accounts').
134
+ with(:body => { :email => 'me@example.com' }).
135
+ to_return(
136
+ :body => '{
137
+ "success": false
138
+ }'
139
+ )
140
+
141
+ account = ContextIO::Account.new(:email => 'me@example.com')
142
+
143
+ account.save.should be_false
144
+ end
145
+
146
+ context 'for a new account' do
147
+ before(:each) do
148
+ @stub = stub_request(:post, 'https://api.context.io/2.0/accounts').
149
+ with(:body => { :email => 'me@example.com' }).
150
+ to_return(
151
+ :body => '{
152
+ "success": true,
153
+ "id": "abcdef0123456789",
154
+ "resource_url": "https://api.context.io/2.0/accounts/abcdef0123456789"
155
+ }'
156
+ )
157
+ end
158
+
159
+ it 'calls the API request' do
160
+ ContextIO::Account.new(:email => 'me@example.com').save
161
+
162
+ @stub.should have_been_requested
163
+ end
164
+
165
+ it 'sets the ID of the account' do
166
+ account = ContextIO::Account.new(:email => 'me@example.com')
167
+ account.save
168
+ account.id.should == 'abcdef0123456789'
169
+ end
170
+ end
171
+
172
+ context 'for an existing account' do
173
+ it 'calls the API request' do
174
+ @stub = stub_request(:put,
175
+ 'https://api.context.io/2.0/accounts/1234567890abcdef').
176
+ with(:query => { :first_name => 'John' }).
177
+ to_return(
178
+ :body => '{
179
+ "success": true
180
+ }'
181
+ )
182
+
183
+ existing_account.first_name = 'John'
184
+ existing_account.save
185
+
186
+ @stub.should have_been_requested
187
+ end
188
+ end
189
+ end
190
+
191
+ describe '#update_attributes' do
192
+ it 'calls the API request' do
193
+ @stub = stub_request(:put,
194
+ 'https://api.context.io/2.0/accounts/1234567890abcdef').
195
+ with(:query => { :first_name => 'John' }).
196
+ to_return(
197
+ :body => '{
198
+ "success": true
199
+ }'
200
+ )
201
+
202
+ existing_account.update_attributes(:first_name => 'John')
203
+
204
+ @stub.should have_been_requested
205
+ end
206
+
207
+ it 'returns true if the update was successful' do
208
+ stub_request(:put, 'https://api.context.io/2.0/accounts/1234567890abcdef').
209
+ with(:query => { :first_name => 'John' }).
210
+ to_return(
211
+ :body => '{
212
+ "success": true
213
+ }'
214
+ )
215
+
216
+ existing_account.update_attributes(:first_name => 'John').should be_true
217
+ end
218
+
219
+ it 'returns false if the update was unsuccessful' do
220
+ stub_request(:put, 'https://api.context.io/2.0/accounts/1234567890abcdef').
221
+ with(:query => { :first_name => 'John' }).
222
+ to_return(
223
+ :body => '{
224
+ "success": false
225
+ }'
226
+ )
227
+
228
+ existing_account.update_attributes(:first_name => 'John').should be_false
229
+ end
230
+
231
+
232
+ it 'sets the attributes on the account object' do
233
+ stub_request(:put, 'https://api.context.io/2.0/accounts/1234567890abcdef').
234
+ with(:query => { :first_name => 'John' }).
235
+ to_return(
236
+ :body => '{
237
+ "success": true
238
+ }'
239
+ )
240
+
241
+ existing_account.update_attributes(:first_name => 'John')
242
+
243
+ existing_account.first_name.should == 'John'
244
+ end
245
+ end
246
+ end
247
+