nylas 6.2.3 → 6.8.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.
- checksums.yaml +4 -4
- data/lib/nylas/client.rb +43 -0
- data/lib/nylas/errors.rb +6 -4
- data/lib/nylas/handler/api_operations.rb +45 -27
- data/lib/nylas/handler/http_client.rb +265 -26
- data/lib/nylas/handler/service_account_signer.rb +112 -0
- data/lib/nylas/resources/applications.rb +14 -0
- data/lib/nylas/resources/bookings.rb +4 -2
- data/lib/nylas/resources/calendars.rb +10 -3
- data/lib/nylas/resources/domains.rb +269 -0
- data/lib/nylas/resources/events.rb +17 -1
- data/lib/nylas/resources/folders.rb +5 -0
- data/lib/nylas/resources/lists.rb +36 -0
- data/lib/nylas/resources/messages.rb +24 -1
- data/lib/nylas/resources/notetakers.rb +142 -0
- data/lib/nylas/resources/policies.rb +124 -0
- data/lib/nylas/resources/redirect_uris.rb +2 -2
- data/lib/nylas/resources/rules.rb +161 -0
- data/lib/nylas/resources/webhooks.rb +5 -4
- data/lib/nylas/resources/workspaces.rb +111 -0
- data/lib/nylas/utils/file_utils.rb +7 -3
- data/lib/nylas/version.rb +1 -1
- data/lib/nylas.rb +8 -15
- metadata +48 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 537c2669d5e30d03ad70d6acbd18c782865038fcff32779ffca36ece16234900
|
|
4
|
+
data.tar.gz: bf78777657d01f6907505b473d5304419d95a8f8984455604fe59c86e5a1279b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb7bca82b3c7dbb97b8365a4ef5882d86f41256952e17f854e03e1eff92d16a8cee53f57a971a8bc70209d26a7f5721915a6d458b8582adc24aaf9a055257469
|
|
7
|
+
data.tar.gz: 52196420bcaeb94ea0b31c2a513b57dee5a8dd7eed2f5689564cba0d84e2b63b91751a0616881e0813b63a02a29cd16a1064d78f0a57b6e1e6c4ce31fa0e395f
|
data/lib/nylas/client.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "resources/auth"
|
|
|
8
8
|
require_relative "resources/webhooks"
|
|
9
9
|
require_relative "resources/applications"
|
|
10
10
|
require_relative "resources/folders"
|
|
11
|
+
require_relative "resources/lists"
|
|
12
|
+
require_relative "resources/notetakers"
|
|
11
13
|
require_relative "resources/scheduler"
|
|
12
14
|
|
|
13
15
|
module Nylas
|
|
@@ -98,6 +100,13 @@ module Nylas
|
|
|
98
100
|
Grants.new(self)
|
|
99
101
|
end
|
|
100
102
|
|
|
103
|
+
# The list resources for your Nylas application.
|
|
104
|
+
#
|
|
105
|
+
# @return [Nylas::Lists] List resources for your Nylas application
|
|
106
|
+
def lists
|
|
107
|
+
Lists.new(self)
|
|
108
|
+
end
|
|
109
|
+
|
|
101
110
|
# The message resources for your Nylas application.
|
|
102
111
|
#
|
|
103
112
|
# @return [Nylas::Messages] Message resources for your Nylas application
|
|
@@ -112,6 +121,34 @@ module Nylas
|
|
|
112
121
|
Threads.new(self)
|
|
113
122
|
end
|
|
114
123
|
|
|
124
|
+
# The policy resources for your Nylas application.
|
|
125
|
+
#
|
|
126
|
+
# @return [Nylas::Policies] Policy resources for your Nylas application.
|
|
127
|
+
def policies
|
|
128
|
+
Policies.new(self)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The rule resources for your Nylas application.
|
|
132
|
+
#
|
|
133
|
+
# @return [Nylas::Rules] Rule resources for your Nylas application.
|
|
134
|
+
def rules
|
|
135
|
+
Rules.new(self)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The workspace resources for your Nylas application.
|
|
139
|
+
#
|
|
140
|
+
# @return [Nylas::Workspaces] Workspace resources for your Nylas application.
|
|
141
|
+
def workspaces
|
|
142
|
+
Workspaces.new(self)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The domain resources for your Nylas application.
|
|
146
|
+
#
|
|
147
|
+
# @return [Nylas::Domains] Domain resources for your Nylas application.
|
|
148
|
+
def domains
|
|
149
|
+
Domains.new(self)
|
|
150
|
+
end
|
|
151
|
+
|
|
115
152
|
# The webhook resources for your Nylas application.
|
|
116
153
|
#
|
|
117
154
|
# @return [Nylas::Webhooks] Webhook resources for your Nylas application.
|
|
@@ -124,5 +161,11 @@ module Nylas
|
|
|
124
161
|
def scheduler
|
|
125
162
|
Scheduler.new(self)
|
|
126
163
|
end
|
|
164
|
+
|
|
165
|
+
# The Notetaker resources for your Nylas application.
|
|
166
|
+
# @return [Nylas::Notetakers] Notetaker resources for your Nylas application.
|
|
167
|
+
def notetakers
|
|
168
|
+
Notetakers.new(self)
|
|
169
|
+
end
|
|
127
170
|
end
|
|
128
171
|
end
|
data/lib/nylas/errors.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Nylas
|
|
|
14
14
|
|
|
15
15
|
# Error class representing a failed response from the Nylas API.
|
|
16
16
|
class NylasApiError < AbstractNylasApiError
|
|
17
|
-
attr_accessor :type, :request_id, :provider_error, :status_code
|
|
17
|
+
attr_accessor :type, :request_id, :provider_error, :status_code, :headers
|
|
18
18
|
|
|
19
19
|
# Initializes an error and assigns the given attributes to it.
|
|
20
20
|
#
|
|
@@ -23,24 +23,26 @@ module Nylas
|
|
|
23
23
|
# @param status_code [Integer] Error status code.
|
|
24
24
|
# @param provider_error [Hash, nil] The error from the provider.
|
|
25
25
|
# @param request_id [Hash, nil] The ID of the request.
|
|
26
|
-
def initialize(type, message, status_code, provider_error = nil, request_id = nil)
|
|
26
|
+
def initialize(type, message, status_code, provider_error = nil, request_id = nil, headers = nil)
|
|
27
27
|
super(message)
|
|
28
28
|
self.type = type
|
|
29
29
|
self.status_code = status_code
|
|
30
30
|
self.provider_error = provider_error
|
|
31
31
|
self.request_id = request_id
|
|
32
|
+
self.headers = headers
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# Parses the error response.
|
|
35
36
|
#
|
|
36
37
|
# @param response [Hash] Response from the Nylas API.
|
|
37
38
|
# @param status_code [Integer] Error status code.
|
|
38
|
-
def self.parse_error_response(response, status_code)
|
|
39
|
+
def self.parse_error_response(response, status_code, headers = nil)
|
|
39
40
|
new(
|
|
40
41
|
response["type"],
|
|
41
42
|
response["message"],
|
|
42
43
|
status_code,
|
|
43
|
-
response["provider_error"]
|
|
44
|
+
response["provider_error"],
|
|
45
|
+
headers
|
|
44
46
|
)
|
|
45
47
|
end
|
|
46
48
|
end
|
|
@@ -15,22 +15,25 @@ module Nylas
|
|
|
15
15
|
#
|
|
16
16
|
# @param path [String] Destination path for the call.
|
|
17
17
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
18
|
-
# @
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
19
|
+
# @return [Array([Hash, Array], String, Hash)] Nylas data object, API Request ID, and response headers.
|
|
20
|
+
def get(path:, query_params: {}, headers: {})
|
|
21
|
+
response = get_raw(path: path, query_params: query_params, headers: headers)
|
|
21
22
|
|
|
22
|
-
[response[:data], response[:request_id]]
|
|
23
|
+
[response[:data], response[:request_id], response[:headers]]
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Performs a GET call to the Nylas API for a list response.
|
|
26
27
|
#
|
|
27
28
|
# @param path [String] Destination path for the call.
|
|
28
29
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
29
|
-
# @
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
31
|
+
# @return [Array<Array<Hash>, String, String, Hash>]
|
|
32
|
+
# Nylas data array, API Request ID, next cursor, and response headers.response headers.
|
|
33
|
+
def get_list(path:, query_params: {}, headers: {})
|
|
34
|
+
response = get_raw(path: path, query_params: query_params, headers: headers)
|
|
32
35
|
|
|
33
|
-
[response[:data], response[:request_id], response[:next_cursor]]
|
|
36
|
+
[response[:data], response[:request_id], response[:next_cursor], response[:headers]]
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
private
|
|
@@ -39,16 +42,20 @@ module Nylas
|
|
|
39
42
|
#
|
|
40
43
|
# @param path [String] Destination path for the call.
|
|
41
44
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
45
|
+
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
42
46
|
# @return [Hash] The JSON response from the Nylas API.
|
|
43
|
-
def get_raw(path:, query_params: {})
|
|
44
|
-
|
|
47
|
+
def get_raw(path:, query_params: {}, headers: {})
|
|
48
|
+
request = {
|
|
45
49
|
method: :get,
|
|
46
50
|
path: path,
|
|
47
51
|
query: query_params,
|
|
48
52
|
payload: nil,
|
|
49
53
|
api_key: api_key,
|
|
50
54
|
timeout: timeout
|
|
51
|
-
|
|
55
|
+
}
|
|
56
|
+
request[:headers] = headers unless headers.empty?
|
|
57
|
+
|
|
58
|
+
execute(**request)
|
|
52
59
|
end
|
|
53
60
|
end
|
|
54
61
|
|
|
@@ -62,20 +69,23 @@ module Nylas
|
|
|
62
69
|
# @param path [String] Destination path for the call.
|
|
63
70
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
64
71
|
# @param request_body [Hash, nil] Request body to pass to the call.
|
|
72
|
+
# Defaults to {} when nil to ensure Content-Type: application/json is sent.
|
|
65
73
|
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
66
|
-
# @return Nylas data object
|
|
67
|
-
def post(path:, query_params: {}, request_body: nil, headers: {})
|
|
68
|
-
|
|
74
|
+
# @return [Array(Hash, String, Hash)] Nylas data object, API Request ID, and response headers.
|
|
75
|
+
def post(path:, query_params: {}, request_body: nil, headers: {}, serialized_json_body: nil)
|
|
76
|
+
request = {
|
|
69
77
|
method: :post,
|
|
70
78
|
path: path,
|
|
71
79
|
query: query_params,
|
|
72
|
-
payload: request_body,
|
|
80
|
+
payload: request_body || {},
|
|
73
81
|
headers: headers,
|
|
74
82
|
api_key: api_key,
|
|
75
83
|
timeout: timeout
|
|
76
|
-
|
|
84
|
+
}
|
|
85
|
+
request[:serialized_json_body] = serialized_json_body unless serialized_json_body.nil?
|
|
86
|
+
response = execute(**request)
|
|
77
87
|
|
|
78
|
-
[response[:data], response[:request_id]]
|
|
88
|
+
[response[:data], response[:request_id], response[:headers]]
|
|
79
89
|
end
|
|
80
90
|
end
|
|
81
91
|
|
|
@@ -89,18 +99,21 @@ module Nylas
|
|
|
89
99
|
# @param path [String] Destination path for the call.
|
|
90
100
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
91
101
|
# @param request_body [Hash, nil] Request body to pass to the call.
|
|
102
|
+
# Defaults to {} when nil to ensure Content-Type: application/json is sent.
|
|
92
103
|
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
93
104
|
# @return Nylas data object and API Request ID.
|
|
94
|
-
def put(path:, query_params: {}, request_body: nil, headers: {})
|
|
95
|
-
|
|
105
|
+
def put(path:, query_params: {}, request_body: nil, headers: {}, serialized_json_body: nil)
|
|
106
|
+
request = {
|
|
96
107
|
method: :put,
|
|
97
108
|
path: path,
|
|
98
109
|
query: query_params,
|
|
99
|
-
payload: request_body,
|
|
110
|
+
payload: request_body || {},
|
|
100
111
|
headers: headers,
|
|
101
112
|
api_key: api_key,
|
|
102
113
|
timeout: timeout
|
|
103
|
-
|
|
114
|
+
}
|
|
115
|
+
request[:serialized_json_body] = serialized_json_body unless serialized_json_body.nil?
|
|
116
|
+
response = execute(**request)
|
|
104
117
|
|
|
105
118
|
[response[:data], response[:request_id]]
|
|
106
119
|
end
|
|
@@ -116,18 +129,21 @@ module Nylas
|
|
|
116
129
|
# @param path [String] Destination path for the call.
|
|
117
130
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
118
131
|
# @param request_body [Hash, nil] Request body to pass to the call.
|
|
132
|
+
# Defaults to {} when nil to ensure Content-Type: application/json is sent.
|
|
119
133
|
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
120
134
|
# @return Nylas data object and API Request ID.
|
|
121
|
-
def patch(path:, query_params: {}, request_body: nil, headers: {})
|
|
122
|
-
|
|
135
|
+
def patch(path:, query_params: {}, request_body: nil, headers: {}, serialized_json_body: nil)
|
|
136
|
+
request = {
|
|
123
137
|
method: :patch,
|
|
124
138
|
path: path,
|
|
125
139
|
query: query_params,
|
|
126
|
-
payload: request_body,
|
|
140
|
+
payload: request_body || {},
|
|
127
141
|
headers: headers,
|
|
128
142
|
api_key: api_key,
|
|
129
143
|
timeout: timeout
|
|
130
|
-
|
|
144
|
+
}
|
|
145
|
+
request[:serialized_json_body] = serialized_json_body unless serialized_json_body.nil?
|
|
146
|
+
response = execute(**request)
|
|
131
147
|
|
|
132
148
|
[response[:data], response[:request_id]]
|
|
133
149
|
end
|
|
@@ -142,15 +158,17 @@ module Nylas
|
|
|
142
158
|
#
|
|
143
159
|
# @param path [String] Destination path for the call.
|
|
144
160
|
# @param query_params [Hash, {}] Query params to pass to the call.
|
|
161
|
+
# @param request_body [Hash, nil] Optional request body (e.g. cancellation_reason for bookings).
|
|
162
|
+
# Defaults to {} to ensure Content-Type: application/json is sent.
|
|
145
163
|
# @param headers [Hash, {}] Additional HTTP headers to include in the payload.
|
|
146
164
|
# @return Nylas data object and API Request ID.
|
|
147
|
-
def delete(path:, query_params: {}, headers: {})
|
|
165
|
+
def delete(path:, query_params: {}, request_body: nil, headers: {})
|
|
148
166
|
response = execute(
|
|
149
167
|
method: :delete,
|
|
150
168
|
path: path,
|
|
151
169
|
query: query_params,
|
|
152
170
|
headers: headers,
|
|
153
|
-
payload:
|
|
171
|
+
payload: request_body || {},
|
|
154
172
|
api_key: api_key,
|
|
155
173
|
timeout: timeout
|
|
156
174
|
)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "httparty"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "net/http/post/multipart"
|
|
4
6
|
|
|
5
7
|
require_relative "../errors"
|
|
6
8
|
require_relative "../version"
|
|
@@ -28,21 +30,28 @@ module Nylas
|
|
|
28
30
|
# @param query [Hash, {}] Hash of names and values to include in the query section of the URI
|
|
29
31
|
# fragment.
|
|
30
32
|
# @param payload [Hash, nil] Body to send with the request.
|
|
33
|
+
# @param serialized_json_body [String, nil] Pre-serialized JSON body to send as-is.
|
|
31
34
|
# @param api_key [Hash, nil] API key to send with the request.
|
|
32
35
|
# @return [Object] Parsed JSON response from the API.
|
|
33
|
-
def execute(method:, path:, timeout:, headers: {}, query: {}, payload: nil, api_key: nil
|
|
36
|
+
def execute(method:, path:, timeout:, headers: {}, query: {}, payload: nil, api_key: nil,
|
|
37
|
+
serialized_json_body: nil)
|
|
34
38
|
request = build_request(method: method, path: path, headers: headers,
|
|
35
|
-
query: query, payload: payload, api_key: api_key, timeout: timeout
|
|
39
|
+
query: query, payload: payload, api_key: api_key, timeout: timeout,
|
|
40
|
+
serialized_json_body: serialized_json_body)
|
|
36
41
|
begin
|
|
37
|
-
|
|
42
|
+
httparty_execute(**request) do |response, _request, result|
|
|
38
43
|
content_type = nil
|
|
39
|
-
if response.headers && response.headers[
|
|
40
|
-
content_type = response.headers[
|
|
44
|
+
if response.headers && response.headers["content-type"]
|
|
45
|
+
content_type = response.headers["content-type"].downcase
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
parse_json_evaluate_error(result.code.to_i, response, path, content_type
|
|
48
|
+
parsed_response = parse_json_evaluate_error(result.code.to_i, response.body, path, content_type,
|
|
49
|
+
response.headers)
|
|
50
|
+
# Include headers in the response
|
|
51
|
+
parsed_response[:headers] = response.headers unless parsed_response.nil?
|
|
52
|
+
parsed_response
|
|
44
53
|
end
|
|
45
|
-
rescue
|
|
54
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
46
55
|
raise Nylas::NylasSdkTimeoutError.new(request[:path], timeout)
|
|
47
56
|
end
|
|
48
57
|
end
|
|
@@ -85,20 +94,32 @@ module Nylas
|
|
|
85
94
|
# @param query [Hash, {}] Hash of names and values to include in the query section of the URI
|
|
86
95
|
# fragment.
|
|
87
96
|
# @param payload [Hash, nil] Body to send with the request.
|
|
97
|
+
# @param serialized_json_body [String, nil] Pre-serialized JSON body to send as-is.
|
|
88
98
|
# @param timeout [Integer, nil] Timeout value to send with the request.
|
|
89
99
|
# @param api_key [Hash, nil] API key to send with the request.
|
|
90
100
|
# @return [Object] The request information after processing. This includes an updated payload
|
|
91
101
|
# and headers.
|
|
92
102
|
def build_request(
|
|
93
|
-
method:, path: nil, headers: {}, query: {}, payload: nil, timeout: nil, api_key: nil
|
|
103
|
+
method:, path: nil, headers: {}, query: {}, payload: nil, timeout: nil, api_key: nil,
|
|
104
|
+
serialized_json_body: nil
|
|
94
105
|
)
|
|
95
106
|
url = build_url(path, query)
|
|
96
107
|
resulting_headers = default_headers.merge(headers).merge(auth_header(api_key))
|
|
97
|
-
|
|
108
|
+
|
|
109
|
+
# Check for multipart flag using both string and symbol keys for backwards compatibility
|
|
110
|
+
is_multipart = !payload.nil? && (payload["multipart"] || payload[:multipart])
|
|
111
|
+
|
|
112
|
+
if !serialized_json_body.nil?
|
|
113
|
+
payload = serialized_json_body
|
|
114
|
+
resulting_headers["Content-type"] = "application/json"
|
|
115
|
+
elsif !payload.nil? && !is_multipart
|
|
116
|
+
normalize_json_encodings!(payload)
|
|
98
117
|
payload = payload&.to_json
|
|
99
118
|
resulting_headers["Content-type"] = "application/json"
|
|
100
|
-
elsif
|
|
119
|
+
elsif is_multipart
|
|
120
|
+
# Remove multipart flag from both possible key types
|
|
101
121
|
payload.delete("multipart")
|
|
122
|
+
payload.delete(:multipart)
|
|
102
123
|
end
|
|
103
124
|
|
|
104
125
|
{ method: method, url: url, payload: payload, headers: resulting_headers, timeout: timeout }
|
|
@@ -116,23 +137,237 @@ module Nylas
|
|
|
116
137
|
def parse_response(response)
|
|
117
138
|
return response if response.is_a?(Enumerable)
|
|
118
139
|
|
|
119
|
-
Yajl::Parser.new(symbolize_names: true).parse(response)
|
|
140
|
+
Yajl::Parser.new(symbolize_names: true).parse(response) || raise(Nylas::JsonParseError)
|
|
120
141
|
rescue Yajl::ParseError
|
|
121
142
|
raise Nylas::JsonParseError
|
|
122
143
|
end
|
|
123
144
|
|
|
124
145
|
private
|
|
125
146
|
|
|
126
|
-
# Sends a request to the Nylas REST API.
|
|
147
|
+
# Sends a request to the Nylas REST API using HTTParty or Net::HTTP for multipart.
|
|
148
|
+
# Multipart requests use Net::HTTP::Post::Multipart (multipart-post gem) because
|
|
149
|
+
# HTTParty's multipart handling produces malformed requests that the Nylas API rejects.
|
|
127
150
|
#
|
|
128
151
|
# @param method [Symbol] HTTP method for the API call. Either :get, :post, :delete, or :patch.
|
|
129
152
|
# @param url [String] URL for the API call.
|
|
130
153
|
# @param headers [Hash] HTTP headers to include in the payload.
|
|
131
154
|
# @param payload [String, Hash] Body to send with the request.
|
|
132
155
|
# @param timeout [Hash] Timeout value to send with the request.
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
def httparty_execute(method:, url:, headers:, payload:, timeout:)
|
|
157
|
+
if method == :post && payload.is_a?(Hash) && file_upload?(payload)
|
|
158
|
+
response = execute_multipart_request(url: url, headers: headers, payload: payload, timeout: timeout)
|
|
159
|
+
else
|
|
160
|
+
options = { headers: headers, timeout: timeout }
|
|
161
|
+
options[:body] = payload if payload
|
|
162
|
+
response = HTTParty.send(method, url, options)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
result = create_response_wrapper(response)
|
|
166
|
+
|
|
167
|
+
if block_given?
|
|
168
|
+
yield response, nil, result
|
|
169
|
+
else
|
|
170
|
+
response
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Executes multipart POST using Net::HTTP::Post::Multipart (fixes issue #538).
|
|
175
|
+
# HTTParty's multipart produces malformed requests; multipart-post/UploadIO works correctly.
|
|
176
|
+
def execute_multipart_request(url:, headers:, payload:, timeout:)
|
|
177
|
+
uri = URI.parse(url)
|
|
178
|
+
params = build_multipart_params(payload)
|
|
179
|
+
|
|
180
|
+
req = Net::HTTP::Post::Multipart.new(uri.path, params)
|
|
181
|
+
headers.each { |key, value| req[key] = value }
|
|
182
|
+
|
|
183
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
184
|
+
http.use_ssl = (uri.scheme == "https")
|
|
185
|
+
http.read_timeout = timeout
|
|
186
|
+
http.open_timeout = timeout
|
|
187
|
+
|
|
188
|
+
response = http.request(req)
|
|
189
|
+
|
|
190
|
+
create_httparty_like_response(response)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Build params hash for Net::HTTP::Post::Multipart with UploadIO for file fields.
|
|
194
|
+
def build_multipart_params(payload)
|
|
195
|
+
params = {}
|
|
196
|
+
payload.each do |key, value|
|
|
197
|
+
params[key] = if key.is_a?(String) && key != "message" && file_like_value?(value)
|
|
198
|
+
value_to_upload_io(value)
|
|
199
|
+
else
|
|
200
|
+
value.to_s
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
params
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def file_like_value?(value)
|
|
207
|
+
return true if value.respond_to?(:read) && (value.is_a?(File) ? !value.closed? : true)
|
|
208
|
+
if value.is_a?(String) && (value.respond_to?(:original_filename) || value.respond_to?(:content_type))
|
|
209
|
+
return true
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Convert File, String, or StringIO to UploadIO for multipart-post.
|
|
216
|
+
def value_to_upload_io(value)
|
|
217
|
+
content_type = value.respond_to?(:content_type) ? value.content_type : "application/octet-stream"
|
|
218
|
+
filename = value.respond_to?(:original_filename) ? value.original_filename : "file.bin"
|
|
219
|
+
|
|
220
|
+
io = if value.respond_to?(:read) && value.respond_to?(:rewind)
|
|
221
|
+
value.rewind if value.respond_to?(:rewind)
|
|
222
|
+
value
|
|
223
|
+
else
|
|
224
|
+
require "stringio"
|
|
225
|
+
content = value.to_s
|
|
226
|
+
content = content.dup.force_encoding(Encoding::ASCII_8BIT) if content.is_a?(String)
|
|
227
|
+
StringIO.new(content)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
UploadIO.new(io, content_type, filename)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Create response object compatible with HTTParty::Response interface.
|
|
234
|
+
def create_httparty_like_response(net_http_response)
|
|
235
|
+
headers = net_http_response.to_hash
|
|
236
|
+
headers = headers.transform_values { |v| v.is_a?(Array) && v.one? ? v.first : v }
|
|
237
|
+
|
|
238
|
+
OpenStruct.new(
|
|
239
|
+
body: net_http_response.body,
|
|
240
|
+
code: net_http_response.code.to_i,
|
|
241
|
+
headers: headers
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Create a response wrapper that mimics RestClient::Response.code behavior
|
|
246
|
+
def create_response_wrapper(response)
|
|
247
|
+
OpenStruct.new(code: response.code)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Check if payload contains file uploads
|
|
251
|
+
def file_upload?(payload)
|
|
252
|
+
return false unless payload.is_a?(Hash)
|
|
253
|
+
|
|
254
|
+
# Check for traditional file uploads (File objects or objects that respond to :read)
|
|
255
|
+
has_file_objects = payload.values.any? do |value|
|
|
256
|
+
value.respond_to?(:read) || (value.is_a?(File) && !value.closed?)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
return true if has_file_objects
|
|
260
|
+
|
|
261
|
+
# Check if payload was prepared by FileUtils.build_form_request for multipart uploads
|
|
262
|
+
# This handles binary content attachments that are strings with added singleton methods
|
|
263
|
+
has_message_field = payload.key?("message") && payload["message"].is_a?(String)
|
|
264
|
+
|
|
265
|
+
# Check for attachment fields - these can have custom content_id values (not just "file{N}")
|
|
266
|
+
# FileUtils.build_form_request creates entries with string values that have singleton methods
|
|
267
|
+
# like original_filename and content_type defined on them
|
|
268
|
+
has_attachment_fields = payload.any? do |key, value|
|
|
269
|
+
next false unless key.is_a?(String) && key != "message"
|
|
270
|
+
|
|
271
|
+
# Check if the value is a string with attachment-like singleton methods
|
|
272
|
+
# (original_filename or content_type), which indicates it's a file content
|
|
273
|
+
value.is_a?(String) && (value.respond_to?(:original_filename) || value.respond_to?(:content_type))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# If we have both a "message" field and attachment fields with file metadata,
|
|
277
|
+
# this indicates the payload was prepared by FileUtils.build_form_request
|
|
278
|
+
has_message_field && has_attachment_fields
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Prepare multipart payload for HTTParty compatibility
|
|
282
|
+
# HTTParty requires all multipart fields to have compatible encodings
|
|
283
|
+
def prepare_multipart_payload(payload)
|
|
284
|
+
require "stringio"
|
|
285
|
+
|
|
286
|
+
modified_payload = payload.dup
|
|
287
|
+
|
|
288
|
+
# First, normalize all string encodings to prevent HTTParty encoding conflicts
|
|
289
|
+
normalize_multipart_encodings!(modified_payload)
|
|
290
|
+
|
|
291
|
+
# Handle binary content attachments (file0, file1, etc.) by converting them to enhanced StringIO
|
|
292
|
+
# HTTParty expects file uploads to be objects with full file-like interface
|
|
293
|
+
modified_payload.each do |key, value|
|
|
294
|
+
next unless key.is_a?(String) && key.match?(/^file\d+$/) && value.is_a?(String)
|
|
295
|
+
|
|
296
|
+
# Get the original value to check for singleton methods
|
|
297
|
+
original_value = payload[key]
|
|
298
|
+
|
|
299
|
+
# Create an enhanced StringIO object for HTTParty compatibility
|
|
300
|
+
string_io = create_file_like_stringio(value)
|
|
301
|
+
|
|
302
|
+
# Preserve filename and content_type if they exist as singleton methods
|
|
303
|
+
if original_value.respond_to?(:original_filename)
|
|
304
|
+
string_io.define_singleton_method(:original_filename) { original_value.original_filename }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if original_value.respond_to?(:content_type)
|
|
308
|
+
string_io.define_singleton_method(:content_type) { original_value.content_type }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
modified_payload[key] = string_io
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
modified_payload
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Normalize string encodings in multipart payload to prevent HTTParty encoding conflicts
|
|
318
|
+
# This ensures all string fields use consistent ASCII-8BIT encoding for multipart compatibility
|
|
319
|
+
def normalize_multipart_encodings!(payload)
|
|
320
|
+
payload.each do |key, value|
|
|
321
|
+
next unless value.is_a?(String)
|
|
322
|
+
|
|
323
|
+
# Force all string values to ASCII-8BIT encoding for multipart compatibility
|
|
324
|
+
# HTTParty/multipart-post expects binary encoding for consistent concatenation
|
|
325
|
+
payload[key] = value.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Normalize JSON encodings for attachment content to ensure binary data is base64 encoded.
|
|
330
|
+
# This handles cases where users pass raw binary content directly instead of file objects.
|
|
331
|
+
def normalize_json_encodings!(payload)
|
|
332
|
+
return unless payload.is_a?(Hash)
|
|
333
|
+
|
|
334
|
+
# Handle attachment content encoding for JSON serialization
|
|
335
|
+
attachments = payload[:attachments] || payload["attachments"]
|
|
336
|
+
return unless attachments
|
|
337
|
+
|
|
338
|
+
attachments.each do |attachment|
|
|
339
|
+
content = attachment[:content] || attachment["content"]
|
|
340
|
+
next unless content.is_a?(String)
|
|
341
|
+
|
|
342
|
+
# If content appears to be binary (non-UTF-8), base64 encode it
|
|
343
|
+
next unless content.encoding == Encoding::ASCII_8BIT || !content.valid_encoding?
|
|
344
|
+
|
|
345
|
+
encoded_content = Base64.strict_encode64(content)
|
|
346
|
+
if attachment.key?(:content)
|
|
347
|
+
attachment[:content] = encoded_content
|
|
348
|
+
else
|
|
349
|
+
attachment["content"] = encoded_content
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Create a StringIO object that behaves more like a File for HTTParty compatibility
|
|
355
|
+
def create_file_like_stringio(content)
|
|
356
|
+
# Content is already normalized to ASCII-8BIT by normalize_multipart_encodings!
|
|
357
|
+
# Create StringIO with the normalized binary content
|
|
358
|
+
string_io = StringIO.new(content)
|
|
359
|
+
|
|
360
|
+
# Add methods that HTTParty/multipart-post might expect
|
|
361
|
+
string_io.define_singleton_method(:path) { nil }
|
|
362
|
+
string_io.define_singleton_method(:local_path) { nil }
|
|
363
|
+
string_io.define_singleton_method(:respond_to_missing?) do |method_name, include_private = false|
|
|
364
|
+
File.instance_methods.include?(method_name) || super(method_name, include_private)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Set binary mode for file-like behavior
|
|
368
|
+
string_io.binmode if string_io.respond_to?(:binmode)
|
|
369
|
+
|
|
370
|
+
string_io
|
|
136
371
|
end
|
|
137
372
|
|
|
138
373
|
def setup_http(path, timeout, headers, query, api_key)
|
|
@@ -160,50 +395,51 @@ module Nylas
|
|
|
160
395
|
end
|
|
161
396
|
|
|
162
397
|
# Parses the response from the Nylas API and evaluates for errors.
|
|
163
|
-
def parse_json_evaluate_error(http_code, response, path, content_type = nil)
|
|
398
|
+
def parse_json_evaluate_error(http_code, response, path, content_type = nil, headers = nil)
|
|
164
399
|
begin
|
|
165
400
|
response = parse_response(response) if content_type == "application/json"
|
|
166
401
|
rescue Nylas::JsonParseError
|
|
167
|
-
handle_failed_response(http_code, response, path)
|
|
402
|
+
handle_failed_response(http_code, response, path, headers)
|
|
168
403
|
raise
|
|
169
404
|
end
|
|
170
405
|
|
|
171
|
-
handle_failed_response(http_code, response, path)
|
|
406
|
+
handle_failed_response(http_code, response, path, headers)
|
|
172
407
|
response
|
|
173
408
|
end
|
|
174
409
|
|
|
175
410
|
# Handles failed responses from the Nylas API.
|
|
176
|
-
def handle_failed_response(http_code, response, path)
|
|
411
|
+
def handle_failed_response(http_code, response, path, headers = nil)
|
|
177
412
|
return if HTTP_SUCCESS_CODES.include?(http_code)
|
|
178
413
|
|
|
179
414
|
case response
|
|
180
415
|
when Hash
|
|
181
|
-
raise error_hash_to_exception(response, http_code, path)
|
|
416
|
+
raise error_hash_to_exception(response, http_code, path, headers)
|
|
182
417
|
else
|
|
183
418
|
raise NylasApiError.parse_error_response(response, http_code)
|
|
184
419
|
end
|
|
185
420
|
end
|
|
186
421
|
|
|
187
422
|
# Converts error hashes to exceptions.
|
|
188
|
-
def error_hash_to_exception(response, status_code, path)
|
|
423
|
+
def error_hash_to_exception(response, status_code, path, headers = nil)
|
|
189
424
|
return if !response || !response.key?(:error)
|
|
190
425
|
|
|
191
426
|
if %W[#{api_uri}/v3/connect/token #{api_uri}/v3/connect/revoke].include?(path)
|
|
192
427
|
NylasOAuthError.new(response[:error], response[:error_description], response[:error_uri],
|
|
193
428
|
response[:error_code], status_code)
|
|
194
429
|
else
|
|
195
|
-
throw_error(response, status_code)
|
|
430
|
+
throw_error(response, status_code, headers)
|
|
196
431
|
end
|
|
197
432
|
end
|
|
198
433
|
|
|
199
|
-
def throw_error(response, status_code)
|
|
434
|
+
def throw_error(response, status_code, headers = nil)
|
|
200
435
|
error_obj = response[:error]
|
|
201
436
|
|
|
202
437
|
# If `error_obj` is just a string, turn it into a hash with default keys.
|
|
203
438
|
if error_obj.is_a?(String)
|
|
204
439
|
error_obj = {
|
|
205
440
|
type: "NylasApiError",
|
|
206
|
-
message: error_obj
|
|
441
|
+
message: error_obj,
|
|
442
|
+
headers: headers
|
|
207
443
|
}
|
|
208
444
|
end
|
|
209
445
|
|
|
@@ -214,7 +450,8 @@ module Nylas
|
|
|
214
450
|
error_obj[:message],
|
|
215
451
|
status_code,
|
|
216
452
|
provider_error,
|
|
217
|
-
response[:request_id]
|
|
453
|
+
response[:request_id],
|
|
454
|
+
headers
|
|
218
455
|
)
|
|
219
456
|
end
|
|
220
457
|
|
|
@@ -260,6 +497,8 @@ module Nylas
|
|
|
260
497
|
|
|
261
498
|
# Set the authorization header for an API query.
|
|
262
499
|
def auth_header(api_key)
|
|
500
|
+
return {} if api_key.nil?
|
|
501
|
+
|
|
263
502
|
{ "Authorization" => "Bearer #{api_key}" }
|
|
264
503
|
end
|
|
265
504
|
end
|