nextmail 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.
- checksums.yaml +7 -0
- data/README.md +183 -0
- data/lib/nextmail/api_paths.rb +447 -0
- data/lib/nextmail/client.rb +81 -0
- data/lib/nextmail/constants.rb +1621 -0
- data/lib/nextmail/credentials.rb +53 -0
- data/lib/nextmail/decode.rb +34 -0
- data/lib/nextmail/defaults.rb +114 -0
- data/lib/nextmail/endpoint.rb +144 -0
- data/lib/nextmail/errors.rb +121 -0
- data/lib/nextmail/http.rb +172 -0
- data/lib/nextmail/image_crop.rb +38 -0
- data/lib/nextmail/languages.rb +42 -0
- data/lib/nextmail/pagination.rb +63 -0
- data/lib/nextmail/protocol.rb +221 -0
- data/lib/nextmail/raw_body.rb +83 -0
- data/lib/nextmail/request_path.rb +50 -0
- data/lib/nextmail/resource.rb +109 -0
- data/lib/nextmail/resources/account.rb +155 -0
- data/lib/nextmail/resources/addresses.rb +38 -0
- data/lib/nextmail/resources/analytics.rb +19 -0
- data/lib/nextmail/resources/app_host.rb +23 -0
- data/lib/nextmail/resources/audiences.rb +95 -0
- data/lib/nextmail/resources/automations.rb +125 -0
- data/lib/nextmail/resources/billing.rb +76 -0
- data/lib/nextmail/resources/bookings.rb +100 -0
- data/lib/nextmail/resources/branding.rb +24 -0
- data/lib/nextmail/resources/broadcasts.rb +102 -0
- data/lib/nextmail/resources/calendar.rb +153 -0
- data/lib/nextmail/resources/chats.rb +62 -0
- data/lib/nextmail/resources/connected_mailboxes.rb +35 -0
- data/lib/nextmail/resources/connectors.rb +75 -0
- data/lib/nextmail/resources/contacts.rb +197 -0
- data/lib/nextmail/resources/dns_connections.rb +23 -0
- data/lib/nextmail/resources/domains.rb +264 -0
- data/lib/nextmail/resources/drafts.rb +36 -0
- data/lib/nextmail/resources/emails.rb +120 -0
- data/lib/nextmail/resources/encryption.rb +19 -0
- data/lib/nextmail/resources/events.rb +22 -0
- data/lib/nextmail/resources/exports.rb +27 -0
- data/lib/nextmail/resources/files.rb +82 -0
- data/lib/nextmail/resources/follow_ups.rb +21 -0
- data/lib/nextmail/resources/forms.rb +112 -0
- data/lib/nextmail/resources/imports.rb +106 -0
- data/lib/nextmail/resources/keys.rb +126 -0
- data/lib/nextmail/resources/knowledge.rb +137 -0
- data/lib/nextmail/resources/labels.rb +43 -0
- data/lib/nextmail/resources/languages.rb +11 -0
- data/lib/nextmail/resources/me.rb +57 -0
- data/lib/nextmail/resources/members.rb +100 -0
- data/lib/nextmail/resources/out_of_office.rb +28 -0
- data/lib/nextmail/resources/provider_imports.rb +48 -0
- data/lib/nextmail/resources/roles.rb +39 -0
- data/lib/nextmail/resources/rules.rb +73 -0
- data/lib/nextmail/resources/security.rb +39 -0
- data/lib/nextmail/resources/segments.rb +31 -0
- data/lib/nextmail/resources/senders.rb +19 -0
- data/lib/nextmail/resources/sending.rb +23 -0
- data/lib/nextmail/resources/settings.rb +16 -0
- data/lib/nextmail/resources/signatures.rb +41 -0
- data/lib/nextmail/resources/subscriptions.rb +55 -0
- data/lib/nextmail/resources/support.rb +11 -0
- data/lib/nextmail/resources/suppressions.rb +43 -0
- data/lib/nextmail/resources/tasks.rb +95 -0
- data/lib/nextmail/resources/temp_mail.rb +67 -0
- data/lib/nextmail/resources/templates.rb +153 -0
- data/lib/nextmail/resources/threads.rb +218 -0
- data/lib/nextmail/resources/tools.rb +35 -0
- data/lib/nextmail/resources/tracking.rb +72 -0
- data/lib/nextmail/resources/webhooks.rb +170 -0
- data/lib/nextmail/resources/workspaces.rb +40 -0
- data/lib/nextmail/results.rb +33 -0
- data/lib/nextmail/transport.rb +272 -0
- data/lib/nextmail/update_notice.rb +69 -0
- data/lib/nextmail/version.rb +5 -0
- data/lib/nextmail/webhook.rb +82 -0
- data/lib/nextmail/wire.rb +102 -0
- data/lib/nextmail.rb +126 -0
- metadata +122 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nextmail
|
|
4
|
+
module Languages
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def key(value)
|
|
8
|
+
value.to_s.strip.downcase
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
BY_CODE = LANGUAGES.to_h { |language| [key(language[:code]), language] }.freeze
|
|
12
|
+
BY_LABEL = LANGUAGES.to_h { |language| [key(language[:label]), language] }.freeze
|
|
13
|
+
BY_NATIVE = LANGUAGES.to_h { |language| [key(language[:native]), language] }.freeze
|
|
14
|
+
ALIASES = Protocol::LANGUAGE_ALIASES.to_h { |(name, code)| [key(name), key(code)] }.freeze
|
|
15
|
+
|
|
16
|
+
def by_code(code)
|
|
17
|
+
return nil if code.nil? || code == ""
|
|
18
|
+
|
|
19
|
+
BY_CODE[key(code)]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolve(input)
|
|
23
|
+
return nil if input.nil? || input == ""
|
|
24
|
+
|
|
25
|
+
wanted = key(input)
|
|
26
|
+
return nil if wanted.empty?
|
|
27
|
+
return BY_CODE[ALIASES[wanted]] if ALIASES.key?(wanted)
|
|
28
|
+
|
|
29
|
+
direct = BY_CODE[wanted] || BY_LABEL[wanted] || BY_NATIVE[wanted]
|
|
30
|
+
return direct if direct
|
|
31
|
+
|
|
32
|
+
base = wanted.split(Defaults::LANGUAGE_SUBTAG_PATTERN).first
|
|
33
|
+
return nil if base.nil? || base.empty? || base == wanted
|
|
34
|
+
|
|
35
|
+
BY_CODE[ALIASES.fetch(base, base)]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rtl?(code)
|
|
39
|
+
resolve(code)&.fetch(:rtl, false) == true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nextmail
|
|
4
|
+
module Pagination
|
|
5
|
+
CURSOR = Protocol::CURSOR_STYLES[:CURSOR]
|
|
6
|
+
PAGE_TOKEN = Protocol::CURSOR_STYLES[:PAGE_TOKEN]
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def fetch_page(transport, path, query: nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
|
|
11
|
+
payload = transport.request(path, query: page_query(query, style, limit, cursor), api_key:)
|
|
12
|
+
envelope = payload.is_a?(Hash) ? payload : {}
|
|
13
|
+
next_cursor = cursor_of(envelope[style[:FIELD].to_sym])
|
|
14
|
+
|
|
15
|
+
Page.new(items: items_of(envelope), has_more: more?(envelope, next_cursor), next_cursor:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def iterate(transport, path, query: nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
|
|
19
|
+
walk(cursor) { |current| fetch_page(transport, path, query:, style:, limit:, cursor: current, api_key:) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def collect_all(transport, path, **options)
|
|
23
|
+
iterate(transport, path, **options).to_a
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch_list(transport, path, query: nil, api_key: nil)
|
|
27
|
+
payload = transport.request(path, query:, api_key:)
|
|
28
|
+
items_of(payload.is_a?(Hash) ? payload : {})
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def walk(cursor, &fetch)
|
|
32
|
+
Enumerator.new do |yielder|
|
|
33
|
+
current = cursor
|
|
34
|
+
|
|
35
|
+
loop do
|
|
36
|
+
page = fetch.call(current)
|
|
37
|
+
page.items.each { |item| yielder << item }
|
|
38
|
+
|
|
39
|
+
break if !page.has_more || page.next_cursor.nil? || page.next_cursor == current
|
|
40
|
+
|
|
41
|
+
current = page.next_cursor
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def page_query(query, style, limit, cursor)
|
|
47
|
+
(query || {}).merge(Protocol::QUERY_KEYS[:LIMIT] => limit, style[:PARAM] => cursor)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def items_of(envelope)
|
|
51
|
+
envelope[:data].is_a?(Array) ? envelope[:data] : []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def more?(envelope, next_cursor)
|
|
55
|
+
flag = envelope[:hasMore]
|
|
56
|
+
(flag == true || flag == false) ? flag : !next_cursor.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cursor_of(value)
|
|
60
|
+
(value.is_a?(String) && !value.empty?) ? value : nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nextmail
|
|
4
|
+
module Protocol
|
|
5
|
+
ACCESS_TOKEN_RULES = {RESERVED_PREFIX: "nm_", MAX_LENGTH: 512}.freeze
|
|
6
|
+
|
|
7
|
+
API_KEY_MODES = {LIVE: "live", TEST: "test"}.freeze
|
|
8
|
+
|
|
9
|
+
API_KEY_PREFIXES = {LIVE: "nm_live_", TEST: "nm_test_"}.freeze
|
|
10
|
+
|
|
11
|
+
CONTENT_TYPES = {
|
|
12
|
+
JSON: "application/json",
|
|
13
|
+
CALENDAR: "text/calendar",
|
|
14
|
+
VCARD: "text/vcard",
|
|
15
|
+
OCTET_STREAM: "application/octet-stream"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
CURSOR_STYLES = {
|
|
19
|
+
CURSOR: {PARAM: "cursor", FIELD: "nextCursor"}.freeze,
|
|
20
|
+
PAGE_TOKEN: {PARAM: "pageToken", FIELD: "nextPageToken"}.freeze
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
ENV_VARS = {
|
|
24
|
+
API_KEY: "NEXTMAIL_API_KEY",
|
|
25
|
+
ACCESS_TOKEN: "NEXTMAIL_ACCESS_TOKEN",
|
|
26
|
+
BASE_URL: "NEXTMAIL_BASE_URL"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
ERROR_CODES = {
|
|
30
|
+
INSUFFICIENT_SCOPE: "insufficient_scope",
|
|
31
|
+
CONSOLE_ONLY: "console_only",
|
|
32
|
+
UNRECOGNISED_RESPONSE: "unrecognised_response"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
ERROR_TYPE_BY_STATUS = {
|
|
36
|
+
401 => "authentication_error",
|
|
37
|
+
403 => "permission_error",
|
|
38
|
+
404 => "not_found_error",
|
|
39
|
+
409 => "conflict_error",
|
|
40
|
+
422 => "validation_error",
|
|
41
|
+
429 => "rate_limit_error"
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
HEADER_KEYS = {
|
|
45
|
+
AUTHORIZATION: "Authorization",
|
|
46
|
+
ACCEPT: "Accept",
|
|
47
|
+
CONTENT_TYPE: "Content-Type",
|
|
48
|
+
USER_AGENT: "User-Agent",
|
|
49
|
+
IDEMPOTENCY_KEY: "Idempotency-Key",
|
|
50
|
+
RETRY_AFTER: "retry-after",
|
|
51
|
+
REQUEST_ID: "x-request-id"
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
HTTP_METHODS = {GET: "GET", POST: "POST", PUT: "PUT", PATCH: "PATCH", DELETE: "DELETE"}.freeze
|
|
55
|
+
|
|
56
|
+
IDEMPOTENCY_KEY_PREFIX = "nm-"
|
|
57
|
+
|
|
58
|
+
IMAGE_CROP_DECIMALS = 6
|
|
59
|
+
|
|
60
|
+
IMAGE_CROP_SEPARATOR = ","
|
|
61
|
+
|
|
62
|
+
LANGUAGE_ALIASES = [
|
|
63
|
+
["zh", "zh-Hans"].freeze,
|
|
64
|
+
["zh-cn", "zh-Hans"].freeze,
|
|
65
|
+
["zh-sg", "zh-Hans"].freeze,
|
|
66
|
+
["cmn", "zh-Hans"].freeze,
|
|
67
|
+
["zh-tw", "zh-Hant"].freeze,
|
|
68
|
+
["zh-mo", "zh-HK"].freeze,
|
|
69
|
+
["iw", "he"].freeze,
|
|
70
|
+
["in", "id"].freeze,
|
|
71
|
+
["ji", "yi"].freeze,
|
|
72
|
+
["mo", "ro"].freeze,
|
|
73
|
+
["sh", "sr-Latn"].freeze,
|
|
74
|
+
["sr-latn-rs", "sr-Latn"].freeze,
|
|
75
|
+
["prs", "fa-AF"].freeze,
|
|
76
|
+
["fa-af", "fa-AF"].freeze,
|
|
77
|
+
["kmr", "ku"].freeze,
|
|
78
|
+
["pt-pt", "pt"].freeze,
|
|
79
|
+
["es-mx", "es-419"].freeze,
|
|
80
|
+
["es-ar", "es-419"].freeze,
|
|
81
|
+
["es-co", "es-419"].freeze,
|
|
82
|
+
["es-cl", "es-419"].freeze
|
|
83
|
+
].freeze
|
|
84
|
+
|
|
85
|
+
LIST_SEPARATOR = ","
|
|
86
|
+
|
|
87
|
+
LOCAL_HOSTS = ["localhost", "::1", "[::1]", "127.0.0.1", "0.0.0.0", "::", "[::]"].freeze
|
|
88
|
+
|
|
89
|
+
LOOPBACK_HOSTNAMES = ["localhost", "::1", "[::1]"].freeze
|
|
90
|
+
|
|
91
|
+
LOOPBACK_IPV4_ADDRESS = "127.0.0.1"
|
|
92
|
+
|
|
93
|
+
LOOPBACK_IPV6_ADDRESS = "[::1]"
|
|
94
|
+
|
|
95
|
+
QUERY_KEYS = {
|
|
96
|
+
LIMIT: "limit",
|
|
97
|
+
CURSOR: "cursor",
|
|
98
|
+
STATUS: "status",
|
|
99
|
+
FROM: "from",
|
|
100
|
+
TO: "to",
|
|
101
|
+
TIMEZONE: "timezone",
|
|
102
|
+
ENABLED: "enabled",
|
|
103
|
+
RULE_ID: "ruleId",
|
|
104
|
+
THREAD_ID: "threadId",
|
|
105
|
+
FOLDER: "folder",
|
|
106
|
+
QUERY: "query",
|
|
107
|
+
LABEL_IDS: "labelIds",
|
|
108
|
+
OPENED: "opened",
|
|
109
|
+
CLICKED: "clicked",
|
|
110
|
+
DAYS: "days",
|
|
111
|
+
MINUTES: "minutes",
|
|
112
|
+
GRAIN: "grain",
|
|
113
|
+
OFFSET_MINUTES: "offsetMinutes",
|
|
114
|
+
INCLUDE_MACHINE: "includeMachine",
|
|
115
|
+
REASSIGN_TO: "reassignTo",
|
|
116
|
+
SOURCE: "source",
|
|
117
|
+
ADDRESS: "address",
|
|
118
|
+
DIRECTION: "direction",
|
|
119
|
+
FILTER: "filter",
|
|
120
|
+
SEARCH: "search",
|
|
121
|
+
SORT: "sort",
|
|
122
|
+
PAGE: "page",
|
|
123
|
+
PAGE_SIZE: "pageSize",
|
|
124
|
+
VERSION: "version",
|
|
125
|
+
TRACKED: "tracked",
|
|
126
|
+
Q: "q",
|
|
127
|
+
AUDIENCE_IDS: "audienceIds",
|
|
128
|
+
AUDIENCE_ID: "audienceId",
|
|
129
|
+
BROADCAST_ID: "broadcastId",
|
|
130
|
+
BROADCAST_IDS: "broadcastIds",
|
|
131
|
+
EMAIL: "email",
|
|
132
|
+
WITHOUT_EMAIL: "withoutEmail",
|
|
133
|
+
BLOCKED: "blocked",
|
|
134
|
+
DATE_FROM: "dateFrom",
|
|
135
|
+
DATE_TO: "dateTo",
|
|
136
|
+
FROM_CONTACTS: "fromContacts",
|
|
137
|
+
SEMANTIC: "semantic",
|
|
138
|
+
REASON: "reason",
|
|
139
|
+
KIND: "kind",
|
|
140
|
+
ENDPOINT_IDS: "endpointIds",
|
|
141
|
+
KEY_IDS: "keyIds",
|
|
142
|
+
RULE_IDS: "ruleIds",
|
|
143
|
+
SINCE: "since",
|
|
144
|
+
UNTIL: "until",
|
|
145
|
+
FAILED_ONLY: "failedOnly",
|
|
146
|
+
FILENAME: "filename",
|
|
147
|
+
DOMAIN: "domain",
|
|
148
|
+
SCHEDULED_FROM: "scheduledFrom",
|
|
149
|
+
SCHEDULED_TO: "scheduledTo",
|
|
150
|
+
ADDRESSES: "addresses",
|
|
151
|
+
OFFSET: "offset",
|
|
152
|
+
REFRESH: "refresh",
|
|
153
|
+
CONFIRM: "confirm",
|
|
154
|
+
USERNAME: "username",
|
|
155
|
+
SCOPE: "scope",
|
|
156
|
+
LEVEL: "level",
|
|
157
|
+
PINNED: "pinned",
|
|
158
|
+
TITLE: "title",
|
|
159
|
+
ITEM_ID: "itemId",
|
|
160
|
+
SENDS: "sends",
|
|
161
|
+
DOMAINS: "domains",
|
|
162
|
+
PEOPLE: "people",
|
|
163
|
+
AI_ACTIONS: "aiActions",
|
|
164
|
+
INTERVAL: "interval",
|
|
165
|
+
CATEGORY: "category",
|
|
166
|
+
DEEP: "deep",
|
|
167
|
+
PATH: "path",
|
|
168
|
+
STEP_KEY: "stepKey",
|
|
169
|
+
NAME: "name",
|
|
170
|
+
CONNECTED_APPS: "connectedApps",
|
|
171
|
+
TASK_ID: "taskId",
|
|
172
|
+
RUN_ID: "runId",
|
|
173
|
+
VIEW: "view",
|
|
174
|
+
PAGE_ID: "pageId",
|
|
175
|
+
DURATION_MINUTES: "durationMinutes",
|
|
176
|
+
APP: "app",
|
|
177
|
+
CONNECTOR_ID: "connectorId",
|
|
178
|
+
INCLUDE_TEAM: "includeTeam",
|
|
179
|
+
EMAILS: "emails",
|
|
180
|
+
OCCURRENCE_START: "occurrenceStart",
|
|
181
|
+
AUTOMATION_IDS: "automationIds",
|
|
182
|
+
EVENT: "event",
|
|
183
|
+
OUTCOME: "outcome",
|
|
184
|
+
TAG: "tag",
|
|
185
|
+
RECIPIENT: "recipient",
|
|
186
|
+
GROUP_ID: "groupId",
|
|
187
|
+
PAGE_IDS: "pageIds",
|
|
188
|
+
BOOKING_ID: "bookingId",
|
|
189
|
+
SUGGESTED: "suggested",
|
|
190
|
+
CROP: "crop",
|
|
191
|
+
ORDER: "order",
|
|
192
|
+
INCLUDE_REVOKED: "includeRevoked",
|
|
193
|
+
START_INDEX: "startIndex",
|
|
194
|
+
COUNT: "count",
|
|
195
|
+
HIDE_LABELLED: "hideLabelled",
|
|
196
|
+
INACTIVE_DAYS: "inactiveDays",
|
|
197
|
+
MESSAGE_ID: "messageId"
|
|
198
|
+
}.freeze
|
|
199
|
+
|
|
200
|
+
RETRY = {
|
|
201
|
+
STATUSES: [408, 429, 500, 502, 503, 504].freeze,
|
|
202
|
+
METHODS: ["GET"].freeze,
|
|
203
|
+
BASE_BACKOFF_MS: 500,
|
|
204
|
+
MAX_BACKOFF_MS: 8000,
|
|
205
|
+
MAX_HONOURED_RETRY_AFTER_MS: 60000,
|
|
206
|
+
RATE_LIMIT_STATUS: 429
|
|
207
|
+
}.freeze
|
|
208
|
+
|
|
209
|
+
SEALED_ENCRYPTION_FORMATS = ["pgp-mime", "pgp-inline", "smime-encrypted"].freeze
|
|
210
|
+
|
|
211
|
+
UNSPECIFIED_HOSTNAMES = ["0.0.0.0", "::", "[::]"].freeze
|
|
212
|
+
|
|
213
|
+
WEBHOOK_SIGNATURE = {
|
|
214
|
+
VERSION: "v1",
|
|
215
|
+
TIMESTAMP: "t",
|
|
216
|
+
PAIR_SEPARATOR: ",",
|
|
217
|
+
VALUE_SEPARATOR: "=",
|
|
218
|
+
TOLERANCE_SECONDS: 300
|
|
219
|
+
}.freeze
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nextmail
|
|
4
|
+
module RawBody
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def read(data)
|
|
8
|
+
case data
|
|
9
|
+
when String then data
|
|
10
|
+
when Pathname then File.binread(data)
|
|
11
|
+
else read_stream(data)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def size(data)
|
|
16
|
+
case data
|
|
17
|
+
when String then data.bytesize
|
|
18
|
+
when Pathname then File.size(data)
|
|
19
|
+
else stream_size(data)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def slice(data, offset, length)
|
|
24
|
+
case data
|
|
25
|
+
when String then data.byteslice(offset, length)
|
|
26
|
+
when Pathname then File.open(data, "rb") { |file| stream_slice(file, offset, length) }
|
|
27
|
+
else stream_slice(data, offset, length)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def content_type(data, explicit)
|
|
32
|
+
return explicit unless explicit.nil? || explicit.to_s.empty?
|
|
33
|
+
|
|
34
|
+
declared = data.respond_to?(:content_type) ? data.content_type : nil
|
|
35
|
+
return declared if declared.is_a?(String) && !declared.empty?
|
|
36
|
+
|
|
37
|
+
name = file_name(data)
|
|
38
|
+
Defaults::CONTENT_TYPES_BY_EXTENSION[File.extname(name).downcase] if name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def file_name(data)
|
|
42
|
+
return data.original_filename if data.respond_to?(:original_filename) && data.original_filename
|
|
43
|
+
return File.basename(data) if data.is_a?(Pathname)
|
|
44
|
+
|
|
45
|
+
File.basename(data.path) if data.respond_to?(:path) && data.path
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read_stream(data)
|
|
49
|
+
raise ArgumentError, Defaults::MESSAGES[:RAW_BODY_SHAPE] unless data.respond_to?(:read)
|
|
50
|
+
|
|
51
|
+
rewind(data)
|
|
52
|
+
data.read.to_s.b
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stream_size(data)
|
|
56
|
+
raise ArgumentError, Defaults::MESSAGES[:RAW_BODY_SHAPE] unless data.respond_to?(:read)
|
|
57
|
+
return data.size if data.respond_to?(:size)
|
|
58
|
+
|
|
59
|
+
data.stat.size
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stream_slice(data, offset, length)
|
|
63
|
+
raise ArgumentError, Defaults::MESSAGES[:RAW_BODY_SHAPE] unless data.respond_to?(:read)
|
|
64
|
+
|
|
65
|
+
position(data, offset)
|
|
66
|
+
data.read(length).to_s.b
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def position(data, offset)
|
|
70
|
+
return data.seek(offset) if data.respond_to?(:seek)
|
|
71
|
+
|
|
72
|
+
rewind(data) if offset.zero?
|
|
73
|
+
rescue Errno::ESPIPE
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rewind(data)
|
|
78
|
+
data.rewind if data.respond_to?(:rewind)
|
|
79
|
+
rescue Errno::ESPIPE
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nextmail
|
|
4
|
+
module RequestPath
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def fill(pattern, params)
|
|
8
|
+
values = params.transform_keys(&:to_s)
|
|
9
|
+
|
|
10
|
+
pattern.scan(Defaults::PATH_PARAM) { |(name)| assert_segment(values[name]) }
|
|
11
|
+
|
|
12
|
+
pattern
|
|
13
|
+
.gsub(Defaults::OPTIONAL_PATH_PARAM) { optional_segment(values[Regexp.last_match(1)]) }
|
|
14
|
+
.gsub(Defaults::PATH_PARAM) { encode(values[Regexp.last_match(1)]) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def assert_segment(value)
|
|
18
|
+
text = segment_text(value)
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, Defaults::MESSAGES[:EMPTY_SEGMENT] if text.nil? || text.empty?
|
|
21
|
+
raise ArgumentError, "#{text.inspect} #{Defaults::MESSAGES[:DOT_SEGMENT]}" if Defaults::DOT_SEGMENT.match?(text)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def segment_text(value)
|
|
25
|
+
case value
|
|
26
|
+
when String, Symbol then utf8(value.to_s)
|
|
27
|
+
when Numeric then Wire.number(value).to_s
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def utf8(text)
|
|
32
|
+
converted = (text.encoding == Encoding::BINARY) ? text.dup.force_encoding(Encoding::UTF_8) : text.encode(Encoding::UTF_8)
|
|
33
|
+
raise ArgumentError, Defaults::MESSAGES[:SEGMENT_ENCODING] unless converted.valid_encoding?
|
|
34
|
+
|
|
35
|
+
converted
|
|
36
|
+
rescue EncodingError
|
|
37
|
+
raise ArgumentError, Defaults::MESSAGES[:SEGMENT_ENCODING]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def optional_segment(value)
|
|
41
|
+
(value.nil? || value == "") ? "" : "/#{encode(value)}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def encode(value)
|
|
45
|
+
segment_text(value).to_s.gsub(Defaults::URI_COMPONENT_UNSAFE) do |character|
|
|
46
|
+
character.bytes.map { |byte| format("%%%02X", byte) }.join
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nextmail
|
|
4
|
+
class Resource
|
|
5
|
+
CURSOR = Pagination::CURSOR
|
|
6
|
+
PAGE_TOKEN = Pagination::PAGE_TOKEN
|
|
7
|
+
|
|
8
|
+
def initialize(transport)
|
|
9
|
+
@transport = transport
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def inspect
|
|
13
|
+
"#<#{self.class.name}>"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def request(path, **options)
|
|
19
|
+
@transport.request(path, **options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fill(pattern, **params)
|
|
23
|
+
RequestPath.fill(pattern, params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def payload(body, fields)
|
|
27
|
+
Wire.body(body, fields)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def query_params(**values)
|
|
31
|
+
values.to_h { |name, value| [Protocol::QUERY_KEYS.fetch(name.to_s.upcase.to_sym), value] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def joined(values)
|
|
35
|
+
Wire.collection?(values) ? Wire.list(values).join(Protocol::LIST_SEPARATOR) : values
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def flag(value)
|
|
39
|
+
value&.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def instant(value)
|
|
43
|
+
Wire.instant(value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def log_window_query(since, until_value)
|
|
47
|
+
query_params(since: instant(since), until: instant(until_value))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def log_request_query(since, until_value, failed_only, statuses, path)
|
|
51
|
+
status = (statuses.nil? || statuses.empty?) ? nil : joined(statuses)
|
|
52
|
+
log_window_query(since, until_value).merge(query_params(failed_only: flag(failed_only), status:, path:))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fetch_page(path, query = nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
|
|
56
|
+
Pagination.fetch_page(@transport, path, query:, style:, limit:, cursor:, api_key:)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def iterate_pages(path, query = nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil, &)
|
|
60
|
+
each_or_enumerator(Pagination.iterate(@transport, path, query:, style:, limit:, cursor:, api_key:), &)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each_or_enumerator(enumerator, &block)
|
|
64
|
+
block ? enumerator.each(&block) : enumerator
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collect_all(path, query = nil, style: CURSOR, limit: nil, cursor: nil, api_key: nil)
|
|
68
|
+
iterate_pages(path, query, style:, limit:, cursor:, api_key:).to_a
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fetch_list(path, query = nil, api_key: nil)
|
|
72
|
+
Pagination.fetch_list(@transport, path, query:, api_key:)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def raw_content_type(data, content_type)
|
|
76
|
+
RawBody.content_type(data, content_type)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def crop_query(crop)
|
|
80
|
+
query_params(crop: ImageCrop.query(crop))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def upload_name(data, filename)
|
|
84
|
+
name = (filename.nil? ? RawBody.file_name(data) : filename).to_s.strip
|
|
85
|
+
raise ArgumentError, Defaults::MESSAGES[:FILENAME_REQUIRED] if name.empty?
|
|
86
|
+
|
|
87
|
+
name
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def optional_upload_name(data, filename)
|
|
91
|
+
filename.nil? ? RawBody.file_name(data) : filename
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def upload_timeout(timeout = nil)
|
|
95
|
+
at_least(Defaults::UPLOAD_TIMEOUT, timeout)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def turn_timeout(timeout = nil)
|
|
99
|
+
at_least(Defaults::CHAT_TURN_TIMEOUT, timeout)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def at_least(floor, timeout)
|
|
103
|
+
return timeout unless timeout.nil?
|
|
104
|
+
|
|
105
|
+
base = @transport.timeout
|
|
106
|
+
base.to_f.positive? ? [base, floor].max : base
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|