mailblastr 5.1.1 → 5.2.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 +4 -4
- data/README.md +47 -1
- data/lib/mailblastr/client.rb +25 -6
- data/lib/mailblastr/domains.rb +5 -0
- data/lib/mailblastr/emails.rb +4 -4
- data/lib/mailblastr/error.rb +18 -0
- data/lib/mailblastr/events.rb +1 -1
- data/lib/mailblastr/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f6ca9c697ddc14eea6b2e355749bfc000d513224e6afe32e04796b3c1cf954d
|
|
4
|
+
data.tar.gz: d0522dd6dfd919babce3fac4324e1adaafc7a9dff6b801815512a53a3d99147e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd2e5834d366d29d01a797ef11db73cc074181084d76be044a2223684d76f0f1935c75df81b449c0f344234a96b9e31d4a941a2e4ff86e4b518c5727c53e7f5e
|
|
7
|
+
data.tar.gz: 977c69e7fcee5e616cc1e55166279d8b784bedeffb45e9d980e288cc6cdbb3a632556ff23a8e73c7412a22e5094413900e1e164cb7a098e0bc6e526a495b88db
|
data/README.md
CHANGED
|
@@ -408,7 +408,7 @@ The key must be **1–255 characters**, measured after the server trims it — 2
|
|
|
408
408
|
|
|
409
409
|
Reusing a key replays the original response; reusing it with a *different* body is a 409 (`invalid_idempotent_request`), and a second request while the first is still in flight is a 409 (`concurrent_idempotent_requests`).
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
`Emails.send`, `Batch.send`, and received-email reply/forward honour the header. Every other endpoint — including `Events.send` — accepts and forwards it but the API ignores it, so a retry there creates a second record. De-duplicate on your side instead.
|
|
412
412
|
|
|
413
413
|
## Rate limits
|
|
414
414
|
|
|
@@ -421,3 +421,49 @@ Full docs: <https://www.mailblastr.com/docs>
|
|
|
421
421
|
## License
|
|
422
422
|
|
|
423
423
|
MIT
|
|
424
|
+
|
|
425
|
+
## Recovery and tracking contracts
|
|
426
|
+
|
|
427
|
+
Use a stable, unique operation key for each intended send, batch, reply, or
|
|
428
|
+
forward. Keep the same key and payload when recovering that operation. These
|
|
429
|
+
are the supported idempotent send endpoints; events do not implement this
|
|
430
|
+
header. Existing calls without options still work.
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
Mailblastr::Emails::Receiving.reply(id, reply, idempotency_key: "reply-operation-1")
|
|
434
|
+
Mailblastr::Emails::Receiving.forward(id, forward, idempotency_key: "forward-operation-1")
|
|
435
|
+
health = Mailblastr::Domains.tracking_health(domain_id)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Automatic retries consider only 429/503. They stop on an original email `id`,
|
|
439
|
+
positive `sent_count`, nonempty `sent` or `reserved`, or `batch_incomplete`.
|
|
440
|
+
An ordinary rate limit can retry; a generic 503 can retry a read or a send with
|
|
441
|
+
the same supported key. Other writes retry only documented pre-processing
|
|
442
|
+
rejections (`service_unavailable`, `sending_service_unavailable`,
|
|
443
|
+
`sending_configuration_unavailable`, `contacts_busy`, `contacts_timeout`).
|
|
444
|
+
No network/body-read failure, 409, 422, or other 5xx is retried automatically.
|
|
445
|
+
The default transport refuses redirects; a custom transport/client must enforce
|
|
446
|
+
its own policy.
|
|
447
|
+
|
|
448
|
+
On a failed or unconfirmed send, inspect `id` with the email retrieval method
|
|
449
|
+
before creating another send. A 422 with an ID can identify an uncertain
|
|
450
|
+
provider handoff; 422 does not always mean nothing happened. For interrupted
|
|
451
|
+
batches, `sent` contains confirmed sends, `reserved` contains the original
|
|
452
|
+
attempted prefix (including uncertain handoffs), and `unsent_count` counts the
|
|
453
|
+
never-attempted tail. Do not resend the full batch or the reserved prefix under
|
|
454
|
+
a new key. Reconcile original IDs first, then submit only known unattempted
|
|
455
|
+
items as a new operation. Recovery fields remain available in the full error
|
|
456
|
+
body as well as language-specific fields/accessors.
|
|
457
|
+
|
|
458
|
+
Tracking health returns `custom_host`, `status` (`shared`, `ready`, or
|
|
459
|
+
`unavailable`), and `checked_at`. Configure custom tracking through the domain
|
|
460
|
+
API and check health before relying on it. A healthy endpoint cannot guarantee
|
|
461
|
+
an open event: recipients may block images, and coupon redemption alone is not
|
|
462
|
+
proof that the tracking pixel loaded. SDKs preserve supplied HTML/text and do
|
|
463
|
+
not infer opens or rewrite editor spacing.
|
|
464
|
+
|
|
465
|
+
Campaign cancellation also stops pending follow-ups for an already-sent
|
|
466
|
+
campaign while retaining its sent history. Permanent received-email deletion
|
|
467
|
+
acknowledges a durable cleanup request; attachment/object cleanup can finish
|
|
468
|
+
asynchronously. Retrying that deletion is safe; it cannot be undone after the
|
|
469
|
+
purge request is accepted.
|
data/lib/mailblastr/client.rb
CHANGED
|
@@ -19,9 +19,8 @@ module Mailblastr
|
|
|
19
19
|
delete: Net::HTTP::Delete
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
|
-
# Only 429
|
|
23
|
-
#
|
|
24
|
-
# side effect (e.g. sending an email twice). Everything else propagates.
|
|
22
|
+
# Only 429/503 are candidates; retry_allowed? also checks progress and
|
|
23
|
+
# whether a write was rejected before processing or protected by a key.
|
|
25
24
|
RETRYABLE_STATUSES = [429, 503].freeze
|
|
26
25
|
|
|
27
26
|
# Upper bound (seconds) on any single backoff wait.
|
|
@@ -30,7 +29,7 @@ module Mailblastr
|
|
|
30
29
|
# `Idempotency-Key` is stored in a VARCHAR(255) column, so the API accepts
|
|
31
30
|
# 1-255 characters measured after it trims the value — 255, not 256 — and
|
|
32
31
|
# answers anything else with 400 invalid_idempotency_key. Only
|
|
33
|
-
# POST /emails
|
|
32
|
+
# POST /emails, POST /emails/batch, and received-email reply/forward read the header; every other endpoint
|
|
34
33
|
# ignores it, so a retry there creates a second resource.
|
|
35
34
|
#
|
|
36
35
|
# Exposed for discoverability only: the SDK sends the key as given and lets
|
|
@@ -68,7 +67,7 @@ module Mailblastr
|
|
|
68
67
|
loop do
|
|
69
68
|
resp = deliver(req, uri)
|
|
70
69
|
code = resp.code.to_i
|
|
71
|
-
return resp unless RETRYABLE_STATUSES.include?(code) && attempt < max
|
|
70
|
+
return resp unless RETRYABLE_STATUSES.include?(code) && attempt < max && retry_allowed?(req, uri, code, resp.body)
|
|
72
71
|
|
|
73
72
|
wait = retry_after_seconds(response_header(resp, "Retry-After"))
|
|
74
73
|
wait ||= [MAX_BACKOFF_SECONDS, 0.5 * (2**attempt)].min
|
|
@@ -77,6 +76,25 @@ module Mailblastr
|
|
|
77
76
|
end
|
|
78
77
|
end
|
|
79
78
|
|
|
79
|
+
def retry_allowed?(req, uri, status, raw)
|
|
80
|
+
body = begin
|
|
81
|
+
JSON.parse(raw.to_s)
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
{}
|
|
84
|
+
end
|
|
85
|
+
body = {} unless body.is_a?(Hash)
|
|
86
|
+
return false if (body["id"].is_a?(String) && !body["id"].empty?) ||
|
|
87
|
+
(body["sent_count"].is_a?(Numeric) && body["sent_count"].positive?) ||
|
|
88
|
+
(body["sent"].is_a?(Array) && !body["sent"].empty?) ||
|
|
89
|
+
(body["reserved"].is_a?(Array) && !body["reserved"].empty?) ||
|
|
90
|
+
body["name"] == "batch_incomplete"
|
|
91
|
+
return true if status != 503 || %w[GET HEAD].include?(req.method)
|
|
92
|
+
return true if %w[service_unavailable sending_service_unavailable sending_configuration_unavailable contacts_busy contacts_timeout].include?(body["name"])
|
|
93
|
+
|
|
94
|
+
req.method == "POST" && !req["Idempotency-Key"].to_s.strip.empty? &&
|
|
95
|
+
%r{/emails(?:/batch|/receiving/[^/]+/(?:reply|forward))?\z}.match?(uri.path)
|
|
96
|
+
end
|
|
97
|
+
|
|
80
98
|
# Wraps Kernel#sleep so tests can stub out the wait. Extracted so the
|
|
81
99
|
# retry loop stays deterministic under test.
|
|
82
100
|
def backoff_sleep(seconds)
|
|
@@ -114,6 +132,7 @@ module Mailblastr
|
|
|
114
132
|
end
|
|
115
133
|
end
|
|
116
134
|
|
|
135
|
+
return nil unless seconds.finite?
|
|
117
136
|
seconds = 0.0 if seconds.negative?
|
|
118
137
|
[seconds.to_f, MAX_BACKOFF_SECONDS].min
|
|
119
138
|
end
|
|
@@ -172,7 +191,7 @@ module Mailblastr
|
|
|
172
191
|
# `sent`/`sent_count` (see Mailblastr::Error).
|
|
173
192
|
raise Mailblastr::Error.new(
|
|
174
193
|
parsed["message"] || "Request failed with status #{code}",
|
|
175
|
-
status_code:
|
|
194
|
+
status_code: code,
|
|
176
195
|
error_name: parsed["name"] || "application_error",
|
|
177
196
|
body: parsed
|
|
178
197
|
)
|
data/lib/mailblastr/domains.rb
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
module Mailblastr
|
|
4
4
|
module Domains
|
|
5
5
|
class << self
|
|
6
|
+
# HTTPS readiness; an unavailable host schedules server-side repair.
|
|
7
|
+
def tracking_health(domain_id)
|
|
8
|
+
Client.request(:get, "/domains/#{Client.path_escape(domain_id)}/tracking-health")
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
# Register a sending domain. POST /domains
|
|
7
12
|
def create(params)
|
|
8
13
|
Client.request(:post, "/domains", body: params)
|
data/lib/mailblastr/emails.rb
CHANGED
|
@@ -134,14 +134,14 @@ module Mailblastr
|
|
|
134
134
|
|
|
135
135
|
# Forward a received email. POST /emails/receiving/:id/forward
|
|
136
136
|
# Receiving.forward(id, { from: "you@yourdomain.com", to: "team@you.com" })
|
|
137
|
-
def forward(email_id, params)
|
|
138
|
-
Client.request(:post, "/emails/receiving/#{Client.path_escape(email_id)}/forward", body: params)
|
|
137
|
+
def forward(email_id, params, options = {})
|
|
138
|
+
Client.request(:post, "/emails/receiving/#{Client.path_escape(email_id)}/forward", body: params, options: options)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
# Reply to a received email's sender, threaded into the conversation.
|
|
142
142
|
# POST /emails/receiving/:id/reply
|
|
143
|
-
def reply(email_id, params)
|
|
144
|
-
Client.request(:post, "/emails/receiving/#{Client.path_escape(email_id)}/reply", body: params)
|
|
143
|
+
def reply(email_id, params, options = {})
|
|
144
|
+
Client.request(:post, "/emails/receiving/#{Client.path_escape(email_id)}/reply", body: params, options: options)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Delete a received email. DELETE /emails/receiving/:id
|
data/lib/mailblastr/error.rb
CHANGED
|
@@ -50,6 +50,24 @@ module Mailblastr
|
|
|
50
50
|
@error_name
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Original email for a failed or unconfirmed send; inspect before resending.
|
|
54
|
+
def id
|
|
55
|
+
value = @body["id"]
|
|
56
|
+
value.is_a?(String) ? value : nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reserved prefix, including uncertain sends. Never automatically resend it.
|
|
60
|
+
def reserved
|
|
61
|
+
value = @body["reserved"]
|
|
62
|
+
value.is_a?(Array) ? value : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The never-attempted tail; distinct from uncertain reserved items.
|
|
66
|
+
def unsent_count
|
|
67
|
+
value = @body["unsent_count"]
|
|
68
|
+
value.is_a?(Integer) ? value : nil
|
|
69
|
+
end
|
|
70
|
+
|
|
53
71
|
# The plan/quota cap this request hit, else nil. Carried by
|
|
54
72
|
# plan_limit_reached, every *_quota_exceeded, contact_limit_reached and
|
|
55
73
|
# ai_credits_exceeded — it says WHICH quota ran out, how much of it was
|
data/lib/mailblastr/events.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Mailblastr
|
|
|
11
11
|
# Identify the contact by `contact_id` OR `email`. Event names cannot
|
|
12
12
|
# start with the reserved "mailblastr:" prefix.
|
|
13
13
|
#
|
|
14
|
-
# NOTE: only POST /emails
|
|
14
|
+
# NOTE: only POST /emails, POST /emails/batch, and received-email reply/forward honour `Idempotency-Key`.
|
|
15
15
|
# An `idempotency_key` passed here is still forwarded, but the server
|
|
16
16
|
# ignores it, so a retry ingests a SECOND event and can enroll the contact
|
|
17
17
|
# twice — de-duplicate on your side instead.
|
data/lib/mailblastr/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailblastr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- MailBlastr
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|