hailer 0.5.0 → 0.6.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 +52 -35
- data/hailer.example.toml +7 -1
- data/lib/hailer/version.rb +1 -1
- metadata +15 -2
- data/docs/durable-delivery.md +0 -214
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8cd926b627c51b8e8c96b95ad79bb68b8fbba9c4ca26ab466f03f41283cc35b
|
|
4
|
+
data.tar.gz: 7a4344f35eccd875fe2110aa10a35b0767d42c6acf025260c6813c732e644cb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b9aeebb39e331fb930cfcce8a7063d28a33fedf2314881bab62e469b4674286e807bffdd14b532d1db47199fcf1eb55f2b3cd0612cb1fcbbea2028430faf055
|
|
7
|
+
data.tar.gz: 7622c206fdfccefbee24a7c05952500c8e83a4fca3d0a84bbe05da2f3ebdf9eec4db1a37d6398f04639ed240c023ca2442f88295d83bd11f6ea41dd537a7860d
|
data/README.md
CHANGED
|
@@ -9,23 +9,38 @@ This repository is two things:
|
|
|
9
9
|
- **`hailer` crate** — a standalone Rust mail ship, supervised by Mothership.
|
|
10
10
|
SESv2 is the first provider.
|
|
11
11
|
|
|
12
|
-
```
|
|
13
|
-
|
|
12
|
+
```mermaid
|
|
13
|
+
flowchart LR
|
|
14
|
+
A[ActionMailer] --> B[hailer gem]
|
|
15
|
+
B --> C[private Mothership route]
|
|
16
|
+
C --> D[hailer binary]
|
|
17
|
+
D --> E[SES]
|
|
14
18
|
```
|
|
15
19
|
|
|
16
20
|
An **opt-in durable queued adapter** sits alongside the default synchronous
|
|
17
|
-
send path
|
|
18
|
-
recovery, routing and operator controls.
|
|
21
|
+
send path (`[queued]` in `hailer.example.toml`).
|
|
19
22
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
Requires Ruby 4.0+ and Rails 8.1 or newer within Rails 8.x.
|
|
23
|
-
Rails main (8.2 alpha) is also exercised in CommonShare. Older releases are not supported.
|
|
23
|
+
## Install
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
+
gem "guardship"
|
|
27
|
+
gem "shuttlebay"
|
|
26
28
|
gem "hailer"
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
Mothership picks up the bay and starts sending mail. Keep using your
|
|
32
|
+
mailers, interceptors, observers, `deliver_now`, and `deliver_later` -
|
|
33
|
+
nothing else about how Rails sends mail changes.
|
|
34
|
+
|
|
35
|
+
Everything past this point is reference material: protocol internals,
|
|
36
|
+
TOML knobs, and operator commands for people running or debugging the
|
|
37
|
+
service, not required reading to use it.
|
|
38
|
+
|
|
39
|
+
## Rails
|
|
40
|
+
|
|
41
|
+
Requires Ruby 4.0+ and Rails 8.1 or newer within Rails 8.x.
|
|
42
|
+
Rails main (8.2 alpha) is also exercised. Older releases are not supported.
|
|
43
|
+
|
|
29
44
|
The Railtie registers and selects Hailer automatically. Set the private service
|
|
30
45
|
URL in the Rails process's environment:
|
|
31
46
|
|
|
@@ -50,11 +65,9 @@ when omitted from `hailer_settings`. There is no default host or fallback to SES
|
|
|
50
65
|
The app can explicitly override the default, for example
|
|
51
66
|
`config.action_mailer.delivery_method = :test` in its test environment.
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
`smtp_envelope_to` overrides. After SES accepts the message, it sets
|
|
57
|
-
`message.header[:ses_message_id]`.
|
|
68
|
+
The adapter sends rendered MIME and separate envelope recipients, preserving
|
|
69
|
+
explicit `smtp_envelope_from` and `smtp_envelope_to` overrides. After SES
|
|
70
|
+
accepts the message, it sets `message.header[:ses_message_id]`.
|
|
58
71
|
|
|
59
72
|
## Rust service
|
|
60
73
|
|
|
@@ -120,8 +133,12 @@ shutdown timeout. PostgreSQL 18 is exercised locally and in CI.
|
|
|
120
133
|
|
|
121
134
|
## Bounces, complaints, and suppressions
|
|
122
135
|
|
|
123
|
-
```
|
|
124
|
-
|
|
136
|
+
```mermaid
|
|
137
|
+
flowchart LR
|
|
138
|
+
A[SES] --> B[SNS]
|
|
139
|
+
B --> C[dedicated SQS queue]
|
|
140
|
+
C --> D[Hailer]
|
|
141
|
+
D --> E[durable events + suppressions]
|
|
125
142
|
```
|
|
126
143
|
|
|
127
144
|
Hailer atomically commits each event, deduplication key, and suppression change
|
|
@@ -133,7 +150,7 @@ are left for SQS's dead-letter policy.
|
|
|
133
150
|
- Transient/undetermined bounces are recorded without permanent suppression.
|
|
134
151
|
- An explicit `not-spam` complaint does not create a new suppression.
|
|
135
152
|
- Suppression checks cover the actual To/Cc/Bcc envelope, including overrides.
|
|
136
|
-
Matching
|
|
153
|
+
Matching is case-insensitive and ignores `+alias` tags.
|
|
137
154
|
- Mixed messages keep allowed recipients and their original addresses; MIME
|
|
138
155
|
is unchanged. If every recipient is suppressed, Hailer returns HTTP 422
|
|
139
156
|
without calling SES. A suppression lookup failure fails closed with HTTP 503.
|
|
@@ -223,11 +240,16 @@ the section disables scheduled sync, not existing suppression enforcement.
|
|
|
223
240
|
### Control plane
|
|
224
241
|
|
|
225
242
|
The behavior circuit evaluates **pure admission decisions**: input validity,
|
|
226
|
-
service/storage readiness, suppression, and
|
|
243
|
+
service/storage readiness, suppression, and rate admission. PostgreSQL,
|
|
227
244
|
AWS calls, clocks, and token reservations remain owned by the async host.
|
|
228
245
|
The SES circuit breaker counts transient/ambiguous failures, not bad addresses
|
|
229
246
|
or permanent request rejections. An optional `[rate_limit]` budgets envelope
|
|
230
|
-
recipients
|
|
247
|
+
recipients through a single shared Postgres-backed token bucket - every
|
|
248
|
+
replica draws down the same account-wide budget, not an independent
|
|
249
|
+
per-process one. `discover_from_aws = true` replaces the configured rate
|
|
250
|
+
with the SES account's real `SendQuota.MaxSendRate`, re-checked
|
|
251
|
+
periodically; a rate-state read/write failure fails closed (denies
|
|
252
|
+
admission) rather than risk exceeding the real AWS quota blind.
|
|
231
253
|
|
|
232
254
|
Private `GET /internal/mail/health` returns sanitized component states and
|
|
233
255
|
counters. `GET /internal/mail/ready` returns 503 during initial sync or storage
|
|
@@ -258,8 +280,13 @@ turn off.
|
|
|
258
280
|
|
|
259
281
|
## Domain blocklist
|
|
260
282
|
|
|
261
|
-
```
|
|
262
|
-
|
|
283
|
+
```mermaid
|
|
284
|
+
flowchart LR
|
|
285
|
+
A[SendRequest] --> B[impossible-address check]
|
|
286
|
+
B --> C["domain blocklist<br/>(free, local)"]
|
|
287
|
+
C --> D["suppression<br/>(free, local)"]
|
|
288
|
+
D --> E["verification<br/>(paid, external)"]
|
|
289
|
+
E --> F[SES]
|
|
263
290
|
```
|
|
264
291
|
|
|
265
292
|
Always active - an empty `hailer.domain_blocks` table is a no-op, there is
|
|
@@ -280,8 +307,11 @@ cannot be removed this way, by design.
|
|
|
280
307
|
|
|
281
308
|
## Address verification
|
|
282
309
|
|
|
283
|
-
```
|
|
284
|
-
|
|
310
|
+
```mermaid
|
|
311
|
+
flowchart LR
|
|
312
|
+
A[SendRequest] --> B["suppression check<br/>(cheap, local)"]
|
|
313
|
+
B --> C["address verification<br/>(paid, external)"]
|
|
314
|
+
C --> D[SES]
|
|
285
315
|
```
|
|
286
316
|
|
|
287
317
|
Opt-in pre-send deliverability check, off by default. Enable with
|
|
@@ -408,19 +438,6 @@ each test creates and drops its own database. Never point this variable at a
|
|
|
408
438
|
customer or Rails production database. Neither suite sends real mail or requires
|
|
409
439
|
AWS credentials.
|
|
410
440
|
|
|
411
|
-
For the opt-in CommonShare compatibility smoke, create a disposable Hailer
|
|
412
|
-
database, then run from that application's checkout:
|
|
413
|
-
|
|
414
|
-
```sh
|
|
415
|
-
RAILS_ENV=test HAILER_SMOKE_BINARY=/path/to/hailer/target/debug/hailer \
|
|
416
|
-
HAILER_DATABASE_URL=postgres://test_user:password@127.0.0.1/hailer_smoke \
|
|
417
|
-
bundle exec rails runner /path/to/hailer/test/commonshare_smoke.rb
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
It refuses a non-test app database, points the AWS SDK at a loopback fixture,
|
|
421
|
-
and rolls back application-history changes. It does not change the app's
|
|
422
|
-
dependency lockfile or send external mail.
|
|
423
|
-
|
|
424
441
|
`HAILER_BINARY` is an explicit, version-checked source-build override; there is
|
|
425
442
|
no download or PATH fallback.
|
|
426
443
|
|
data/hailer.example.toml
CHANGED
|
@@ -12,10 +12,16 @@ shutdown_timeout_seconds = 30
|
|
|
12
12
|
[suppression_sync]
|
|
13
13
|
interval_seconds = 3600
|
|
14
14
|
|
|
15
|
-
# Optional
|
|
15
|
+
# Optional. Shared across every replica (one Postgres-backed token bucket,
|
|
16
|
+
# not a per-process budget) - a second replica cannot double the admitted rate.
|
|
16
17
|
# [rate_limit]
|
|
17
18
|
# recipients_per_second = 10.0
|
|
18
19
|
# burst = 50
|
|
20
|
+
# Replace recipients_per_second with the SES account's real
|
|
21
|
+
# SendQuota.MaxSendRate, re-checked periodically. recipients_per_second/burst
|
|
22
|
+
# above become just the seed used until the first successful discovery.
|
|
23
|
+
# discover_from_aws = true
|
|
24
|
+
# discovery_interval_seconds = 900
|
|
19
25
|
|
|
20
26
|
# Durable intake requires explicit logical payload budgets.
|
|
21
27
|
# [queued]
|
data/lib/hailer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hailer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -123,6 +123,20 @@ dependencies:
|
|
|
123
123
|
- - "~>"
|
|
124
124
|
- !ruby/object:Gem::Version
|
|
125
125
|
version: '6.0'
|
|
126
|
+
- !ruby/object:Gem::Dependency
|
|
127
|
+
name: padded_cell
|
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
type: :development
|
|
134
|
+
prerelease: false
|
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '0'
|
|
126
140
|
- !ruby/object:Gem::Dependency
|
|
127
141
|
name: rake
|
|
128
142
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,7 +163,6 @@ files:
|
|
|
149
163
|
- CHANGELOG.md
|
|
150
164
|
- LICENSE.txt
|
|
151
165
|
- README.md
|
|
152
|
-
- docs/durable-delivery.md
|
|
153
166
|
- exe/hailer
|
|
154
167
|
- hailer.example.toml
|
|
155
168
|
- lib/hailer.rb
|
data/docs/durable-delivery.md
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# Durable delivery
|
|
2
|
-
|
|
3
|
-
The queued Ruby interface, PostgreSQL spool and supervised dispatcher are
|
|
4
|
-
implemented and shipped (0.3.0+). It is opt-in: absent `[queued]` config,
|
|
5
|
-
Hailer stays on the synchronous send path and none of this runs.
|
|
6
|
-
|
|
7
|
-
## Opt-in Rails configuration
|
|
8
|
-
|
|
9
|
-
Keep `HAILER_URL` pointing to the existing private `/internal/mail/send` endpoint.
|
|
10
|
-
The queued client uses the same validated origin with `/internal/mail/messages`.
|
|
11
|
-
|
|
12
|
-
```ruby
|
|
13
|
-
# config/environments/production.rb
|
|
14
|
-
config.action_mailer.delivery_method = :hailer_queued
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The initializer registers the adapter and selects `Hailer::MailDeliveryJob` for
|
|
18
|
-
this opt-in configuration. The default remains the synchronous `:hailer`
|
|
19
|
-
adapter, whose HTTP requests never retry.
|
|
20
|
-
|
|
21
|
-
For a per-mailer opt-in, select both:
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
self.delivery_method = :hailer_queued
|
|
25
|
-
self.delivery_job = Hailer::MailDeliveryJob
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
An explicitly configured application delivery job is not replaced. It must
|
|
29
|
-
inherit `Hailer::MailDeliveryJob` or include `Hailer::DeliveryJobContext` to
|
|
30
|
-
obtain stable submission keys across job retries. This uses public ActiveJob
|
|
31
|
-
callbacks, not Rails monkey patches.
|
|
32
|
-
|
|
33
|
-
## Identities and retries
|
|
34
|
-
|
|
35
|
-
- Standard queued jobs use job ID plus delivery ordinal. Different jobs are
|
|
36
|
-
different intents unless the application supplies a business key.
|
|
37
|
-
- Direct `deliver_now` calls generate a fresh key per invocation.
|
|
38
|
-
- Supply a business key through `delivery_method_options` when separate calls
|
|
39
|
-
represent the same intended message:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
mail(
|
|
43
|
-
to: recipient,
|
|
44
|
-
subject: 'Invoice',
|
|
45
|
-
delivery_method_options: { submission_key: "invoice:#{invoice.id}:issued" }
|
|
46
|
-
)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
- Supply an optional ISO 8601 `expires_at` through those same options. The
|
|
50
|
-
service contract defaults to 24 hours and allows shorter deadlines.
|
|
51
|
-
- A queued intake call serializes its command once. Up to three HTTP attempts
|
|
52
|
-
reuse those exact bytes, key and deadline after transport loss or retryable
|
|
53
|
-
admission errors (429/503). Set `intake_attempts: 1` to disable these retries.
|
|
54
|
-
- Validation, submission conflicts, malformed responses and provider-unknown
|
|
55
|
-
errors do not retry.
|
|
56
|
-
- Rails re-renders mail when a job is performed again. If the bytes or envelope
|
|
57
|
-
change under an accepted key, the service must return a conflict; the adapter
|
|
58
|
-
raises `Hailer::SubmissionConflictError`. Job keys alone do not provide
|
|
59
|
-
transparent exactly-once business delivery.
|
|
60
|
-
|
|
61
|
-
Submission keys are command metadata, not MIME headers.
|
|
62
|
-
|
|
63
|
-
## Private status and lifecycle feed
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
client = Hailer::LifecycleClient.new # same HAILER_URL origin
|
|
67
|
-
status = client.status(hailer_message_id)
|
|
68
|
-
page = client.poll(after: saved_cursor, limit: 100)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Status distinguishes the original RFC Message-ID, Hailer ID and provider ID,
|
|
72
|
-
and never returns MIME. `status.attempts` and `status.recipient_outcomes` contain
|
|
73
|
-
the latest 100 records each, in chronological order. Their corresponding
|
|
74
|
-
`*_truncated` flags report older records; use the lifecycle feed for complete
|
|
75
|
-
observations. An accepted message can still have an uncertain historical
|
|
76
|
-
attempt after an operator-authorized retry. Lifecycle events have their own cursor space; do not
|
|
77
|
-
reuse an existing `FeedbackClient` cursor. Persist `page.next_cursor` only after
|
|
78
|
-
processing the page successfully. Re-reading a cursor does not delete events.
|
|
79
|
-
|
|
80
|
-
The service's `[queued]` configuration requires explicit `max_bytes` and
|
|
81
|
-
`max_messages`. Bytes measure canonical logical command payload, not PostgreSQL
|
|
82
|
-
table/index/WAL/backups or a physical disk quota. All replicas of a deployment
|
|
83
|
-
must use the same budgets. Admission is transactional; a replay does not consume
|
|
84
|
-
another slot, and accepted messages are never evicted to admit new ones.
|
|
85
|
-
|
|
86
|
-
## Acceptance is not delivery
|
|
87
|
-
|
|
88
|
-
HTTP 202 means the service durably owns the message. The adapter exposes its ID
|
|
89
|
-
through `message.header[:hailer_message_id]` before Rails observers run. It does
|
|
90
|
-
not fabricate `ses_message_id`; provider acceptance and recipient delivery
|
|
91
|
-
are later observations.
|
|
92
|
-
|
|
93
|
-
Application observers must distinguish durable ownership from provider
|
|
94
|
-
acceptance: persist the Hailer ID and an owned/queued state, then consume
|
|
95
|
-
lifecycle facts separately. CommonShare's existing observer still writes
|
|
96
|
-
`status: "sent"` and only the SES ID for every delivered callback. The isolated
|
|
97
|
-
queued smoke verifies transport compatibility, not that observer's readiness
|
|
98
|
-
for production opt-in. Keep its synchronous configuration until this application
|
|
99
|
-
cutover work is done.
|
|
100
|
-
|
|
101
|
-
Unknown provider outcomes are held without an automatic resend. Seven-day
|
|
102
|
-
unknown-body retention (measured from durable intake) does not extend the
|
|
103
|
-
24-hour send-start deadline. Body purge retains receipts and uncertainty.
|
|
104
|
-
Only positive evidence of no acceptance permits automatic retry, with jittered
|
|
105
|
-
backoff capped at 600 seconds and at most 255 automatic attempts per intent.
|
|
106
|
-
Expiry stops new attempts; it never proves a previous attempt was unsent.
|
|
107
|
-
|
|
108
|
-
Live bodies are purged after confirmed acceptance, terminal rejection or
|
|
109
|
-
suppression, cancellation, or safe queued expiry. Purge releases logical
|
|
110
|
-
capacity, not PostgreSQL allocated disk, backups or copies already in-flight.
|
|
111
|
-
|
|
112
|
-
## Feedback and private routing
|
|
113
|
-
|
|
114
|
-
Set root `ses_configuration_set = "hailer-feedback"` in Hailer's TOML, or use
|
|
115
|
-
an equivalent default configuration set on the SES sending identity. Configure
|
|
116
|
-
SES event publishing through the dedicated SNS/SQS feedback route, preserving
|
|
117
|
-
`mail.tags.hailer_message_id` and `mail.tags.hailer_attempt_id`. These unique tags
|
|
118
|
-
allow feedback to resolve an unknown SDK response. A provider ID already
|
|
119
|
-
captured from an accepted response also correlates later untagged feedback.
|
|
120
|
-
Without tags and without a captured provider ID, feedback cannot resolve
|
|
121
|
-
uncertainty. Hailer never guesses from recipient, timestamps or RFC Message-ID.
|
|
122
|
-
Feedback received before an untagged provider ID is captured is not retroactively
|
|
123
|
-
correlated; retain the tags for reliable recovery.
|
|
124
|
-
|
|
125
|
-
Mothership must route the new private paths as well as the old synchronous
|
|
126
|
-
ones. A suitable bind pattern is:
|
|
127
|
-
|
|
128
|
-
```toml
|
|
129
|
-
routes = [{ bind = "mail", pattern = "^/internal/mail/(send|events|health|ready|intake-ready|dispatch-ready|messages|messages/events|messages/[0-9a-f-]{36})$" }]
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Keep this bind private. Intake readiness depends on durable writable capacity,
|
|
133
|
-
not SES health. `/internal/mail/dispatch-ready` separately reports worker,
|
|
134
|
-
bootstrap and control eligibility. Neither readiness response guarantees that
|
|
135
|
-
a later send will succeed.
|
|
136
|
-
|
|
137
|
-
## Operator control
|
|
138
|
-
|
|
139
|
-
Run against the intended database using the same `HAILER_DATABASE_URL`:
|
|
140
|
-
|
|
141
|
-
```sh
|
|
142
|
-
hailer message-status --id MESSAGE_UUID
|
|
143
|
-
hailer cancel-message --id MESSAGE_UUID --reason "intent withdrawn"
|
|
144
|
-
hailer resolve-message --id MESSAGE_UUID --outcome accepted \
|
|
145
|
-
--provider-message-id VERIFIED_SES_ID --reason "verified provider evidence"
|
|
146
|
-
hailer resolve-message --id MESSAGE_UUID --outcome not-accepted \
|
|
147
|
-
--reason "verified no provider acceptance"
|
|
148
|
-
hailer resolve-message --id MESSAGE_UUID --outcome retry \
|
|
149
|
-
--acknowledge-duplicate-risk --reason "investigated; accept duplicate risk"
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Cancellation is only possible before dispatch and cannot unsend mail.
|
|
153
|
-
Resolution defaults to the current held-unknown attempt. Use `--attempt-id
|
|
154
|
-
ATTEMPT_UUID` to resolve an older unknown journal entry without overwriting a
|
|
155
|
-
later attempt. Retry is allowed only for the current held intent, with an
|
|
156
|
-
available body, an unexpired deadline, no acceptance latch and explicit
|
|
157
|
-
duplicate-risk acknowledgement. A new attempt never erases the old uncertainty.
|
|
158
|
-
All decisions require an audit reason. After expiry or body purge, sending
|
|
159
|
-
requires a new intent/key.
|
|
160
|
-
|
|
161
|
-
## Quarantine
|
|
162
|
-
|
|
163
|
-
Opt-in, queued pipeline only - a synchronous `:hailer` send has no way to
|
|
164
|
-
hold a message for later review, so a non-ASCII domain or a newly
|
|
165
|
-
registered one makes the synchronous adapter reject outright. The queued
|
|
166
|
-
adapter holds instead:
|
|
167
|
-
|
|
168
|
-
```toml
|
|
169
|
-
[domain_intel]
|
|
170
|
-
provider = "rdap"
|
|
171
|
-
new_domain_threshold_days = 30
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
A recipient domain that is not plain ASCII (a malformed/unencoded IDN, or a
|
|
175
|
-
homograph-spoofing attempt - legitimate IDNs are transmitted as ASCII
|
|
176
|
-
punycode) is always checked, regardless of this block. `[domain_intel]`
|
|
177
|
-
additionally checks the domain's RDAP registration date; newer than the
|
|
178
|
-
threshold holds the message as `quarantined` instead of admitting it as
|
|
179
|
-
`queued`. The whole message is quarantined, not just the flagged recipient -
|
|
180
|
-
this is an operator review gate on intent, not a per-recipient filter like
|
|
181
|
-
suppression or the domain blocklist (`hailer ban-domain`). RDAP's bootstrap
|
|
182
|
-
registry is public; no credential is needed.
|
|
183
|
-
|
|
184
|
-
```sh
|
|
185
|
-
hailer release-quarantine --id MESSAGE_UUID --reason "reviewed, legitimate"
|
|
186
|
-
hailer cancel-message --id MESSAGE_UUID --reason "rejected on review"
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Releasing resets `next_attempt_at` to now, so the message re-enters the
|
|
190
|
-
normal dispatch queue immediately. `cancel-message` also accepts a
|
|
191
|
-
quarantined message. An unactioned quarantine still expires on the same
|
|
192
|
-
deadline as a normal `queued` message.
|
|
193
|
-
|
|
194
|
-
## Local application verification
|
|
195
|
-
|
|
196
|
-
`test/commonshare_smoke.rb` runs inside CommonShare's test environment with a
|
|
197
|
-
real Mothership bay and loopback SES, never real AWS credentials. Use a fresh
|
|
198
|
-
loopback PostgreSQL database named `hailer_smoke_*` for each queued run.
|
|
199
|
-
The script intentionally reuses one business key to check replay after purge.
|
|
200
|
-
|
|
201
|
-
```sh
|
|
202
|
-
RAILS_ENV=test DISABLE_BOOTSNAP=1 \
|
|
203
|
-
HAILER_SMOKE_QUEUED=1 \
|
|
204
|
-
HAILER_SMOKE_BINARY=/absolute/path/to/current/hailer \
|
|
205
|
-
HAILER_DATABASE_URL=postgres://...@127.0.0.1:5432/hailer_smoke_queued \
|
|
206
|
-
bundle exec rails runner /absolute/path/to/hailer/test/commonshare_smoke.rb
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
An isolated temporary `BUNDLE_GEMFILE` can select the working-tree gem or locally
|
|
210
|
-
installed verification gem without editing CommonShare's Gemfile or lock.
|
|
211
|
-
Set `HAILER_SMOKE_EXPECTED_PLATFORM=arm64-darwin` to assert native-gem selection.
|
|
212
|
-
Omit `HAILER_SMOKE_QUEUED` for the unchanged synchronous contract. The test holds
|
|
213
|
-
the provider response until Rails has its receipt, then checks the journal,
|
|
214
|
-
correlation tags, envelope/attachment, body purge, replay, conflict and feeds.
|