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,23 @@
1
+ # encoding: utf-8
2
+
3
+ module ContextIO
4
+ # Methods related to authentication and configuration.
5
+ #
6
+ # @api private
7
+ module Authentication
8
+ # @return [Hash] The authentication details for OAuth.
9
+ def authentication
10
+ {
11
+ :consumer_key => consumer_key,
12
+ :consumer_secret => consumer_secret,
13
+ :token => nil,
14
+ :token_secret => nil,
15
+ }
16
+ end
17
+
18
+ # @return [Boolean] Whether authentication details are configured or not.
19
+ def authenticated?
20
+ authentication[:consumer_key] and authentication[:consumer_secret]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,103 @@
1
+ require 'context-io/version'
2
+
3
+ module ContextIO
4
+ # Defines constants and methods related to configuration
5
+ #
6
+ # You shouldn't interact with this module directly, as it's included in the
7
+ # {ContextIO} module. Mostly you'll use the {#configure} method on
8
+ # {ContextIO}, see the example on {#configure}.
9
+ #
10
+ # @see #configure
11
+ module Config
12
+ # The HTTP connection adapter that will be used to connect if none is set
13
+ DEFAULT_ADAPTER = :net_http
14
+
15
+ # The Faraday connection options if none is set
16
+ DEFAULT_CONNECTION_OPTIONS = {}
17
+
18
+ # The consumer key if none is set
19
+ DEFAULT_CONSUMER_KEY = nil
20
+
21
+ # The consumer secret if none is set
22
+ DEFAULT_CONSUMER_SECRET = nil
23
+
24
+ # The proxy server if none is set
25
+ DEFAULT_PROXY = nil
26
+
27
+ # The value of sent in the 'User-Agent' header if none is set
28
+ DEFAULT_USER_AGENT = "context-io ruby gem #{ContextIO::VERSION}"
29
+
30
+ # The configuration options that are settable.
31
+ VALID_OPTIONS_KEYS = [
32
+ :adapter,
33
+ :connection_options,
34
+ :consumer_key,
35
+ :consumer_secret,
36
+ :proxy,
37
+ :user_agent
38
+ ]
39
+
40
+ # @return [Symbol] The HTTP connection adapter. Check the Faraday
41
+ # documentation for possible adapters.
42
+ attr_accessor :adapter
43
+
44
+ # @return [Hash] Connection options passed to the Faraday connection object.
45
+ attr_accessor :connection_options
46
+
47
+ # @return [String] The OAuth consumer key received from Context.IO.
48
+ attr_accessor :consumer_key
49
+
50
+ # @return [String] The OAuth consumer secret received from Context.IO.
51
+ attr_accessor :consumer_secret
52
+
53
+ # @return [String, URI] The URI to the HTTP proxy to use.
54
+ attr_accessor :proxy
55
+
56
+ # @return [String] The value to be sent in the User-Agent header. Probably
57
+ # doesn't need to be changed unless you're writing a big app, or another
58
+ # library based on this one.
59
+ attr_accessor :user_agent
60
+
61
+ # Makes sure the default values are always set
62
+ #
63
+ # @api private
64
+ def self.extended(base)
65
+ base.reset
66
+ end
67
+
68
+ # Configure with a block
69
+ #
70
+ # @api public
71
+ #
72
+ # @example Configuring OAuth
73
+ # ContextIO.configure do |config|
74
+ # config.consumer_key = 'my_consumer_key'
75
+ # config.consumer_secret = 'my_consumer_secret'
76
+ # end
77
+ #
78
+ # @return [self]
79
+ def configure
80
+ yield self
81
+ self
82
+ end
83
+
84
+ # Reset the configuration to the default values
85
+ #
86
+ # @api public
87
+ #
88
+ # @example Reset the configuration
89
+ # ContextIO.adapter = :some_erroneous_thing
90
+ # ContextIO.reset
91
+ # ContextIO.adapter # => DEFAULT_ADAPTER
92
+ #
93
+ # @return [void]
94
+ def reset
95
+ self.adapter = DEFAULT_ADAPTER
96
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
97
+ self.consumer_key = DEFAULT_CONSUMER_KEY
98
+ self.consumer_secret = DEFAULT_CONSUMER_SECRET
99
+ self.proxy = DEFAULT_PROXY
100
+ self.user_agent = DEFAULT_USER_AGENT
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,45 @@
1
+ require 'faraday'
2
+ require 'context-io/core_ext/hash'
3
+ require 'context-io/request/oauth'
4
+ require 'context-io/response/parse_json'
5
+ require 'context-io/response/raise_client_error'
6
+ require 'context-io/response/raise_server_error'
7
+
8
+ module ContextIO
9
+ # Methods for creating a connection to the API server.
10
+ #
11
+ # @api private
12
+ # @private
13
+ module Connection
14
+ # Create and configure a Faraday connection
15
+ #
16
+ # @api private
17
+ #
18
+ # @param [true, false] raw Set this to true to disable JSON parsing of the
19
+ # response body.
20
+ #
21
+ # @return [Faraday::Connection] A Connection object that's correctly
22
+ # configured.
23
+ def connection(raw=false)
24
+ default_options = {
25
+ :headers => {
26
+ :accept => 'application/json',
27
+ :user_agent => ContextIO.user_agent,
28
+ },
29
+ :proxy => ContextIO.proxy,
30
+ :ssl => { :verify => false },
31
+ :url => 'https://api.context.io'
32
+ }
33
+ Faraday.new(default_options.deep_merge(ContextIO.connection_options)) do |builder|
34
+ builder.use ContextIO::Request::ContextIOOAuth, ContextIO.authentication if ContextIO.authenticated?
35
+ builder.use Faraday::Request::UrlEncoded
36
+ builder.use ContextIO::Response::RaiseClientError
37
+ builder.use ContextIO::Response::ParseJson unless raw
38
+ builder.use ContextIO::Response::RaiseServerError
39
+ builder.adapter(ContextIO.adapter)
40
+ end
41
+ end
42
+ private :connection
43
+ end
44
+ end
45
+
@@ -0,0 +1,31 @@
1
+ # Hash extensions
2
+ #
3
+ # This is borrowed from ActiveSupport. We don't want the entire ActiveSupport
4
+ # library (it's huge), so we'll just add this method.
5
+ class Hash
6
+ # Merge self with another hash recursively
7
+ #
8
+ # @api public
9
+ #
10
+ # @param [Hash] hash The hash to merge into this one.
11
+ #
12
+ # @example Merge two hashes with some common keys
13
+ # a_hash = { :foo => :bar, :baz => { :foobar => "hey" }}
14
+ # another_hash = { :foo => :foobar, :baz => { :foo => :bar }}
15
+ # a_hash.deep_merge(another_hash)
16
+ # # => { :foo => :foobar, :baz => { :foobar => "hey", :foo => :bar }}
17
+ #
18
+ # @return [Hash] The given hash merged recursively into this one.
19
+ def deep_merge(hash)
20
+ target = self.dup
21
+ hash.keys.each do |key|
22
+ if hash[key].is_a?(Hash) && self[key].is_a?(Hash)
23
+ target[key] = target[key].deep_merge(hash[key])
24
+ next
25
+ end
26
+ target[key] = hash[key]
27
+ end
28
+ target
29
+ end
30
+ end
31
+
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module ContextIO
4
+ # Base class for ContextIO exceptions.
5
+ #
6
+ # @api public
7
+ class Error < StandardError
8
+ # @return [Hash{String => String}] The HTTP headers of the response.
9
+ attr_reader :http_headers
10
+
11
+ # Initialize a new ContextIO error
12
+ #
13
+ # @api private
14
+ # @private
15
+ #
16
+ # @param [String] message The error message.
17
+ # @param [Hash{String => String}] http_headers The HTTP headers.
18
+ def initialize(message, http_headers)
19
+ @http_headers = Hash[http_headers]
20
+ super(message)
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,12 @@
1
+ require 'context-io/error/client_error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns the HTTP status code 400
5
+ #
6
+ # This usually means that some required info is missing.
7
+ #
8
+ # @api public
9
+ class Error::BadRequest < ContextIO::Error::ClientError
10
+ end
11
+ end
12
+
@@ -0,0 +1,10 @@
1
+ require 'context-io/error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns a 4xx HTTP status code
5
+ #
6
+ # @api public
7
+ class Error::ClientError < ContextIO::Error
8
+ end
9
+ end
10
+
@@ -0,0 +1,12 @@
1
+ require 'context-io/error/client_error'
2
+
3
+ module ContextIO
4
+ # Raised when ContextIO returns the HTTP status code 403
5
+ #
6
+ # This usually means that the resource isn't accessible.
7
+ #
8
+ # @api public
9
+ class Error::Forbidden < ContextIO::Error::ClientError
10
+ end
11
+ end
12
+
@@ -0,0 +1,10 @@
1
+ require 'context-io/error/server_error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns the HTTP status code 500.
5
+ #
6
+ # @api public
7
+ class Error::InternalServerError < ContextIO::Error::ServerError
8
+ end
9
+ end
10
+
@@ -0,0 +1,12 @@
1
+ require 'context-io/error/client_error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns the HTTP status code 404
5
+ #
6
+ # This means that the resource you tried to get doesn't exist.
7
+ #
8
+ # @api public
9
+ class Error::NotFound < ContextIO::Error::ClientError
10
+ end
11
+ end
12
+
@@ -0,0 +1,13 @@
1
+ require 'context-io/error/client_error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns the HTTP status code 402
5
+ #
6
+ # This means that you're trying to do something that your plan isn't allowed
7
+ # to do (anymore), so you need to log in and upgrade your plan.
8
+ #
9
+ # @api public
10
+ class Error::PaymentRequired < ContextIO::Error::ClientError
11
+ end
12
+ end
13
+
@@ -0,0 +1,10 @@
1
+ require 'context-io/error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns a 5xx HTTP status code.
5
+ #
6
+ # @api public
7
+ class Error::ServerError < ContextIO::Error
8
+ end
9
+ end
10
+
@@ -0,0 +1,13 @@
1
+ require 'context-io/error/server_error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns the HTTP status code 503
5
+ #
6
+ # The 503 status code means a request required a connection to the mail
7
+ # server, but that connection failed.
8
+ #
9
+ # @api public
10
+ class Error::ServiceUnavailable < ContextIO::Error::ServerError
11
+ end
12
+ end
13
+
@@ -0,0 +1,12 @@
1
+ require 'context-io/error/client_error'
2
+
3
+ module ContextIO
4
+ # Raised when Context.IO returns the HTTP status code 401
5
+ #
6
+ # This means that the OAuth signature can't be validated.
7
+ #
8
+ # @api public
9
+ class Error::Unauthorized < ContextIO::Error::ClientError
10
+ end
11
+ end
12
+
@@ -0,0 +1,234 @@
1
+ require 'context-io/resource'
2
+
3
+ module ContextIO
4
+ # A file found as an email attachment
5
+ #
6
+ # @api public
7
+ class File < Resource
8
+ # @api public
9
+ # @return [String] The ID of the file
10
+ attr_reader :id
11
+
12
+ # @api public
13
+ # @return [Integer] The size of the file, in bytes.
14
+ attr_reader :size
15
+
16
+ # @api public
17
+ # @return [String] The MIME type of the file.
18
+ attr_reader :type
19
+
20
+ # @api public
21
+ # @return [String] The subject of the message this file was attached to
22
+ attr_reader :subject
23
+
24
+ # @api public
25
+ # @return [Time] When this file was sent
26
+ attr_reader :date
27
+
28
+ # @api public
29
+ # @return [Hash] Information on the different addresses attached to this
30
+ # file's message.
31
+ attr_reader :addresses
32
+
33
+ # @api public
34
+ # @return [String] The full filename
35
+ attr_reader :file_name
36
+
37
+ # @api public
38
+ # @return [Integer]
39
+ attr_reader :body_section
40
+
41
+ # @api public
42
+ # @return [true, false] Whether the file supports preview
43
+ attr_reader :supports_preview
44
+
45
+ # @api public
46
+ # @return [String] The (Context.IO) ID of the message this file was attached to
47
+ attr_reader :message_id
48
+
49
+ # @api public
50
+ # @return [Time] When Context.IO indexed the file (not the same as when it
51
+ # was sent)
52
+ attr_reader :date_indexed
53
+
54
+ # @api public
55
+ # @return [String] The email message ID (The Message-ID header)
56
+ attr_reader :email_message_id
57
+
58
+ # @api public
59
+ # @return [Hash] Information about the people involved with the message
60
+ attr_reader :person_info
61
+
62
+ # @api public
63
+ # @return [Array] The file name split up into parts
64
+ attr_reader :file_name_structure
65
+
66
+ # Get all files for a given account, optionally filtered with a query
67
+ #
68
+ # @see Account#messages
69
+ #
70
+ # @api public
71
+ #
72
+ # @param [Account, #to_s] account The account or account ID to search for
73
+ # files in.
74
+ # @param [Hash] query A query to filter files by.
75
+ # @option query [String, Regexp] :file_name The filename to search for. The
76
+ # string can contain shell globs ('*', '?' and '[]').
77
+ # @option query [String] :email The email address of the contact for whom
78
+ # you want the latest files exchanged with. By "exchanged with contact X",
79
+ # we mean any email received from contact X, sent to contact X or sent by
80
+ # anyone to both contact X and the source owner.
81
+ # @option query [String] :to The email address of a contact files have been
82
+ # sent to.
83
+ # @option query [String] :from The email address of a contact files have
84
+ # been sent from.
85
+ # @option query [String] :cc The email address of a contact CC'ed on the
86
+ # messages.
87
+ # @option query [String] :bcc The email address of a contact BCC'ed on the
88
+ # messages.
89
+ # @option query [#to_i] :date_before Only include files attached to messages
90
+ # sent before this timestamp. The value of this filter is applied to the
91
+ # Date header of the message, which refers to the time the message is sent
92
+ # from the origin.
93
+ # @option query [#to_i] :date_after Only include files attached to messages
94
+ # sent after this timestamp. The value of this filter is applied to the
95
+ # Date header of the message, which refers to the time the message is sent
96
+ # from the origin.
97
+ # @option query [#to_i] :indexed_before Only include files attached to
98
+ # messages indexed before this timestamp. This is not the same as the date
99
+ # of the email, it is the time Context.IO indexed this message.
100
+ # @option query [#to_i] :indexed_after Only include files attached to
101
+ # messages indexed after this timestamp. This is not the same as the date
102
+ # of the email, it is the time Context.IO indexed this message.
103
+ # @option query [true, false] :group_by_revisions (false) If this is set to
104
+ # true, the method will return an array of Hashes, where each Hash
105
+ # represents a group of revisions of the same file. The Hash has an
106
+ # `:occurences` field, which is an Array of {File} objects, a `:file_name`
107
+ # field, which is the name of the file, and a `:latest_date` field, which
108
+ # is a Time object representing the last time a message with this file was
109
+ # sent.
110
+ # @option query [#to_i] :limit The maximum count of results to return.
111
+ # @option query [#to_i] :offset (0) The offset to begin returning files at.
112
+ #
113
+ # @example Get all files for the account
114
+ # files = ContextIO::File.all(account)
115
+ #
116
+ # @example Get 10 files that we have sent
117
+ # files = ContextIO::File.all(account,
118
+ # :from => account.email_addresses.first,
119
+ # :limit => 10
120
+ # )
121
+ #
122
+ # @example Find PDF files
123
+ # files = ContextIO::File.all(account, :file_name => '*.pdf')
124
+ #
125
+ # @example Find JP(E)G files
126
+ # files = ContextIO::File.all(account, :file_name => /\.jpe?g$/)
127
+ #
128
+ # @return [Array<File>, Array<Hash>] The matching file objects. If the
129
+ # `:group_by_revisions` flag is set, the return value changes, see the
130
+ # documentation for that flag above.
131
+ def self.all(account, query={})
132
+ if query[:file_name] && query[:file_name].is_a?(Regexp)
133
+ query[:file_name] = "/#{query[:file_name].source}/"
134
+ end
135
+
136
+ [:date_before, :date_after, :indexed, :indexed_after].each do |field|
137
+ if query[field] && query[field].respond_to?(:to_i)
138
+ query[field] = query[field].to_i
139
+ end
140
+ end
141
+
142
+ if query[:group_by_revisions]
143
+ query[:group_by_revisions] = query[:group_by_revisions] ? '1' : '0'
144
+ end
145
+
146
+ account_id = account.is_a?(Account) ? account.id : account.to_s
147
+ get("/2.0/accounts/#{account_id}/files", query).map do |file|
148
+ if query[:group_by_revisions]
149
+ occurences = file['occurences'].map do |file|
150
+ File.from_json(account_id, file)
151
+ end
152
+
153
+ {
154
+ :occurences => occurences,
155
+ :file_name => file['file_name'],
156
+ :latest_date => Time.at(file['latest_date'].to_i)
157
+ }
158
+ else
159
+ File.from_json(account_id, file)
160
+ end
161
+ end
162
+ end
163
+
164
+ # Fetch the content of the message.
165
+ #
166
+ # @api public
167
+ #
168
+ # @note Data transfer for this call is metered and charged at the end of the
169
+ # month. See [Context.IO's pricing page](http://context.io/pricing) for
170
+ # more info.
171
+ #
172
+ # @return [String] The raw contents of the file.
173
+ def content
174
+ get("/2.0/accounts/#@account_id/files/#@id/content", :raw => true)
175
+ end
176
+
177
+ # Create a File with the JSON from Context.IO.
178
+ #
179
+ # @api private
180
+ #
181
+ # @param [String] account_id The account ID.
182
+ # @param [Hash] json The parsed JSON object returned by a Context.IO API
183
+ # request. See their documentation for possible keys.
184
+ #
185
+ # @return [File] A file with the given attributes.
186
+ def self.from_json(account_id, json)
187
+ file = new
188
+ file.instance_eval do
189
+ @id = json['file_id']
190
+ @account_id = account_id
191
+ @size = json['size']
192
+ @type = json['type']
193
+ @subject = json['subject']
194
+ @date = Time.at(json['date'])
195
+ @addresses = {}
196
+ json['addresses'].each do |type, info|
197
+ @addresses[type.to_sym] = {}
198
+ if info.is_a?(Hash)
199
+ info.each do |key, value|
200
+ @addresses[type.to_sym][key.to_sym] = value
201
+ end
202
+ elsif info.is_a?(Array)
203
+ @addresses[type.to_sym] = []
204
+ info.each do |email_info|
205
+ @addresses[type.to_sym] << {}
206
+ email_info.each do |key, value|
207
+ @addresses[type.to_sym].last[key.to_sym] = value
208
+ end
209
+ end
210
+ end
211
+ end
212
+ @file_name = json['file_name']
213
+ @body_section = json['body_section']
214
+ @supports_preview = json['supports_preview']
215
+ @message_id = json['message_id']
216
+ @date_indexed = Time.at(json['date_indexed'])
217
+ @email_message_id = json['email_message_id']
218
+ @person_info = {}
219
+ json['person_info'].each do |email, info|
220
+ @person_info[email] = {}
221
+ info.each do |key, value|
222
+ @person_info[email][key.to_sym] = value
223
+ end
224
+ end
225
+ @file_name_structure = []
226
+ json['file_name_structure'].each do |part|
227
+ @file_name_structure << [part.first, part.last.to_sym]
228
+ end
229
+ end
230
+
231
+ file
232
+ end
233
+ end
234
+ end