context-io 0.0.1

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.
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
+