nylas-shipmnts 3.0.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.
- checksums.yaml +7 -0
- data/lib/account.rb +32 -0
- data/lib/api_account.rb +22 -0
- data/lib/api_thread.rb +85 -0
- data/lib/calendar.rb +16 -0
- data/lib/contact.rb +10 -0
- data/lib/draft.rb +49 -0
- data/lib/event.rb +47 -0
- data/lib/expanded_message.rb +20 -0
- data/lib/file.rb +40 -0
- data/lib/folder.rb +12 -0
- data/lib/label.rb +4 -0
- data/lib/message.rb +102 -0
- data/lib/mixins.rb +26 -0
- data/lib/nylas.rb +394 -0
- data/lib/parameters.rb +26 -0
- data/lib/restful_model.rb +82 -0
- data/lib/restful_model_collection.rb +162 -0
- data/lib/time_attr_accessor.rb +12 -0
- data/lib/version.rb +3 -0
- metadata +295 -0
data/lib/mixins.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Nylas
|
2
|
+
module ReadUnreadMethods
|
3
|
+
def update_param!(param, value)
|
4
|
+
update('PUT', '', {
|
5
|
+
param => value,
|
6
|
+
})
|
7
|
+
end
|
8
|
+
|
9
|
+
def mark_as_read!
|
10
|
+
update_param!(:unread, false)
|
11
|
+
end
|
12
|
+
|
13
|
+
def mark_as_unread!
|
14
|
+
update_param!(:unread, true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def star!
|
18
|
+
update_param!(:starred, true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def unstar!
|
22
|
+
update_param!(:starred, false)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
data/lib/nylas.rb
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rest-client'
|
3
|
+
require 'yajl'
|
4
|
+
require 'em-http'
|
5
|
+
|
6
|
+
|
7
|
+
require 'ostruct'
|
8
|
+
require 'active_support/core_ext/hash'
|
9
|
+
|
10
|
+
require 'account'
|
11
|
+
require 'api_account'
|
12
|
+
require 'api_thread'
|
13
|
+
require 'calendar'
|
14
|
+
require 'account'
|
15
|
+
require 'message'
|
16
|
+
require 'expanded_message'
|
17
|
+
require 'draft'
|
18
|
+
require 'contact'
|
19
|
+
require 'file'
|
20
|
+
require 'calendar'
|
21
|
+
require 'event'
|
22
|
+
require 'folder'
|
23
|
+
require 'restful_model'
|
24
|
+
require 'restful_model_collection'
|
25
|
+
require 'version'
|
26
|
+
|
27
|
+
module Nylas
|
28
|
+
Error = Class.new(::StandardError)
|
29
|
+
NoAuthToken = Class.new(Error)
|
30
|
+
UnexpectedAccountAction = Class.new(Error)
|
31
|
+
UnexpectedResponse = Class.new(Error)
|
32
|
+
class APIError < Error
|
33
|
+
attr_accessor :type
|
34
|
+
attr_accessor :message
|
35
|
+
attr_accessor :server_error
|
36
|
+
|
37
|
+
def initialize(type, message, server_error = nil)
|
38
|
+
super(message)
|
39
|
+
self.type = type
|
40
|
+
self.message = message
|
41
|
+
self.server_error = server_error
|
42
|
+
end
|
43
|
+
end
|
44
|
+
AccessDenied = Class.new(APIError)
|
45
|
+
ResourceNotFound = Class.new(APIError)
|
46
|
+
InvalidRequest = Class.new(APIError)
|
47
|
+
MessageRejected = Class.new(APIError)
|
48
|
+
SendingQuotaExceeded = Class.new(APIError)
|
49
|
+
ServiceUnavailable = Class.new(APIError)
|
50
|
+
BadGateway = Class.new(APIError)
|
51
|
+
InternalError = Class.new(APIError)
|
52
|
+
MailProviderError = Class.new(APIError)
|
53
|
+
|
54
|
+
HTTP_CODE_TO_EXCEPTIONS = {
|
55
|
+
400 => InvalidRequest,
|
56
|
+
402 => MessageRejected,
|
57
|
+
403 => AccessDenied,
|
58
|
+
404 => ResourceNotFound,
|
59
|
+
422 => MailProviderError,
|
60
|
+
429 => SendingQuotaExceeded,
|
61
|
+
500 => InternalError,
|
62
|
+
502 => BadGateway,
|
63
|
+
503 => ServiceUnavailable,
|
64
|
+
}.freeze
|
65
|
+
|
66
|
+
def self.http_code_to_exception(http_code)
|
67
|
+
HTTP_CODE_TO_EXCEPTIONS.fetch(http_code, APIError)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.interpret_response(result, result_content, options = {})
|
71
|
+
|
72
|
+
# We expected a certain kind of object, but the API didn't return anything
|
73
|
+
raise UnexpectedResponse.new if options[:expected_class] && result_content.empty?
|
74
|
+
|
75
|
+
# If it's already parsed, or if we've received an actual raw payload on success, don't parse
|
76
|
+
if options[:result_parsed] || (options[:raw_response] && result.code.to_i == 200)
|
77
|
+
response = result_content
|
78
|
+
else
|
79
|
+
response = JSON.parse(result_content)
|
80
|
+
end
|
81
|
+
|
82
|
+
if result.code.to_i != 200
|
83
|
+
exc = Nylas.http_code_to_exception(result.code.to_i)
|
84
|
+
if response.is_a?(Hash)
|
85
|
+
raise exc.new(response['type'], response['message'], response.fetch('server_error', nil))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
raise UnexpectedResponse.new(result.msg) if result.is_a?(Net::HTTPClientError)
|
90
|
+
raise UnexpectedResponse.new if options[:expected_class] && !response.is_a?(options[:expected_class])
|
91
|
+
response
|
92
|
+
|
93
|
+
rescue JSON::ParserError => e
|
94
|
+
# Handle parsing errors
|
95
|
+
raise UnexpectedResponse.new(e.message)
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
class API
|
100
|
+
attr_accessor :api_server
|
101
|
+
attr_reader :access_token
|
102
|
+
attr_reader :app_id
|
103
|
+
attr_reader :app_secret
|
104
|
+
|
105
|
+
def initialize(app_id, app_secret, access_token = nil, api_server = 'https://api.nylas.com',
|
106
|
+
service_domain = 'api.nylas.com')
|
107
|
+
raise "When overriding the Nylas API server address, you must include https://" unless api_server.include?('://')
|
108
|
+
@api_server = api_server
|
109
|
+
@access_token = access_token
|
110
|
+
@app_secret = app_secret
|
111
|
+
@app_id = app_id
|
112
|
+
@service_domain = service_domain
|
113
|
+
@version = Nylas::VERSION
|
114
|
+
|
115
|
+
if ::RestClient.before_execution_procs.empty?
|
116
|
+
::RestClient.add_before_execution_proc do |req, params|
|
117
|
+
req.add_field('X-Inbox-API-Wrapper', 'ruby')
|
118
|
+
req['User-Agent'] = "Nylas Ruby SDK #{@version} - #{RUBY_VERSION}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def url_for_path(path)
|
124
|
+
raise NoAuthToken.new if @access_token == nil and (@app_secret != nil or @app_id != nil)
|
125
|
+
protocol, domain = @api_server.split('//')
|
126
|
+
"#{protocol}//#{@access_token}:@#{domain}#{path}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def url_for_authentication(redirect_uri, login_hint = '', options = {})
|
130
|
+
params = {
|
131
|
+
:client_id => @app_id,
|
132
|
+
:trial => options.fetch(:trial, false),
|
133
|
+
:response_type => 'code',
|
134
|
+
:scope => 'email',
|
135
|
+
:login_hint => login_hint,
|
136
|
+
:redirect_uri => redirect_uri,
|
137
|
+
}
|
138
|
+
|
139
|
+
if options.has_key?(:state) then
|
140
|
+
params[:state] = options[:state]
|
141
|
+
end
|
142
|
+
|
143
|
+
"https://#{@service_domain}/oauth/authorize?" + params.to_query
|
144
|
+
end
|
145
|
+
|
146
|
+
def url_for_management
|
147
|
+
protocol, domain = @api_server.split('//')
|
148
|
+
accounts_path = "#{protocol}//#{@app_secret}:@#{domain}/a/#{@app_id}/accounts"
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_access_token(token)
|
152
|
+
@access_token = token
|
153
|
+
end
|
154
|
+
|
155
|
+
def token_for_code(code)
|
156
|
+
data = {
|
157
|
+
'client_id' => app_id,
|
158
|
+
'client_secret' => app_secret,
|
159
|
+
'grant_type' => 'authorization_code',
|
160
|
+
'code' => code
|
161
|
+
}
|
162
|
+
|
163
|
+
::RestClient.post("https://#{@service_domain}/oauth/token", data) do |response, request, result|
|
164
|
+
json = Nylas.interpret_response(result, response, :expected_class => Object)
|
165
|
+
return json['access_token']
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# API Methods
|
170
|
+
def threads
|
171
|
+
@threads ||= RestfulModelCollection.new(Thread, self)
|
172
|
+
end
|
173
|
+
|
174
|
+
def messages(expanded: false)
|
175
|
+
@messages ||= Hash.new do |h, is_expanded|
|
176
|
+
h[is_expanded] = \
|
177
|
+
if is_expanded
|
178
|
+
RestfulModelCollection.new(ExpandedMessage, self, view: 'expanded')
|
179
|
+
else
|
180
|
+
RestfulModelCollection.new(Message, self)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
@messages[expanded]
|
184
|
+
end
|
185
|
+
|
186
|
+
def files
|
187
|
+
@files ||= RestfulModelCollection.new(File, self)
|
188
|
+
end
|
189
|
+
|
190
|
+
def drafts
|
191
|
+
@drafts ||= RestfulModelCollection.new(Draft, self)
|
192
|
+
end
|
193
|
+
|
194
|
+
def contacts
|
195
|
+
@contacts ||= RestfulModelCollection.new(Contact, self)
|
196
|
+
end
|
197
|
+
|
198
|
+
def calendars
|
199
|
+
@calendars ||= RestfulModelCollection.new(Calendar, self)
|
200
|
+
end
|
201
|
+
|
202
|
+
def events
|
203
|
+
@events ||= RestfulModelCollection.new(Event, self)
|
204
|
+
end
|
205
|
+
|
206
|
+
def folders
|
207
|
+
@folders ||= RestfulModelCollection.new(Folder, self)
|
208
|
+
end
|
209
|
+
|
210
|
+
def labels
|
211
|
+
@labels ||= RestfulModelCollection.new(Label, self)
|
212
|
+
end
|
213
|
+
|
214
|
+
def account
|
215
|
+
path = self.url_for_path("/account")
|
216
|
+
|
217
|
+
RestClient.get(path, {}) do |response,request,result|
|
218
|
+
json = Nylas.interpret_response(result, response, {:expected_class => Object})
|
219
|
+
model = APIAccount.new(self)
|
220
|
+
model.inflate(json)
|
221
|
+
model
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def using_hosted_api?
|
226
|
+
return !@app_id.nil?
|
227
|
+
end
|
228
|
+
|
229
|
+
def accounts
|
230
|
+
if self.using_hosted_api?
|
231
|
+
@accounts ||= ManagementModelCollection.new(Account, self)
|
232
|
+
else
|
233
|
+
@accounts ||= RestfulModelCollection.new(APIAccount, self)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def latest_cursor
|
238
|
+
# Get the cursor corresponding to a specific timestamp.
|
239
|
+
path = self.url_for_path("/delta/latest_cursor")
|
240
|
+
|
241
|
+
cursor = nil
|
242
|
+
|
243
|
+
RestClient.post(path, :content_type => :json) do |response,request,result|
|
244
|
+
json = Nylas.interpret_response(result, response, {:expected_class => Object})
|
245
|
+
cursor = json["cursor"]
|
246
|
+
end
|
247
|
+
|
248
|
+
cursor
|
249
|
+
end
|
250
|
+
|
251
|
+
OBJECTS_TABLE = {
|
252
|
+
"account" => Nylas::Account,
|
253
|
+
"calendar" => Nylas::Calendar,
|
254
|
+
"draft" => Nylas::Draft,
|
255
|
+
"thread" => Nylas::Thread,
|
256
|
+
"contact" => Nylas::Contact,
|
257
|
+
"event" => Nylas::Event,
|
258
|
+
"file" => Nylas::File,
|
259
|
+
"message" => Nylas::Message,
|
260
|
+
"folder" => Nylas::Folder,
|
261
|
+
"label" => Nylas::Label,
|
262
|
+
}
|
263
|
+
|
264
|
+
# It's possible to ask the API to expand objects.
|
265
|
+
# In this case, we do the right thing and return
|
266
|
+
# an expanded object.
|
267
|
+
EXPANDED_OBJECTS_TABLE = {
|
268
|
+
"message" => Nylas::ExpandedMessage,
|
269
|
+
}
|
270
|
+
|
271
|
+
def _build_exclude_types(exclude_types)
|
272
|
+
exclude_string = "&exclude_types="
|
273
|
+
|
274
|
+
exclude_types.each do |value|
|
275
|
+
count = 0
|
276
|
+
if OBJECTS_TABLE.has_value?(value)
|
277
|
+
param_name = OBJECTS_TABLE.key(value)
|
278
|
+
exclude_string += "#{param_name},"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
exclude_string = exclude_string[0..-2]
|
283
|
+
end
|
284
|
+
|
285
|
+
def deltas(cursor, exclude_types=[], expanded_view=false)
|
286
|
+
return enum_for(:deltas, cursor, exclude_types, expanded_view) unless block_given?
|
287
|
+
|
288
|
+
exclude_string = ""
|
289
|
+
|
290
|
+
if exclude_types.any?
|
291
|
+
exclude_string = _build_exclude_types(exclude_types)
|
292
|
+
end
|
293
|
+
|
294
|
+
# loop and yield deltas until we've come to the end.
|
295
|
+
loop do
|
296
|
+
path = self.url_for_path("/delta?exclude_folders=false&cursor=#{cursor}#{exclude_string}")
|
297
|
+
if expanded_view
|
298
|
+
path += '&view=expanded'
|
299
|
+
end
|
300
|
+
|
301
|
+
json = nil
|
302
|
+
|
303
|
+
RestClient.get(path) do |response,request,result|
|
304
|
+
json = Nylas.interpret_response(result, response, {:expected_class => Object})
|
305
|
+
end
|
306
|
+
|
307
|
+
start_cursor = json["cursor_start"]
|
308
|
+
end_cursor = json["cursor_end"]
|
309
|
+
|
310
|
+
json["deltas"].each do |delta|
|
311
|
+
if not OBJECTS_TABLE.has_key?(delta['object'])
|
312
|
+
next
|
313
|
+
end
|
314
|
+
|
315
|
+
cls = OBJECTS_TABLE[delta['object']]
|
316
|
+
if EXPANDED_OBJECTS_TABLE.has_key?(delta['object']) and expanded_view
|
317
|
+
cls = EXPANDED_OBJECTS_TABLE[delta['object']]
|
318
|
+
end
|
319
|
+
|
320
|
+
obj = cls.new(self)
|
321
|
+
|
322
|
+
case delta["event"]
|
323
|
+
when 'create', 'modify'
|
324
|
+
obj.inflate(delta['attributes'])
|
325
|
+
obj.cursor = delta["cursor"]
|
326
|
+
yield delta["event"], obj
|
327
|
+
when 'delete'
|
328
|
+
obj.id = delta["id"]
|
329
|
+
obj.cursor = delta["cursor"]
|
330
|
+
yield delta["event"], obj
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
break if start_cursor == end_cursor
|
335
|
+
cursor = end_cursor
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def delta_stream(cursor, exclude_types=[], timeout=0, expanded_view=false)
|
340
|
+
raise 'Please provide a block for receiving the delta objects' if !block_given?
|
341
|
+
|
342
|
+
exclude_string = ""
|
343
|
+
|
344
|
+
if exclude_types.any?
|
345
|
+
exclude_string = _build_exclude_types(exclude_types)
|
346
|
+
end
|
347
|
+
|
348
|
+
# loop and yield deltas indefinitely.
|
349
|
+
path = self.url_for_path("/delta/streaming?exclude_folders=false&cursor=#{cursor}#{exclude_string}")
|
350
|
+
if expanded_view
|
351
|
+
path += '&view=expanded'
|
352
|
+
end
|
353
|
+
|
354
|
+
parser = Yajl::Parser.new(:symbolize_keys => false)
|
355
|
+
parser.on_parse_complete = proc do |data|
|
356
|
+
delta = Nylas.interpret_response(OpenStruct.new(:code => '200'), data, {:expected_class => Object, :result_parsed => true})
|
357
|
+
|
358
|
+
if not OBJECTS_TABLE.has_key?(delta['object'])
|
359
|
+
next
|
360
|
+
end
|
361
|
+
|
362
|
+
cls = OBJECTS_TABLE[delta['object']]
|
363
|
+
if EXPANDED_OBJECTS_TABLE.has_key?(delta['object']) and expanded_view
|
364
|
+
cls = EXPANDED_OBJECTS_TABLE[delta['object']]
|
365
|
+
end
|
366
|
+
|
367
|
+
obj = cls.new(self)
|
368
|
+
|
369
|
+
case delta["event"]
|
370
|
+
when 'create', 'modify'
|
371
|
+
obj.inflate(delta['attributes'])
|
372
|
+
obj.cursor = delta["cursor"]
|
373
|
+
yield delta["event"], obj
|
374
|
+
when 'delete'
|
375
|
+
obj.id = delta["id"]
|
376
|
+
obj.cursor = delta["cursor"]
|
377
|
+
yield delta["event"], obj
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
http = EventMachine::HttpRequest.new(path, :connect_timeout => 0, :inactivity_timeout => timeout).get(:keepalive => true)
|
382
|
+
|
383
|
+
# set a callback on the HTTP stream that parses incoming chunks as they come in
|
384
|
+
http.stream do |chunk|
|
385
|
+
parser << chunk
|
386
|
+
end
|
387
|
+
|
388
|
+
http.errback do
|
389
|
+
raise UnexpectedResponse.new http.error
|
390
|
+
end
|
391
|
+
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
data/lib/parameters.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Nylas
|
2
|
+
module Parameters
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def parameters
|
8
|
+
self.class.instance_variable_get("@parameters")
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def parameter(*params)
|
13
|
+
@parameters ||= []
|
14
|
+
params.each do |param|
|
15
|
+
attr_accessor param
|
16
|
+
@parameters << param.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def inherited(subclass)
|
21
|
+
parameters = instance_variable_get("@parameters") || []
|
22
|
+
subclass.instance_variable_set("@parameters", parameters.clone)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'time_attr_accessor'
|
2
|
+
require 'parameters'
|
3
|
+
|
4
|
+
module Nylas
|
5
|
+
class RestfulModel
|
6
|
+
extend Nylas::TimeAttrAccessor
|
7
|
+
include Nylas::Parameters
|
8
|
+
|
9
|
+
parameter :id
|
10
|
+
parameter :account_id
|
11
|
+
parameter :cursor # Only used by the delta sync API
|
12
|
+
time_attr_accessor :created_at
|
13
|
+
attr_reader :raw_json
|
14
|
+
|
15
|
+
def self.collection_name
|
16
|
+
"#{self.to_s.downcase}s".split('::').last
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(api, account_id = nil)
|
20
|
+
raise StandardError.new unless api.class <= Nylas::API
|
21
|
+
@account_id = account_id
|
22
|
+
@_api = api
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(comparison_object)
|
26
|
+
comparison_object.equal?(self) || (comparison_object.instance_of?(self.class) && comparison_object.id == id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def inflate(json)
|
30
|
+
@raw_json = json
|
31
|
+
parameters.each do |property_name|
|
32
|
+
send("#{property_name}=", json[property_name]) if json.has_key?(property_name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def save!(params={})
|
37
|
+
if id
|
38
|
+
update('PUT', '', as_json(), params)
|
39
|
+
else
|
40
|
+
update('POST', '', as_json(), params)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def url(action = "")
|
45
|
+
action = "/#{action}" unless action.empty?
|
46
|
+
@_api.url_for_path("/#{self.class.collection_name}/#{id}#{action}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def as_json(options = {})
|
50
|
+
hash = {}
|
51
|
+
parameters.each do |getter|
|
52
|
+
unless options[:except] && options[:except].include?(getter)
|
53
|
+
value = send(getter)
|
54
|
+
unless value.is_a?(RestfulModelCollection)
|
55
|
+
value = value.as_json(options) if value.respond_to?(:as_json)
|
56
|
+
hash[getter] = value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
hash
|
61
|
+
end
|
62
|
+
|
63
|
+
def update(http_method, action, data = {}, params = {})
|
64
|
+
http_method = http_method.downcase
|
65
|
+
|
66
|
+
::RestClient.send(http_method, self.url(action), data.to_json, :content_type => :json, :params => params) do |response, request, result|
|
67
|
+
unless http_method == 'delete'
|
68
|
+
json = Nylas.interpret_response(result, response, :expected_class => Object)
|
69
|
+
inflate(json)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def destroy(params = {})
|
76
|
+
::RestClient.send('delete', self.url, :params => params) do |response, request, result|
|
77
|
+
Nylas.interpret_response(result, response, {:raw_response => true})
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|