thousandmails 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b4a4b596ac5abce715b93136c69714c6020c3cb5c0fe5e3ef85309784171c7a
4
+ data.tar.gz: 1ac22010648ed66129b927163f5cc1b0c2e20584a461527c548ef9069e3609ff
5
+ SHA512:
6
+ metadata.gz: b3b86c412bdf3ea27f520f7c285d0bb3f2f317a0bfce50f9ce93528e14fea27946c86bc5cb62e4e2128e85dfef2db54e7855d7d8f7bb967716b63f7de82d840f
7
+ data.tar.gz: d1be573a21663fbea2de6d6f7b78ec8b7785e1034f92aeb3da8a69de51ce49df9f87385975dfe6fc1a24a8e5d7af6ba3fb098e94d050b8f97c3cd7b41852417e
data/LICENSE ADDED
@@ -0,0 +1,70 @@
1
+ ThousandMails Ruby SDK — Licence
2
+
3
+ Copyright © 2026 Techinorm Solutions Pvt Ltd. All rights reserved.
4
+
5
+ This software is licensed, not sold. By installing, copying or using it you
6
+ accept the terms below. If you do not accept them, do not install or use it.
7
+
8
+ 1. Grant of licence
9
+
10
+ Techinorm Solutions Pvt Ltd grants you a non-exclusive, non-transferable,
11
+ revocable, worldwide licence to:
12
+
13
+ a. install and use this software, in unmodified form, within your own
14
+ applications, for the sole purpose of accessing the ThousandMails service;
15
+ and
16
+
17
+ b. redistribute it, in unmodified form, only as an installed dependency of
18
+ those applications.
19
+
20
+ 2. Restrictions
21
+
22
+ You may not:
23
+
24
+ a. modify, adapt, translate or otherwise alter the source code, or create
25
+ derivative works based on it, in whole or in part;
26
+
27
+ b. reverse engineer, decompile or disassemble the software, except to the
28
+ extent applicable law expressly permits despite this restriction;
29
+
30
+ c. remove, obscure or alter any copyright, trademark or other proprietary
31
+ notice contained in the software;
32
+
33
+ d. distribute, publish, sublicense, sell, rent, lease or lend the software
34
+ other than as permitted by section 1(b), or publish it — in whole or in
35
+ part, under this or any other name — as a separate package or as part of
36
+ a competing software development kit; or
37
+
38
+ e. use the software for any purpose other than accessing the ThousandMails
39
+ service.
40
+
41
+ 3. Automated build steps
42
+
43
+ Bundling, minification, transpilation, compression, tree-shaking and similar
44
+ automated build steps that do not change the behaviour of the software are
45
+ permitted, and are not "modification" for the purposes of section 2(a).
46
+
47
+ 4. Ownership
48
+
49
+ All right, title and interest in the software, including all intellectual
50
+ property rights in it, remain with Techinorm Solutions Pvt Ltd. No rights are
51
+ granted to you except those expressly set out in this licence.
52
+
53
+ 5. No warranty
54
+
55
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+
59
+ 6. Limitation of liability
60
+
61
+ IN NO EVENT SHALL TECHINORM SOLUTIONS PVT LTD BE LIABLE FOR ANY CLAIM,
62
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
63
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
64
+ USE OF OR OTHER DEALINGS IN THE SOFTWARE.
65
+
66
+ 7. Termination
67
+
68
+ This licence terminates automatically, without notice, if you breach any of
69
+ its terms. On termination you must stop using the software and destroy all
70
+ copies in your possession.
data/README.md ADDED
@@ -0,0 +1,390 @@
1
+ # thousandmails
2
+
3
+ Ruby SDK for ThousandMails — transactional email, attachments, statistics,
4
+ delivery logs and a live event stream, behind one client object.
5
+
6
+ - One thing to configure: your API key. No host, no setup step, no init call
7
+ - Zero dependencies — Ruby 3.0+, standard library only
8
+ - Local validation, so a typo fails in microseconds instead of a round trip
9
+ - Automatic idempotency keys on single sends, so a retry can't double-send
10
+ - Typed errors, retry with backoff, and a reconnecting event stream
11
+
12
+ ```bash
13
+ gem install thousandmails
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ Your API key is the only thing to configure. Generate one on the in-app **API
19
+ keys** page, and you're sending:
20
+
21
+ ```ruby
22
+ require "thousandmails"
23
+
24
+ thousandmails = ThousandMails.new(api_key: "tm_live_...")
25
+
26
+ result = thousandmails.send_mail(
27
+ senderemail: "noreply@yourdomain.com",
28
+ to: "customer@example.com",
29
+ subject: "Your receipt",
30
+ html: "<p>Thanks for your order.</p>"
31
+ )
32
+
33
+ puts result["status"], result["messageId"]
34
+ ```
35
+
36
+ Set `THOUSANDMAILS_API_KEY` in your environment and even that argument goes away:
37
+
38
+ ```ruby
39
+ thousandmails = ThousandMails.new
40
+ ```
41
+
42
+ `ThousandMails.new` is `ThousandMails::Client.new`; use whichever reads better.
43
+
44
+ Everything else — the host, timeouts, retries, idempotency keys, validation — is
45
+ configured for you with working defaults. There is no setup step, no
46
+ initialisation call, and nothing to install alongside it.
47
+
48
+ Messages and query filters are plain hashes whose keys are exactly what the API
49
+ documents, so anything you read in the API reference works here unchanged.
50
+ Symbol and string keys are both accepted. Responses come back as hashes with
51
+ **string** keys, matching the JSON on the wire.
52
+
53
+ Because every method also takes options (`timeout:`, `idempotency_key:`), you
54
+ can pass a message either way:
55
+
56
+ ```ruby
57
+ thousandmails.send_mail(senderemail: "...", to: "...", subject: "...", text: "...")
58
+ thousandmails.send_mail({ senderemail: "...", to: "..." }, timeout: 5_000)
59
+ ```
60
+
61
+ > **Timeouts are in milliseconds**, not seconds — `timeout: 30_000` is thirty
62
+ > seconds. The Node, PHP, Python and Ruby SDKs take the same number so a service
63
+ > running more than one of them configures them the same way.
64
+
65
+ ## Methods
66
+
67
+ Every method is reachable two ways — through its resource and as a flat alias on
68
+ the client. `thousandmails.emails.send` and `thousandmails.send_mail` run the same
69
+ code; use whichever reads better.
70
+
71
+ ### Sending — `thousandmails.emails`
72
+
73
+ | Method | Flat alias | Does |
74
+ | ----------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------- |
75
+ | `send(message, **options)` | `send_mail` | Sends one email. Routes to the multipart path when the message carries files. |
76
+ | `send_batch(messages, **options)` | `send_batch` | Up to 100 emails in one request, each with its own outcome. |
77
+ | `send_with_attachments(message, ...)` | `send_mail_with_attachments` | One email with 1–5 files. |
78
+ | `send_batch_with_attachments(messages)` | `send_batch_with_attachments` | Up to 20 emails, each carrying its own files. |
79
+ | `get(message_id)` | `get_message` | Looks up a send by its record id or its SMTP message id. |
80
+
81
+ `send` shadows Ruby's `Object#send` on that object. `deliver` is an alias if you
82
+ would rather not.
83
+
84
+ ### Statistics — `thousandmails.stats`
85
+
86
+ | Method | Flat alias | Does |
87
+ | -------------------- | ------------------------ | ------------------------------------------------------------- |
88
+ | `summary(params)` | `get_stats_summary` | Totals for a window, the preceding window, and derived rates. |
89
+ | `timeseries(params)` | `get_stats_timeseries` | The same metrics bucketed by day, week or month. |
90
+ | `by_sender(params)` | `get_stats_by_sender` | Totals grouped by sender address. |
91
+ | `by_tag(params)` | `get_stats_by_tag` | Send outcomes grouped by tag. |
92
+
93
+ ### Realtime — `thousandmails.realtime`
94
+
95
+ | Method | Flat alias | Does |
96
+ | -------------------- | --------------------------- | -------------------------------------------------------- |
97
+ | `stats(params)` | `get_realtime_stats` | Every metric summed over the last N minutes (1–120). |
98
+ | `per_minute(params)` | `get_realtime_per_minute` | The last N per-minute buckets, oldest first. |
99
+ | `per_second(params)` | `get_realtime_per_second` | Per-second buckets over the last N seconds, zero-filled. |
100
+ | `activity(params)` | `get_realtime_activity` | The most recent events, newest first (1–100). |
101
+ | `stream(**options)` | `stream_events` | A live event stream. Returns an `EventStream`. |
102
+
103
+ ### Logs — `thousandmails.logs`
104
+
105
+ | Method | Flat alias | Does |
106
+ | ------------------------------- | ----------------------- | --------------------------------------------------- |
107
+ | `list(query)` | `get_logs` | One page of events, newest first. |
108
+ | `iterate(query)` | `iterate_logs` | Enumerator over every matching event, paged for you.|
109
+ | `get(event_id)` | `get_log` | One event by id. |
110
+ | `export(query)` | `export_logs` | The filtered log as CSV text. |
111
+ | `export_to_file(path, query)` | `export_logs_to_file` | Writes that CSV to disk and returns the row count. |
112
+
113
+ Every method takes a final `timeout:` keyword (milliseconds, overriding the
114
+ client's; `0` to disable). Single sends also accept `idempotency_key:`.
115
+
116
+ ## Sending
117
+
118
+ ### Raw and templated
119
+
120
+ ```ruby
121
+ # Raw
122
+ thousandmails.send_mail(
123
+ senderemail: "noreply@yourdomain.com",
124
+ to: ["ada@example.com", { name: "Bob", address: "bob@example.com" }],
125
+ cc: "manager@example.com",
126
+ bcc: "archive@example.com",
127
+ subject: "Welcome, {{name}}",
128
+ html: "<p>Hello {{name}}</p>",
129
+ templaterequiredfields: { name: "Ada" }
130
+ )
131
+
132
+ # From a saved template
133
+ thousandmails.send_mail(
134
+ senderemail: "noreply@yourdomain.com",
135
+ to: "ada@example.com",
136
+ templateid: "tpl_abc123",
137
+ templaterequiredfields: { name: "Ada", order: "4417" }
138
+ )
139
+ ```
140
+
141
+ Send _either_ `templateid` _or_ `subject`/`text`/`html` — never both.
142
+ `{{placeholders}}` work in raw bodies as well as templates, and every one of them
143
+ needs a value.
144
+
145
+ Recipients accept a string, a comma-separated string, an array, or
146
+ `{ name:, address: }` hashes. Everyone on `to`/`cc` sees each other — use
147
+ `send_batch` for individually addressed mail. `bcc` is stripped from the message
148
+ and travels in the envelope only, hidden from every other recipient.
149
+
150
+ `senderemail` also answers to `from`, `templateid` to `templateId`, and
151
+ `templaterequiredfields` to `templateRequiredFields`.
152
+
153
+ ### Attachments
154
+
155
+ `send_mail` routes to the multipart path automatically when the message has
156
+ `attachments`:
157
+
158
+ ```ruby
159
+ thousandmails.send_mail(
160
+ senderemail: "billing@yourdomain.com",
161
+ to: "customer@example.com",
162
+ subject: "Invoice 4417",
163
+ text: "Attached.",
164
+ attachments: [
165
+ "./invoices/4417.pdf", # a path
166
+ { path: "/tmp/tmp-2f8a.pdf", filename: "terms.pdf" }, # a path, renamed
167
+ { filename: "lines.csv", content: "sku,qty\nA1,2" }, # text
168
+ { filename: "logo.png", content: png_bytes } # bytes
169
+ ]
170
+ )
171
+ ```
172
+
173
+ Limits, checked locally before anything uploads: 1–5 files per message, 5 MB per
174
+ file, 20 MB per request, unique filenames, and only
175
+ `csv, xls, xlsx, doc, docx, pdf, jpg, jpeg, png`. Files are also inspected on
176
+ arrival — a renamed `.exe` is rejected however it is labelled.
177
+
178
+ ### Batches
179
+
180
+ ```ruby
181
+ batch = thousandmails.send_batch([
182
+ { senderemail: "noreply@yourdomain.com", to: "a@example.com",
183
+ subject: "Hi", text: "One" },
184
+ { senderemail: "noreply@yourdomain.com", to: "b@example.com",
185
+ subject: "Hi", text: "Two" }
186
+ ])
187
+
188
+ batch["results"].each do |entry|
189
+ warn "#{entry['index']} #{entry['error']['message']}" if entry["outcome"] == "error"
190
+ end
191
+ ```
192
+
193
+ A batch **always returns** — each entry succeeds or fails on its own, so check
194
+ `outcome` per entry rather than relying on the call not raising. Up to 100
195
+ messages, or 20 for the attachment variant, where every message must carry at
196
+ least one file.
197
+
198
+ ## Idempotency
199
+
200
+ Single sends carry an idempotency key by default, generated per call. If the SDK
201
+ retries after a timeout or a server error, the key is recognised and the original
202
+ result comes back instead of a second send.
203
+
204
+ ```ruby
205
+ # Your own key, stable across process restarts — de-duplicates at your level too
206
+ thousandmails.send_mail(message, idempotency_key: "receipt-#{order_id}")
207
+
208
+ # Opt out
209
+ thousandmails.send_mail(message, idempotency_key: false)
210
+ ```
211
+
212
+ Keys do not apply to batches, so batch sends are never retried automatically.
213
+
214
+ ## Retries
215
+
216
+ `max_retries` (default 2) applies with exponential backoff and full jitter:
217
+
218
+ | Situation | Retried? |
219
+ | ----------------------------------------- | ------------------------------------------- |
220
+ | Rate limited | yes — the request was refused untouched |
221
+ | Upload capacity reached | yes — same reason, honours `Retry-After` |
222
+ | Server error, timeout, dropped connection | only with an idempotency key |
223
+ | Batch sends | never |
224
+ | Any other rejection | never |
225
+
226
+ A `Retry-After` longer than the SDK is willing to wait (8s) is not slept
227
+ through. The window would still be shut when the retry landed, so the call fails
228
+ immediately instead and `error.retry_after` carries the server's real figure for
229
+ you to back off against.
230
+
231
+ `timeout` applies **per attempt**, not per call: a retried request gets a fresh
232
+ window, so a call can take up to `(max_retries + 1) × timeout` in the worst case.
233
+
234
+ `thousandmails.rate_limit` holds `{ "limit", "remaining", "reset" }` from the last
235
+ response so you can pace yourself before being throttled. The budget is per API
236
+ key — 1000 requests per 15 minutes.
237
+
238
+ ## Reading
239
+
240
+ ```ruby
241
+ summary = thousandmails.get_stats_summary(from: "2026-07-01", to: "2026-07-31")
242
+ puts summary["totals"]["delivered"], summary["rates"]["openRate"]
243
+
244
+ thousandmails.get_stats_timeseries(from: "2026-07-01", interval: "week")
245
+ thousandmails.get_stats_by_sender
246
+ thousandmails.get_realtime_stats(minutes: 15)
247
+
248
+ # One page, or every event
249
+ page = thousandmails.get_logs(event: "bounced", pageSize: 100)
250
+ thousandmails.iterate_logs(event: "bounced").each do |event|
251
+ puts "#{event['recipient']} #{event['dsn']}"
252
+ end
253
+
254
+ thousandmails.export_logs_to_file("./bounces.csv", event: "bounced")
255
+ ```
256
+
257
+ Dates are `YYYY-MM-DD` (UTC), a `Date`, or a `Time`. An unparsable date is
258
+ silently ignored upstream and would quietly widen your window, so the SDK rejects
259
+ it instead.
260
+
261
+ ### Live event stream
262
+
263
+ ```ruby
264
+ stream = thousandmails.stream_events
265
+
266
+ stream.on("email") { |event| puts "#{event['type']} #{event['recipient']}" }
267
+ stream.on("error") { |error| warn "stream: #{error.message}" }
268
+ stream.listen # blocks, dispatching to the handlers above
269
+
270
+ # …or iterate
271
+ thousandmails.stream_events.events.each do |event|
272
+ handle_bounce(event) if event["type"] == "bounced"
273
+ break # breaking out closes the connection
274
+ end
275
+ ```
276
+
277
+ `EventStream` dispatches `ready`, `email`, `heartbeat`, `frame`, `open`, `close`,
278
+ `error` and `end`. It reconnects with backoff after a dropped connection and
279
+ gives up on an authentication failure, which will not resolve itself. Pass
280
+ `reconnect: false` to disable.
281
+
282
+ Nothing is queued: an event goes to exactly one consumer and is then dropped, so
283
+ a stream held open for days does not grow. Always `close` it (or `break` out of
284
+ the loop) — the connection is held open by design and blocks the process
285
+ otherwise.
286
+
287
+ Closing from a handler, or breaking out of `events`, takes effect at once.
288
+ Closing from *another* thread sets `closed?` immediately, but the reader stays
289
+ parked on a blocking socket read until the next frame arrives — at worst one
290
+ heartbeat, which the server sends every 25 seconds.
291
+
292
+ > **A reconnect does not replay what it missed.** The SDK sends `Last-Event-ID`
293
+ > when the server labels frames with an `id:`, but the stream endpoint does not
294
+ > label them today, so a reconnect resumes from the live edge and events that
295
+ > occurred during the gap are not delivered. Where you cannot afford to miss
296
+ > one, treat the stream as a low-latency notification and reconcile against
297
+ > `client.logs` — the log is the record of what happened, the stream is not.
298
+
299
+ ## Errors
300
+
301
+ ```ruby
302
+ begin
303
+ thousandmails.send_mail(message)
304
+ rescue ThousandMails::InvalidInputError
305
+ # rejected locally — nothing was sent
306
+ rescue ThousandMails::ValidationError => e
307
+ warn e.missing_fields.inspect
308
+ rescue ThousandMails::PermissionError => e
309
+ warn "suppressed: #{e.suppressed.inspect}"
310
+ rescue ThousandMails::RateLimitError => e
311
+ warn "retry after #{e.retry_after} seconds"
312
+ end
313
+ ```
314
+
315
+ Every error is a `ThousandMails::Error`. Anything that reached the service and came
316
+ back a failure also carries `status`, `body`, `headers`, `http_method` and `url`.
317
+
318
+ | Class | `status` | Meaning |
319
+ | ------------------------------------------------ | -------- | ---------------------------------------------- |
320
+ | `InvalidInputError` | — | Rejected locally, before anything was sent |
321
+ | `BadRequestError` | 400 | Unknown template, sender, message or event id |
322
+ | `AuthenticationError` | 401 | Missing, malformed or unknown key |
323
+ | `APIError` | 402 | Your plan does not cover it — see `body` |
324
+ | `PermissionError` | 403 | Inactive key, IP-pinned sender, all suppressed |
325
+ | `PayloadTooLargeError` | 413 | An attachment or field went over the limit |
326
+ | `UnsupportedMediaTypeError` | 415 | An upload was not encoded as multipart |
327
+ | `ValidationError` | 422 | Understood and rejected |
328
+ | `RateLimitError` | 429 | Throttled — see `retry_after` |
329
+ | `ServiceUnavailableError` | 503 | Upload capacity reached — safe to retry |
330
+ | `ServerError` | 5xx | Service or transport failure |
331
+ | `TimeoutError` / `ConnectionError` | — | The request never completed |
332
+ | `ConfigError` | — | Bad or missing client configuration |
333
+
334
+ The base class is `ThousandMails::Error` rather than `ThousandMailsError` — the
335
+ namespace already says which library it came from. Everything else is named the
336
+ same as in the Node, PHP and Python SDKs.
337
+
338
+ ### Plan limits
339
+
340
+ Your key inherits its account's plan, so the same subscription that governs the
341
+ dashboard governs this SDK. Two refusals come from there. Both are `402`, both
342
+ arrive as `APIError` with the response body intact, and neither is retried —
343
+ the answer will not change until the plan does or the billing period rolls over.
344
+
345
+ - **`Not included in your plan`** — the call needs an entitlement the plan does
346
+ not carry. The stats, realtime and log methods all need `apiAccess` at
347
+ `full`; a `limited` plan may send and read back its own send records, and
348
+ gets this on everything else. The body names the `feature`, what you hold
349
+ (`current`) and what is `required`.
350
+ - **`Monthly send limit reached`** — the period's send allowance is spent. The
351
+ body carries `limit`, `used`, `requested`, `remaining` and `resetsAt`. A batch
352
+ is weighed whole, so an over-cap batch sends nothing rather than part of
353
+ itself.
354
+
355
+ A `503` carrying `Entitlement check unavailable` is a different thing: the
356
+ service could not read your plan, not a statement about it. That one is
357
+ transient and the retry policy handles it for you.
358
+
359
+ Full detail in the
360
+ [client API reference](../../../backend/client/docs/api-reference.md#plan-entitlements).
361
+
362
+ ## Bring your own HTTP client
363
+
364
+ The SDK talks to the network through a small `Transport` interface and ships a
365
+ `Net::HTTP` implementation, which is what keeps the dependency list empty.
366
+ Supply your own to route through a pooled client or a test double:
367
+
368
+ ```ruby
369
+ class MyTransport < ThousandMails::Transports::Transport
370
+ def send_request(request) = ...
371
+ def stream(request, &on_chunk) = ...
372
+ end
373
+
374
+ thousandmails = ThousandMails.new(api_key: ..., transport: MyTransport.new)
375
+ ```
376
+
377
+ ## License
378
+
379
+ Proprietary. Copyright © 2026 Techinorm Solutions Pvt Ltd. All rights reserved.
380
+
381
+ You may install and use this SDK, **unmodified**, inside your own applications
382
+ to access the ThousandMails service, and redistribute it unmodified as a dependency
383
+ of those applications.
384
+
385
+ You may not modify or adapt the source, create derivative works from it, reverse
386
+ engineer it, strip its notices, or republish it as a separate gem. Automated
387
+ build steps that don't change behaviour — bundling, packaging — are fine.
388
+
389
+ The software is provided "as is", without warranty of any kind. See
390
+ [LICENSE](LICENSE) for the full terms.
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThousandMails
4
+ # The ThousandMails client.
5
+ #
6
+ # require "thousandmails"
7
+ #
8
+ # thousandmails = ThousandMails::Client.new(api_key: ENV["THOUSANDMAILS_API_KEY"])
9
+ # thousandmails.send_mail(senderemail: ..., to: ..., subject: ..., html: ...)
10
+ #
11
+ # Every endpoint is reachable two ways: through its resource
12
+ # (`client.emails.send`) or through a flat alias on the client itself
13
+ # (`client.send_mail`). They run the same code — pick whichever reads better.
14
+ class Client
15
+ attr_reader :http, :emails, :stats, :realtime, :logs
16
+ # Run the local pre-flight checks before sending.
17
+ attr_reader :validate_input
18
+ # Mint an Idempotency-Key per single send so a retry can't double-send.
19
+ attr_reader :auto_idempotency
20
+ attr_reader :validate_options
21
+
22
+ # @param api_key [String] defaults to the THOUSANDMAILS_API_KEY environment variable
23
+ # @param base_url [String] defaults to THOUSANDMAILS_BASE_URL, then the shipped host
24
+ # @param timeout [Integer] per-attempt timeout in **milliseconds** (default
25
+ # 30000); 0 disables it. Milliseconds, not seconds, so the Node, PHP,
26
+ # Python and Ruby SDKs configure identically. A retry gets a fresh window,
27
+ # so a call can take up to (max_retries + 1) x timeout in the worst case.
28
+ # @param max_retries [Integer] retries for safe requests (default 2)
29
+ # @param validate_input [Boolean] run the local pre-flight checks (default true)
30
+ # @param validate_recipients [Boolean] check recipient syntax locally (default true)
31
+ # @param auto_idempotency [Boolean] mint a key per single send (default true)
32
+ # @param headers [Hash] extra headers on every request
33
+ # @param transport [ThousandMails::Transports::Transport] inject an HTTP implementation
34
+ def initialize(api_key: nil, base_url: nil, timeout: nil, max_retries: nil,
35
+ validate_input: true, validate_recipients: true,
36
+ auto_idempotency: true, headers: nil, user_agent: nil,
37
+ transport: nil)
38
+ @http = Http.new(
39
+ api_key: api_key,
40
+ base_url: base_url,
41
+ timeout: timeout,
42
+ max_retries: max_retries,
43
+ headers: headers,
44
+ user_agent: user_agent,
45
+ transport: transport
46
+ )
47
+
48
+ @validate_input = validate_input != false
49
+ @auto_idempotency = auto_idempotency != false
50
+ @validate_options = { validate_recipients: validate_recipients != false }
51
+
52
+ @emails = Resources::Emails.new(self)
53
+ @stats = Resources::Stats.new(self)
54
+ @realtime = Resources::Realtime.new(self)
55
+ @logs = Resources::Logs.new(self)
56
+ end
57
+
58
+ # Where this client points.
59
+ def base_url
60
+ http.base_url
61
+ end
62
+
63
+ # Rate-limit headers from the most recent response, or nil before the first
64
+ # one: `{ "limit" => ..., "remaining" => ..., "reset" => ... }`.
65
+ #
66
+ # The budget is per API key (1000 requests per 15 minutes), which is the
67
+ # tighter of the two limiters in front of the API and therefore the one worth
68
+ # pacing against.
69
+ def rate_limit
70
+ http.last_rate_limit
71
+ end
72
+
73
+ # Escape hatch for anything this SDK doesn't wrap yet. `path` is relative to
74
+ # /mailerapi/client, and the response body is returned as-is.
75
+ #
76
+ # client.request("GET", "/stats/summary")
77
+ def request(http_method, path, **options)
78
+ http.request(http_method, path, **options)[:data]
79
+ end
80
+
81
+ # --- flat aliases --------------------------------------------------------
82
+
83
+ def send_mail(message = nil, **options)
84
+ emails.send(message, **options)
85
+ end
86
+
87
+ def send_batch(messages = nil, **options)
88
+ emails.send_batch(messages, **options)
89
+ end
90
+
91
+ def send_mail_with_attachments(message = nil, **options)
92
+ emails.send_with_attachments(message, **options)
93
+ end
94
+
95
+ def send_batch_with_attachments(messages = nil, **options)
96
+ emails.send_batch_with_attachments(messages, **options)
97
+ end
98
+
99
+ def get_message(message_id, **options)
100
+ emails.get(message_id, **options)
101
+ end
102
+
103
+ def get_stats_summary(params = nil, **options)
104
+ stats.summary(params, **options)
105
+ end
106
+
107
+ def get_stats_timeseries(params = nil, **options)
108
+ stats.timeseries(params, **options)
109
+ end
110
+
111
+ def get_stats_by_sender(params = nil, **options)
112
+ stats.by_sender(params, **options)
113
+ end
114
+
115
+ def get_stats_by_tag(params = nil, **options)
116
+ stats.by_tag(params, **options)
117
+ end
118
+
119
+ def get_realtime_stats(params = nil, **options)
120
+ realtime.stats(params, **options)
121
+ end
122
+
123
+ def get_realtime_per_minute(params = nil, **options)
124
+ realtime.per_minute(params, **options)
125
+ end
126
+
127
+ def get_realtime_per_second(params = nil, **options)
128
+ realtime.per_second(params, **options)
129
+ end
130
+
131
+ def get_realtime_activity(params = nil, **options)
132
+ realtime.activity(params, **options)
133
+ end
134
+
135
+ def stream_events(**options)
136
+ realtime.stream(**options)
137
+ end
138
+
139
+ def get_logs(query = nil, **options)
140
+ logs.list(query, **options)
141
+ end
142
+
143
+ def iterate_logs(query = nil, **options, &block)
144
+ logs.iterate(query, **options, &block)
145
+ end
146
+
147
+ def get_log(event_id, **options)
148
+ logs.get(event_id, **options)
149
+ end
150
+
151
+ def export_logs(query = nil, **options)
152
+ logs.export(query, **options)
153
+ end
154
+
155
+ def export_logs_to_file(file_path, query = nil, **options)
156
+ logs.export_to_file(file_path, query, **options)
157
+ end
158
+ end
159
+ end