cloudflare-email 0.4.0 → 0.4.2
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/CHANGELOG.md +59 -2
- data/README.md +63 -379
- data/SECURITY.md +1 -9
- data/docs/activerecord-tenanted.md +0 -11
- data/docs/custom-ingress.md +1 -15
- data/docs/delivery-events.md +2 -5
- data/docs/features.md +49 -123
- data/docs/getting-started.md +90 -172
- data/docs/mailboxes.md +24 -60
- data/docs/management-engine.md +1 -3
- data/docs/outbox.md +5 -6
- data/docs/reference.md +190 -0
- data/docs/routing-diagnostics.md +0 -2
- data/docs/thread-correlation.md +2 -4
- data/docs/troubleshooting.md +4 -4
- data/lib/cloudflare/email/client.rb +23 -5
- data/lib/cloudflare/email/ingress.rb +16 -4
- data/lib/cloudflare/email/version.rb +1 -1
- data/templates/deploy-to-cloudflare/README.md +8 -10
- data/templates/deploy-to-cloudflare/docs/domain-setup.md +86 -166
- data/templates/worker/README.md +6 -8
- data/templates/worker/docs/domain-setup.md +86 -166
- data/templates/worker/docs/durable-inbound.md +4 -16
- metadata +4 -19
- data/docs/architecture.md +0 -105
- data/docs/tenant-mailboxes-plan.md +0 -149
- data/docs/upgrading-0.2.md +0 -101
- data/docs/upgrading-0.3.md +0 -55
- data/docs/verification/2026-09-10-followup.md +0 -49
- data/docs/verification/2026-09-10-install-upgrade.md +0 -86
- data/docs/verification/2026-09-10-live.md +0 -102
- data/docs/verification/2026-09-10.md +0 -175
- data/docs/verification/2026-09-11-custom-ingress.md +0 -49
- data/docs/verification/2026-09-11-extraction.md +0 -32
- data/docs/verification/2026-09-11-outbound-ledger.md +0 -60
- data/docs/verification/2026-09-11-security.md +0 -76
- data/docs/verification/2026-09-11-tenant-mailboxes.md +0 -75
- data/docs/verification/2026-09-12-destination-context.md +0 -46
- data/docs/verification/2026-09-13-action-mailbox-core.md +0 -96
- data/docs/verification/2026-09-13-durable-inbound-and-routing.md +0 -71
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# Tenant-aware mailbox layer — implementation proposal
|
|
2
|
-
|
|
3
|
-
Status: original design record. The reusable mailbox module and tenant adapter
|
|
4
|
-
are implemented on the feature branch; see [mailboxes](mailboxes.md) and
|
|
5
|
-
[tenant setup](activerecord-tenanted.md) for the actual API. Rebulk application
|
|
6
|
-
migration and live customer rollout remain separate work. Based on the gem's merged 0.2
|
|
7
|
-
code and Rebulk/rebulk-system main at
|
|
8
|
-
`17ffafccb69968dae022acdbb4620b84c65bedfb`.
|
|
9
|
-
|
|
10
|
-
## Product goal
|
|
11
|
-
|
|
12
|
-
Let a Rails application create, suspend and manage named mailboxes for its
|
|
13
|
-
organizations, sites or users. Support organization subdomains such as
|
|
14
|
-
`houston@venezia.rebulk.com`. Reuse the gem's transport, outbox and receipts;
|
|
15
|
-
do not create another delivery ledger. SQLite remains the default integration
|
|
16
|
-
target, including separate SQLite databases with `activerecord-tenanted`.
|
|
17
|
-
|
|
18
|
-
## Rebulk already has the foundations
|
|
19
|
-
|
|
20
|
-
- `Organization` is shared and carries `email_subdomain`.
|
|
21
|
-
- `TenantRecord` uses `activerecord-tenanted` and requires a tenant context.
|
|
22
|
-
- Sites have per-organization email local parts. `tracking@<org>.rebulk.com`
|
|
23
|
-
is an existing reserved organization-level destination.
|
|
24
|
-
- `Customer` is a buyer within an organization, not the tenant root. A customer
|
|
25
|
-
mailbox can belong to that record without creating another tenant database.
|
|
26
|
-
- ActiveStorage is tenant-specific. Mail must resolve its tenant before blob
|
|
27
|
-
access, persistence or job deserialization that needs tenant records.
|
|
28
|
-
- `InboundEmail` records document-processing outcomes inside the tenant.
|
|
29
|
-
`CommunicationMessage` is a shared, encrypted cross-channel operational view.
|
|
30
|
-
Preserve these product roles instead of replacing them with duplicate models.
|
|
31
|
-
- Existing ingest has its own Worker contract, sender authentication and
|
|
32
|
-
authorization, raw archive and document processing. It is not currently the
|
|
33
|
-
gem's ActionMailbox pipeline. Its policy cannot be discarded during migration.
|
|
34
|
-
- `OrganizationEmailDomain` supports sign-in auto-join. Receiving-domain
|
|
35
|
-
ownership must be separate; creating a receiving domain must never grant users
|
|
36
|
-
membership or change login policy.
|
|
37
|
-
|
|
38
|
-
## Database and routing boundary
|
|
39
|
-
|
|
40
|
-
Keep a minimal shared receiving-domain directory: exact canonical domain,
|
|
41
|
-
immutable tenant identity, tenant connection key, lifecycle state and provisioning
|
|
42
|
-
evidence. Enforce domain ownership with a database uniqueness constraint. Never
|
|
43
|
-
construct a SQLite path from an arbitrary recipient string or request parameter.
|
|
44
|
-
|
|
45
|
-
Keep mailbox definitions, address aliases, message memberships, outbox records
|
|
46
|
-
and tenant receipts in the tenant database. Support an application-selected
|
|
47
|
-
ActiveRecord base class so every related gem model uses the intended connection.
|
|
48
|
-
Merely wrapping today's hardcoded `ActiveRecord::Base` models in `with_tenant`
|
|
49
|
-
does not establish that connection contract.
|
|
50
|
-
|
|
51
|
-
The ingress sequence must be:
|
|
52
|
-
|
|
53
|
-
1. Bound the body and verify the complete Worker signature and envelope.
|
|
54
|
-
2. Resolve the exact signed recipient domain in the shared active directory.
|
|
55
|
-
3. Enter the mapped tenant context, without creating a tenant implicitly.
|
|
56
|
-
4. Resolve the registered active mailbox/alias inside that tenant.
|
|
57
|
-
5. Persist raw mail and mailbox membership idempotently, then schedule processing.
|
|
58
|
-
6. Restore tenant context even when processing raises.
|
|
59
|
-
|
|
60
|
-
No MIME To/Cc, browser session fallback, sender domain or guessed organization
|
|
61
|
-
slug may select the receiving tenant. Unknown/suspended domains and addresses
|
|
62
|
-
must have explicit rejection or restricted quarantine outcomes, with no fallback
|
|
63
|
-
to an arbitrary tenant/mailbox. Directory changes and mailbox suspension must
|
|
64
|
-
also be checked when deferred work resumes; never silently reassign old mail.
|
|
65
|
-
|
|
66
|
-
## Optional mailbox module
|
|
67
|
-
|
|
68
|
-
Add explicit opt-in loading and a generator. The initial contract should cover:
|
|
69
|
-
|
|
70
|
-
- Tenant/domain registration and verified provisioning state.
|
|
71
|
-
- Mailbox creation, aliases, suspension and lookup by registered address.
|
|
72
|
-
- Application ownership links for user/site/customer mailboxes, checked within
|
|
73
|
-
the resolved tenant; authenticated actor authorization stays in the host app.
|
|
74
|
-
- Incoming message membership and deduplication without another copy of MIME.
|
|
75
|
-
- Outgoing preparation through an active mailbox with an allowed From address,
|
|
76
|
-
linked to the existing outbox. A customer cannot select another tenant's
|
|
77
|
-
sender merely because both share the same Cloudflare account token.
|
|
78
|
-
- An explicit tenant context adapter and connection-class configuration;
|
|
79
|
-
`activerecord-tenanted` is an optional integration, not a plain-Ruby dependency.
|
|
80
|
-
|
|
81
|
-
Do not claim a finished inbox UI, IMAP/POP service, thread model, AI workflow,
|
|
82
|
-
search engine or membership system. Rebulk supplies those product decisions.
|
|
83
|
-
Document local-part case policy, reserved names and alias behavior before schema
|
|
84
|
-
implementation; normalization must agree across directory, mailbox and send APIs.
|
|
85
|
-
|
|
86
|
-
## Jobs, events and cross-database commits
|
|
87
|
-
|
|
88
|
-
Jobs carry stable tenant and operation/mailbox identities. Establish tenant
|
|
89
|
-
context before loading records. Never rely on a previous request's `Current`
|
|
90
|
-
state, a naked numeric record ID or a Cloudflare account ID as a tenant key.
|
|
91
|
-
Cloudflare account identity and application tenant identity are different.
|
|
92
|
-
|
|
93
|
-
Queue events arrive without a Rails tenant context and may precede a saved send
|
|
94
|
-
response. Use a shared durable receipt intake and a provider correlation
|
|
95
|
-
directory populated by tenant outbox results. Retain unmatched events and retry
|
|
96
|
-
when correlation becomes available; do not infer tenant ownership solely from
|
|
97
|
-
the recipient's external domain. Ambiguous mappings remain unprojected.
|
|
98
|
-
|
|
99
|
-
Delivery from shared intake to tenant projection is at least once. A tenant
|
|
100
|
-
receipt deduplicates it. Mark shared processing complete only after tenant commit;
|
|
101
|
-
crashes between commits cause safe replay. Writes to the shared directory,
|
|
102
|
-
tenant SQLite and job queue are not one transaction. Recovery must cover each
|
|
103
|
-
gap without allowing another send or prematurely acknowledging an event.
|
|
104
|
-
|
|
105
|
-
## Customer subdomains and Cloudflare
|
|
106
|
-
|
|
107
|
-
An organization may use a Rebulk-owned subdomain or, later, an explicitly verified
|
|
108
|
-
customer-owned domain. Track receive readiness and send-domain verification
|
|
109
|
-
separately. A Rails database record or wildcard DNS entry alone does not enable
|
|
110
|
-
Cloudflare email routing or sending.
|
|
111
|
-
|
|
112
|
-
Prefer explicit address rules for the first supported provisioner. Reconcile
|
|
113
|
-
desired state with Cloudflare and retain failures for retry rather than marking
|
|
114
|
-
a mailbox active before its route is ready. Multiple tenants may share a Worker
|
|
115
|
-
and ingress secret within one application/environment; tenant routing stays in
|
|
116
|
-
Rails. Separate Cloudflare accounts require account-aware credentials/jobs.
|
|
117
|
-
|
|
118
|
-
The current catch-all task operates at zone scope. Do not alter the Rebulk apex
|
|
119
|
-
catch-all or promise automatic subdomain catch-alls as part of mailbox creation.
|
|
120
|
-
Verify provider support and limits for the exact subdomain topology before a
|
|
121
|
-
live rollout. Custom domains need ownership verification and safe offboarding.
|
|
122
|
-
|
|
123
|
-
## Implementation slices and acceptance checks
|
|
124
|
-
|
|
125
|
-
1. **Tenant foundation:** configurable model connections, tenant-aware job
|
|
126
|
-
wrappers and migrations in the tenant migration path. Test two real SQLite
|
|
127
|
-
tenant databases with overlapping IDs, missing context, retries and cleanup.
|
|
128
|
-
2. **Mailbox registry:** shared domain mapping plus tenant mailboxes/aliases,
|
|
129
|
-
lifecycle and ownership APIs. Test exact matching, uniqueness races,
|
|
130
|
-
suspension, reserved addresses and isolation for lookup and sending.
|
|
131
|
-
3. **Tenant ingress:** authenticated envelope resolution before ActionMailbox
|
|
132
|
-
storage. Test Bcc routing, raw bytes, duplicate delivery, unknown destinations,
|
|
133
|
-
tenant-specific blob access and processing jobs after process restart.
|
|
134
|
-
4. **Outbound/event integration:** reuse saved MIME/claims, enforce mailbox sender
|
|
135
|
-
identity, add durable shared-to-tenant event dispatch and repair scans. Test
|
|
136
|
-
event-before-response, duplicate receipts and crashes at each commit boundary.
|
|
137
|
-
5. **Rebulk adoption:** map existing org/site addresses, preserve tracking and
|
|
138
|
-
sender-review policies, link existing communication/ingest records, and add
|
|
139
|
-
mailbox management through Rebulk's access resolver. Start behind a feature
|
|
140
|
-
flag on an isolated development address; keep current production routes intact.
|
|
141
|
-
6. **Provisioning and live verification:** test domain/route setup with bounded
|
|
142
|
-
retries, then a real send/reply/attachment/event flow for two test tenants.
|
|
143
|
-
Verify the deployed revision and preserve a dated report before customer rollout.
|
|
144
|
-
|
|
145
|
-
Required regressions include existing plain-Ruby, single-database SQLite and
|
|
146
|
-
PostgreSQL behavior. Do not call the integration complete merely because a
|
|
147
|
-
single-tenant send works. Existing Rebulk sender-authentication metadata must
|
|
148
|
-
remain authenticated through the new Worker contract before its old ingress
|
|
149
|
-
can be retired; the gem's v2 envelope alone does not supply that policy evidence.
|
data/docs/upgrading-0.2.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Upgrade from 0.1.0 to 0.2.0
|
|
2
|
-
|
|
3
|
-
0.2.0 adds delivery events, optional managed mailboxes and opt-in tenant adapters,
|
|
4
|
-
and changes defaults where 0.1.0 could resend mail or misconfigure routing.
|
|
5
|
-
Database multi-tenancy remains off unless explicitly configured.
|
|
6
|
-
|
|
7
|
-
## Application changes
|
|
8
|
-
|
|
9
|
-
1. **Ruby:** the declared minimum is now 3.2, matching the oldest compatibility job. Prefer a maintained Ruby version. CI also covers Ruby 4.0 with Rails 8.1.
|
|
10
|
-
Base64 is now an explicit runtime dependency so isolated Ruby 3.4/4 applications can load the gem without Rails' transitive dependencies.
|
|
11
|
-
2. **Initializer:** replace direct credential lookups with `Cloudflare::Email::Credentials.account_id` and `.api_token`. This fixes the documented environment fallback for existing apps; upgrading the gem does not rewrite an initializer.
|
|
12
|
-
3. **Send retries:** 429 and pre-send connection failures retry by default. Ambiguous failures (read/write timeouts, resets, and 5xx) no longer resend automatically. Set `retry_ambiguous: true` only if duplicate sends are an acceptable tradeoff. Review ActiveJob retries separately.
|
|
13
|
-
4. **Receiving environments:** the installer now configures development and production ingress. Existing applications should add `config.action_mailbox.ingress = :cloudflare` where needed. `--all-envs` also includes test; staging is explicit.
|
|
14
|
-
5. **Dev tunnel:** `cloudflare:email:dev` refuses staging/production. Use separate routed addresses and Workers for development.
|
|
15
|
-
6. **Test sends:** both `FROM=` and `TO=` are required. The unsupported account-domain autodetection endpoint was removed.
|
|
16
|
-
7. **JSON/Rails:** the gem's development bundle pins JSON below 3 because current tested Rails releases call the pre-3.0 positional-options API. If an existing Rails application's bundle selects JSON 3 and fails in `ActiveSupport::JSON.decode`, constrain `gem "json", "< 3"` in that application's Gemfile until its Rails version supports JSON 3. The gem does not impose this Rails-specific constraint on plain Ruby consumers.
|
|
17
|
-
|
|
18
|
-
## Worker and routing migration
|
|
19
|
-
|
|
20
|
-
Copy/review the updated Worker files from this gem; the generator does not silently replace your deployed Worker.
|
|
21
|
-
|
|
22
|
-
0.2.0 is a preproduction protocol break: only the bundled v2 Worker protocol is
|
|
23
|
-
accepted. Missing or v1 signature versions receive HTTP 401 before persistence
|
|
24
|
-
or routing. Its HMAC covers the SMTP envelope as well as unchanged MIME.
|
|
25
|
-
Pause test ingress while updating Rails and the deployed Worker together; verify
|
|
26
|
-
the matching ingress URL and shared secret before resuming delivery. There is no
|
|
27
|
-
legacy routing mode or supported mixed-version rollout.
|
|
28
|
-
|
|
29
|
-
The 0.3 custom-ingress additions preserve v2 as the default and add
|
|
30
|
-
opt-in v3 for authenticated Worker metadata. That additive upgrade is separate
|
|
31
|
-
from the v1-to-v2 break above: upgrade the Rails receiver before enabling v3 in
|
|
32
|
-
your custom Worker. See [custom ingestion](custom-ingress.md).
|
|
33
|
-
|
|
34
|
-
Rails stores authenticated metadata before enqueueing routing jobs.
|
|
35
|
-
`Cloudflare::Email::Envelope.for(inbound_email)` returns a string-keyed `from`/`to`
|
|
36
|
-
hash for accepted ingress. It returns `nil` for records from another ingress or
|
|
37
|
-
older stored records without verified metadata; applications should refuse to
|
|
38
|
-
route those using sender-supplied MIME headers. Route tenant mailboxes using the
|
|
39
|
-
trusted SMTP recipient instead of MIME `To`/`Cc`.
|
|
40
|
-
|
|
41
|
-
The [earlier upgrade rehearsal](verification/2026-09-10-install-upgrade.md) is a
|
|
42
|
-
historical report and its temporary legacy-mode instructions no longer apply.
|
|
43
|
-
Identical MIME for separate SMTP recipients is stored separately; retries for the
|
|
44
|
-
same exact recipient remain duplicates. This includes Bcc deliveries without a
|
|
45
|
-
visible recipient header. The envelope format supports ASCII dot-atom addresses
|
|
46
|
-
up to 254 bytes, with a 64-byte local part, and an empty SMTP sender for bounces.
|
|
47
|
-
|
|
48
|
-
- Node tooling uses a tracked package-lock, `npm ci`, Node 22.12+ (or supported newer), Wrangler 4.131+, and Vitest 5.
|
|
49
|
-
- Include `scripts/wrangler.mjs`, `package-lock.json`, and the updated `wrangler.toml`.
|
|
50
|
-
- Wrangler deploy/dev scripts require `--env development|staging|production`.
|
|
51
|
-
- Both deployment paths now target `cloudflare-email-ingress-<environment>`. Existing unsuffixed Workers are not deleted. Set both secrets on the new Worker, deploy, verify, then deliberately update the corresponding route.
|
|
52
|
-
- The forwarding Worker now times out after 15 seconds and refuses redirects. Ensure the Rails URL is the final HTTPS endpoint. Both deployment paths use compatibility date 2026-09-10.
|
|
53
|
-
- For an installer `--worker-dir` other than the default, pass `SCRIPT=your-directory/src/index.js` to the Ruby deploy task.
|
|
54
|
-
- The development tunnel forces a dedicated origin Host and permits only POSTs to the email ingress. Restart the Rails development server after upgrading; the tunnel task verifies the ingress guard before starting.
|
|
55
|
-
|
|
56
|
-
## Security hardening
|
|
57
|
-
|
|
58
|
-
- Upgrade host applications to patched Rails: tested floors are 7.2.3.2, 8.0.5.1, and 8.1.3.1, with SQLite 2.9.6 in the test stacks. Rails 7.1 is no longer supported. These are development/test constraints, not runtime dependency enforcement in your application.
|
|
59
|
-
- Redeploy the bundled Worker after updating the gem. Rails and the Worker now default to a 25 MiB raw-email limit. Set `MAX_EMAIL_BYTES` to the same positive integer on both sides when overriding it. Configure upstream request limits and deadlines too.
|
|
60
|
-
- API and ingress endpoints require HTTPS, with HTTP allowed only for literal loopback development hosts. Remove URL credentials, fragments, and query options from configured Ruby endpoints.
|
|
61
|
-
- Delivery events require recipient, ISO8601 timestamp, a boolean `terminal`, valid identifiers, and object-shaped optional details. Invalid messages remain unacknowledged; monitor and quarantine poison messages through your queue operations.
|
|
62
|
-
- Event receipt identity and original payload are read-only through normal ActiveRecord updates. Privileged database access remains trusted.
|
|
63
|
-
- See [the security review](verification/2026-09-11-security.md) for evidence and limits.
|
|
64
|
-
- Subdomain routes require separately onboarded routing DNS and DNS Read access for preflight. The provisioner no longer enables the parent apex to make a subdomain work.
|
|
65
|
-
- Catch-all provisioning is explicitly zone-wide. A request for a subdomain that resolves to a parent zone fails rather than replacing the parent's catch-all.
|
|
66
|
-
- Apex enablement uses the current routing DNS API. Permission/setup errors stop provisioning; they are no longer ignored.
|
|
67
|
-
- Rule lookup now paginates and only updates matching single literal-address rules.
|
|
68
|
-
|
|
69
|
-
Do not enable Cloudflare routing at the apex of a domain whose mail belongs to Workspace/Outlook. Follow Cloudflare's current subdomain onboarding flow.
|
|
70
|
-
|
|
71
|
-
## Delivery state and thread correlation
|
|
72
|
-
|
|
73
|
-
Responses and notifications expose `message_id` when present and `suppressed_recipients`. API success can include permanent bounces or suppression. Monitor recipient outcomes.
|
|
74
|
-
|
|
75
|
-
Add a dedicated Queue, Email Sending event subscription, and HTTP pull consumer to use [delivery events](delivery-events.md). Existing applications are not subscribed or polled automatically. Provide an idempotent handler and configure retries/dead-letter handling.
|
|
76
|
-
|
|
77
|
-
For durable sending, install the optional [outbox](outbox.md) migrations and use
|
|
78
|
-
its saved-operation API. Ordinary `deliver_now`/`deliver_later` do not silently
|
|
79
|
-
opt into durable claims. Existing receipt tables from the earlier preproduction
|
|
80
|
-
adapter should normalize their stored `message_id` column before filtered replay;
|
|
81
|
-
retain the raw payload. The reference inbox's migration imports its existing
|
|
82
|
-
attempts, recipients and audits without clearing uncertainty. Outbox and tracking
|
|
83
|
-
generators now refuse destructive rollback; plan forward fixes and backups.
|
|
84
|
-
|
|
85
|
-
The old README's signed-reply identity and exactly-once claims were too strong. The unused signed-ID helper has been removed. A repeated Message-ID is not a send idempotency key. Live testing confirmed Cloudflare replaces custom IDs: store provider IDs and include parent IDs in outgoing reply headers. See [thread correlation](thread-correlation.md).
|
|
86
|
-
|
|
87
|
-
## Verification before publishing/deploying
|
|
88
|
-
|
|
89
|
-
Local automated coverage includes fresh Rails installation/migrations/mailbox execution, task orchestration, packaged installation, duplicate deliveries, mocked sending/routing/event APIs, Worker unit tests, dry-run builds, and actual local workerd-to-Rails forwarding. The runtime test caught and fixed unsupported `redirect: "error"`; the Worker now uses `manual` and rejects 3xx responses. The API tests exercise documented contracts; they do not prove a live account is configured. See the [verification report](verification/2026-09-10.md).
|
|
90
|
-
|
|
91
|
-
In an account and mailboxes you control, verify these before production rollout:
|
|
92
|
-
|
|
93
|
-
- Sending-domain DNS and outbound MIME/attachments/cc/bcc behavior.
|
|
94
|
-
- Subdomain receiving routes and each environment's deployed Worker.
|
|
95
|
-
- Ingress response handling and application mailbox jobs.
|
|
96
|
-
- Queue subscription, event encoding, handler persistence, and acknowledgements.
|
|
97
|
-
- Actual delivered provider Message-ID and reply threading.
|
|
98
|
-
|
|
99
|
-
The automated suite needs no live account. Separate authorized isolated live runs
|
|
100
|
-
are documented in the [live follow-up](verification/2026-09-10-followup.md).
|
|
101
|
-
The gem has not been published by these verification tasks.
|
data/docs/upgrading-0.3.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Upgrade from 0.2 to 0.3
|
|
2
|
-
|
|
3
|
-
Update your Gemfile to `gem "cloudflare-email", "~> 0.3.0"`, run
|
|
4
|
-
`bundle update cloudflare-email`, and deploy Rails first. Updating the gem does
|
|
5
|
-
not replace your deployed Worker, change DNS, or enable optional mailbox features.
|
|
6
|
-
If you are upgrading from 0.1, complete the [0.2 upgrade](upgrading-0.2.md) too.
|
|
7
|
-
|
|
8
|
-
## Receiving mail while Rails is down
|
|
9
|
-
|
|
10
|
-
New installations can use the [Deploy to Cloudflare template](../templates/deploy-to-cloudflare/README.md)
|
|
11
|
-
to provision the Worker resources through Cloudflare's setup form. Existing
|
|
12
|
-
installations should preserve their resources and pending mail using the steps below.
|
|
13
|
-
|
|
14
|
-
The bundled Worker now stores inbound mail in private R2 before accepting it,
|
|
15
|
-
then delivers through a Queue with scheduled recovery. Before deploying it:
|
|
16
|
-
|
|
17
|
-
1. Create an R2 bucket and Queue for each environment.
|
|
18
|
-
2. Configure the `INBOUND_EMAIL_STORE` binding, `INBOUND_EMAIL_QUEUE` producer
|
|
19
|
-
and consumer, and once-per-minute cron from the bundled `wrangler.toml`.
|
|
20
|
-
3. Preserve your ingress URL and shared secret. Deploy with Wrangler first;
|
|
21
|
-
the Rails deploy task does not provision infrastructure.
|
|
22
|
-
4. Verify that mail queues during a Rails outage, drains after recovery, and
|
|
23
|
-
retries without duplicating application records. Monitor pending mail and age.
|
|
24
|
-
|
|
25
|
-
Follow the [durable delivery guide](../templates/worker/docs/durable-inbound.md)
|
|
26
|
-
for commands, retention requirements, capacity limits, and recovery behavior.
|
|
27
|
-
Missing infrastructure fails visibly. To deliberately retain single-attempt
|
|
28
|
-
delivery, explicitly select `INBOUND_DELIVERY_MODE=direct` before deploying.
|
|
29
|
-
Direct mode cannot buffer outages. Keep existing bindings and cron so previously
|
|
30
|
-
stored mail continues to drain. The old `DURABLE_INBOUND_ENABLED` flag is removed.
|
|
31
|
-
|
|
32
|
-
Messages without a parseable Message-ID now receive a deterministic fallback
|
|
33
|
-
across Rails hosts. Historical records created with the old hostname-dependent
|
|
34
|
-
fallback may duplicate on replay; reconcile old archives before replaying them.
|
|
35
|
-
Retain deduplication records for your recovery horizon and make app processing
|
|
36
|
-
idempotent. Durable transport is not an exactly-once delivery guarantee.
|
|
37
|
-
|
|
38
|
-
## Optional features
|
|
39
|
-
|
|
40
|
-
- [Management engine](management-engine.md): mount a server-rendered mailbox UI
|
|
41
|
-
with host authentication, scopes, and permissions. It is not mounted automatically.
|
|
42
|
-
- [Mailboxes](mailboxes.md): enable domain catch-all explicitly on an existing
|
|
43
|
-
active address. Follow the catch-all schema upgrade instructions; exact
|
|
44
|
-
registered addresses still take precedence, and sending remains exact-address only.
|
|
45
|
-
- [Custom ingestion](custom-ingress.md): integrate verified envelopes and raw
|
|
46
|
-
persistence into an existing app. Deploy Rails before enabling v3 metadata
|
|
47
|
-
signatures in a custom Worker; ordinary v2 forwarding remains supported.
|
|
48
|
-
- [Routing delivery confirmation](routing-deliveries.md): configure separate
|
|
49
|
-
analytics credentials and durable receipts if needed. API acceptance alone
|
|
50
|
-
is not proof of delivery, and ambiguous evidence must not trigger a resend.
|
|
51
|
-
|
|
52
|
-
Database tenancy remains opt-in. Existing tenant-enabled applications can now
|
|
53
|
-
preload their schemas without selecting a tenant; actual mailbox access still
|
|
54
|
-
requires explicit tenant context. No new database tables are required merely to
|
|
55
|
-
upgrade the gem or use the durable inbound Worker.
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Envelope, drafting, and delivery verification — 2026-09-10
|
|
2
|
-
|
|
3
|
-
The [install/upgrade rehearsal](2026-09-10-install-upgrade.md) and the
|
|
4
|
-
[inbox E2E report](https://github.com/cole-robertson/agentic-inbox-rails-full/blob/verification/remaining-e2e/docs/verification/2026-09-10-e2e.md)
|
|
5
|
-
extend this pass with crash/restart and live queue dead-letter/replay evidence.
|
|
6
|
-
|
|
7
|
-
This follow-up supersedes the missing-envelope, missing-ledger, and maintenance-failure findings in the initial reports. The app pins gem candidate `be879439353752d2127dd6413bf915f7321e3fa6`; its runtime matches the live-tested `cb9b87a` (the later commit fixes test-fixture reloading only).
|
|
8
|
-
|
|
9
|
-
## Implemented
|
|
10
|
-
|
|
11
|
-
- Worker v2 authenticates SMTP sender/recipient alongside unchanged MIME. Rails stores trusted metadata before routing jobs enqueue and deduplicates separately per recipient. The app routes strictly by trusted recipient; legacy visible-To fallback requires explicit configuration.
|
|
12
|
-
- Drafting supplies actual incoming content and sent history, validates structured output, rejects placeholders, retains existing drafts on failures, and exposes retry/review UI. Re-triage preserves completed tasks and avoids duplicate titles.
|
|
13
|
-
- Review flags combine model output with conservative application checks for approval/financial content, commitments, clarification, and unread attachments. Auto-send and agent SendDraft refuse flagged drafts; the owner can explicitly approve in the UI. These heuristics do not guarantee semantic correctness or protect against every possible tool chain.
|
|
14
|
-
- Durable send claims and immutable attempt snapshots block repeated sends after ambiguous outcomes. Operator reconciliation is scoped, documented, and audited. Event receipts deduplicate before ACK, correlate by provider ID and recipient, and handle ordering/unmatched replay.
|
|
15
|
-
- Compose, UI approval, agent tools, and auto-send share delivery handling. The UI shows state, uncertainty, review reasons, and generation failures. Draft editor state resets when the selected draft changes.
|
|
16
|
-
- Updated Ruby/npm dependencies and resolved the earlier lint, formatting, scanner, and audit findings.
|
|
17
|
-
|
|
18
|
-
## Live evidence
|
|
19
|
-
|
|
20
|
-
Only dedicated addresses under `test.rebulk.com` and an isolated SQLite database/storage were used. Real LLM inference used the configured `gpt-4o-mini`.
|
|
21
|
-
|
|
22
|
-
1. A structured send to To/Cc/Bcc produced three received records sharing the same visible To and Message-ID. Each authenticated SMTP recipient routed to its own mailbox and separate conversation, including the hidden Bcc mailbox.
|
|
23
|
-
2. Four received messages were processed with real triage/drafting. Replies referred to the blue sample box; none contained the earlier scaffolding or placeholders.
|
|
24
|
-
3. A purchase/next-day commitment scenario initially returned a sensible clarification but an incorrect model review flag. Added independent application rules and a regression. The rerun required review, included concrete reasons, and stayed unsent.
|
|
25
|
-
4. An explicitly approved synthetic draft went through the real ActionMailer adapter. Its durable attempt became accepted and then delivered after its provider event was consumed. Cloudflare replaced the custom signed Message-ID; parent reply headers were preserved.
|
|
26
|
-
5. The dedicated queue produced four delivered events (three direct-client recipients plus the app send). A deliberate exception after durable storage caused redelivery; the retry retained four unique receipts and ACKed all four. The app event matched its attempt/recipient; direct-client events remained stored unmatched.
|
|
27
|
-
|
|
28
|
-
The first simultaneous ingress requests reached a cold, reload-enabled temporary test harness: two returned routing errors while one succeeded. After routes loaded, a new send produced all three successful copies. This was a harness startup limitation, not counted as a passing delivery. Use eager loading/disabled reloading and warm the receiver before concurrent live tests.
|
|
29
|
-
|
|
30
|
-
The earlier live report separately covers raw MIME, binary attachment integrity, provider-ID reply round trips, and six initial delivered events.
|
|
31
|
-
|
|
32
|
-
## Automated verification and limits
|
|
33
|
-
|
|
34
|
-
Gem: 197 tests / 631 assertions; 19 Worker tests; 19 real local workerd-to-Rails checks. All 13 CI jobs passed at be87943, including 11 Ruby/Rails combinations:
|
|
35
|
-
https://github.com/cole-robertson/cloudflare-email/actions/runs/34531698927
|
|
36
|
-
|
|
37
|
-
The app suite passes **213 examples**, seed 42422. It covers authenticated ingress, strict mailbox selection, approval endpoints, review/auto-send guards, rejection and ambiguity, process/persistence failure, attempt snapshots, operator authorization, ordering, deduplication, event task wiring, and malformed LLM responses. Lint, TypeScript, Prettier, RuboCop, Brakeman and Ruby/npm audits pass (zero reported vulnerabilities/warnings).
|
|
38
|
-
|
|
39
|
-
Browser acceptance was attempted using the shared T3 preview: status/open repeatedly timed out, including a fresh-tab attempt. No visual browser pass is claimed; authenticated request tests and frontend static checks passed.
|
|
40
|
-
|
|
41
|
-
Only delivered lifecycle events were induced live. Bounce, suppression, complaint, and deferred/failure handling have automated coverage. Queue dead-letter exhaustion was not exercised; no exhaustive provider failure simulation or spam complaints were generated. The token lacks DNS Read, so complete route-provisioning preflight still needs an appropriately scoped credential. Exact test rules were created through Email Routing after the prior DNS verification. No exactly-once guarantee is possible across provider/database boundaries; separate compose submissions are distinct messages.
|
|
42
|
-
|
|
43
|
-
## Cleanup and reproduction
|
|
44
|
-
|
|
45
|
-
Deleted the four exact test routes, dedicated Worker, event subscription, and queue. Existing routing rules and catch-all exactly match the saved baseline. Stopped the local receiver/tunnel; no DNS changes, production migration, merge, or gem publication occurred.
|
|
46
|
-
|
|
47
|
-
Private harness/evidence: `/tmp/cf-email-envelope-live-20260911` (the suffix is a unique run identifier; execution was September 10). No credentials or queue leases are in tracked evidence. Sanitized observations are in [2026-09-10-followup-results.json](2026-09-10-followup-results.json).
|
|
48
|
-
|
|
49
|
-
Run `bundle exec rspec`, `bundle exec rubocop`, `bin/brakeman --no-pager`, `bin/bundler-audit`, `npm run lint`, `npm run check`, `npm run format`, and the checked-in Worker tests/build. See the app's delivery operations guide for event consumption and operator reconciliation. Deploy the new Rails gem and app migrations before the v2 Worker.
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# Install, upgrade, rollback, and staged protocol rehearsal
|
|
2
|
-
|
|
3
|
-
Executed 2026-09-10 with Ruby 4.0.0, Rails 8.1.3.1, gem 0.2.0.
|
|
4
|
-
Gem merge: `cb8d8932296f64b5531ee2e1b773e9937c6df884`.
|
|
5
|
-
Inbox merge: `21bc3627ad70c586c45e7df297db4af4cd070066`.
|
|
6
|
-
The inbox retains its release-candidate gem pin `be879439353752d2127dd6413bf915f7321e3fa6`.
|
|
7
|
-
|
|
8
|
-
## Packaged install
|
|
9
|
-
|
|
10
|
-
```sh
|
|
11
|
-
bundle exec ruby script/verify_package.rb
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Passed: isolated consumer installed the built `.gem` into a temporary bundle,
|
|
15
|
-
loaded version 0.2.0, and did not load Rails. Rails integration fixtures loaded
|
|
16
|
-
the extracted packaged files in separate processes:
|
|
17
|
-
|
|
18
|
-
| Fixture | Tests | Assertions | Failures/errors/skips |
|
|
19
|
-
| --- | ---: | ---: | --- |
|
|
20
|
-
| Inbound | 18 | 95 | 0/0/0 |
|
|
21
|
-
| Send only | 11 | 40 | 0/0/0 |
|
|
22
|
-
| Fresh inbound | 19 | 100 | 0/0/0 |
|
|
23
|
-
|
|
24
|
-
The fresh Rails harness invoked the actual install generator, installed and
|
|
25
|
-
ran Active Storage/Action Mailbox migrations, generated mailbox files and Worker
|
|
26
|
-
files, then received and routed a signed message. This verifies the packaged
|
|
27
|
-
generator in a minimal fresh Rails application; it does not claim a RubyGems
|
|
28
|
-
publication, Docker deployment, or a production installation.
|
|
29
|
-
|
|
30
|
-
## Existing inbox migration and backup recovery
|
|
31
|
-
|
|
32
|
-
From the gem checkout, using the inbox bundle:
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
INBOX_ROOT=/absolute/path/to/agentic-inbox-rails-full \
|
|
36
|
-
BUNDLE_GEMFILE=/absolute/path/to/agentic-inbox-rails-full/Gemfile \
|
|
37
|
-
bundle exec ruby script/verification/install_upgrade.rb
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
All **16 checks passed**. The script creates temporary SQLite databases and
|
|
41
|
-
storage, uses inert test credentials, disables mail delivery, and blocks network
|
|
42
|
-
access during routing. It removes those resources on exit and never migrates the
|
|
43
|
-
configured inbox database.
|
|
44
|
-
|
|
45
|
-
- Migrate a completely empty database to the pre-ledger migration
|
|
46
|
-
`20260418012251`; insert representative incoming, sent, and unapproved messages.
|
|
47
|
-
- Upgrade to the current schema; preserve every original message field and
|
|
48
|
-
timestamp. Previously sent messages become `legacy_sent`, while unsent messages
|
|
49
|
-
become `pending`. No provider IDs or delivery attempts are fabricated.
|
|
50
|
-
- Check review-field defaults on existing rows.
|
|
51
|
-
- Insert an ambiguous send and durable attempt. Take a quiesced SQLite snapshot
|
|
52
|
-
with `VACUUM INTO`, roll back both new migrations, and reapply them.
|
|
53
|
-
- Restore the snapshot into a different database; verify the original ambiguous
|
|
54
|
-
attempt and message state survive. Re-running migrations is idempotent, and an
|
|
55
|
-
actual `DraftDelivery.call` remains blocked without creating another attempt.
|
|
56
|
-
|
|
57
|
-
**Schema rollback is destructive to delivery history.** The rehearsal confirmed
|
|
58
|
-
that down/up migrations preserve old message content but drop attempts and reset
|
|
59
|
-
an ambiguous draft to `pending`. A schema rollback after send attempts exist must
|
|
60
|
-
not be treated as a safe retry/recovery procedure. Stop writers and retain the
|
|
61
|
-
ledger; prefer a forward fix. If restoring a backup, coordinate ingress, jobs,
|
|
62
|
-
and provider reconciliation first: the local quiesced backup test does not prove
|
|
63
|
-
that restoring an earlier production snapshot is safe while external sends are
|
|
64
|
-
in flight. Rolling back to old sending code can also bypass the new ledger guard.
|
|
65
|
-
|
|
66
|
-
## Rails-before-Worker rollout
|
|
67
|
-
|
|
68
|
-
The same script POSTs correctly signed v1/v2 payloads to the real Rails ingress
|
|
69
|
-
and routes them through `AgentMailbox`. A closed, existing conversation avoids
|
|
70
|
-
LLM requests and outbound mail.
|
|
71
|
-
|
|
72
|
-
| Protocol and configuration | Observed result |
|
|
73
|
-
| --- | --- |
|
|
74
|
-
| v1, `ALLOW_LEGACY_EMAIL_ROUTING=true`, single To/no Cc | Accepted and routed into the existing conversation |
|
|
75
|
-
| v1, transitional mode, Cc present | Ingress accepts; mailbox refuses ambiguous routing and records bounced |
|
|
76
|
-
| v1, strict mode | Ingress accepts; mailbox refuses missing authenticated envelope and records bounced |
|
|
77
|
-
| v2, strict mode | Accepted and routed into the existing conversation |
|
|
78
|
-
|
|
79
|
-
Only the two permitted messages enter the conversation; no outbound mail is sent.
|
|
80
|
-
For a staged rollout, deploy Rails with the explicitly temporary legacy flag,
|
|
81
|
-
deploy the v2 Worker, verify authenticated-envelope delivery, then disable the
|
|
82
|
-
flag. Legacy multi-recipient traffic is intentionally refused even during this
|
|
83
|
-
transition; use a controlled ingress pause if this traffic must not be interrupted.
|
|
84
|
-
The protocol replay tests exercise real HMAC verification, persistence, and
|
|
85
|
-
mailbox execution, with synthetic local HTTP requests; they are not additional
|
|
86
|
-
Cloudflare-deployed Worker tests.
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# Live verification — 2026-09-10
|
|
2
|
-
|
|
3
|
-
> Initial live pass. The [follow-up](2026-09-10-followup.md) resolves the
|
|
4
|
-
> envelope-routing, missing-ledger, and maintenance findings below, improves
|
|
5
|
-
> drafting/review behavior, and records new live evidence and remaining limits.
|
|
6
|
-
|
|
7
|
-
The updated gem was exercised against the real Cloudflare account owning
|
|
8
|
-
`rebulk.com`, using only isolated addresses under its existing enabled sending
|
|
9
|
-
subdomain `test.rebulk.com`. The Rails consumer was `agentic-inbox-rails-full`,
|
|
10
|
-
with a private temporary SQLite database/storage and a restricted loopback
|
|
11
|
-
server exposed through a temporary tunnel. Existing production resources and
|
|
12
|
-
the old `cole@test.rebulk.com` experiment were preserved.
|
|
13
|
-
|
|
14
|
-
## Results
|
|
15
|
-
|
|
16
|
-
| Check | Live evidence |
|
|
17
|
-
|---|---|
|
|
18
|
-
| Structured send | One message addressed To/Cc/Bcc: three queued recipients, three actual received copies, three delivered events |
|
|
19
|
-
| MIME headers | Text and HTML received, Cc retained, Bcc absent from MIME headers; sending-domain DKIM, SPF and DMARC passed |
|
|
20
|
-
| Raw MIME | One real send with a 260-byte binary attachment; received and application-stored SHA-256 matches `0d42a8b2489dc6e5632cec0b1f06b4f41b61742536d2c3c361edb990e3b945cd` |
|
|
21
|
-
| Deployed Worker | Gem `WorkerDeployer` uploaded the shipped Worker and set both secrets; all six received copies reached real Rails HMAC ingress |
|
|
22
|
-
| Reply round trip | Two explicitly approved synthetic draft sends through real `DraftDelivery`/ActionMailer; returned reply matched the original conversation using the stored provider ID |
|
|
23
|
-
| Custom signed Message-ID | **Not preserved.** Raw send and ActionMailer sends were assigned Cloudflare IDs; received IDs matched provider responses |
|
|
24
|
-
| Actual LLM | Four inbound messages ran real triage and drafting; categories, summaries, action items and drafts were persisted; auto-send stayed off |
|
|
25
|
-
| Ingress retries | Exact signed replay returned 200 without a new record; invalid signature returned 401 without persistence |
|
|
26
|
-
| Delivery events | Six unique `delivered` events persisted and acknowledged through the real HTTP pull API; subsequent poll returned zero |
|
|
27
|
-
| Handler failure/redelivery | Deliberate exception after durable event insert prevented acknowledgement; next poll redelivered the same event, inserted zero duplicate rows, then acknowledged it |
|
|
28
|
-
| Isolation/cleanup | Four temporary literal rules, one Worker, one event subscription, two queues and local server/tunnel removed; original eight rules and catch-all exactly matched the saved baseline |
|
|
29
|
-
|
|
30
|
-
Four outbound API calls produced six recipient deliveries. No mail was sent to
|
|
31
|
-
people or production ingestion addresses. No DNS records were changed, and the
|
|
32
|
-
apex Google Workspace MX configuration was not touched.
|
|
33
|
-
|
|
34
|
-
## Defects found and fixed
|
|
35
|
-
|
|
36
|
-
1. **Queue JSON encoding.** Real Email Sending subscriptions return plain JSON
|
|
37
|
-
strings with `CF-Content-Type: json`. The consumer previously assumed Base64,
|
|
38
|
-
so the first live poll failed without acknowledging the message. Commit
|
|
39
|
-
`3fda75aa7cd5c54fc7753676e340d1deef4f0ce4` accepts plain JSON and keeps Base64
|
|
40
|
-
compatibility. A sanitized live-shaped fixture and regression checks were
|
|
41
|
-
added. The fixed consumer passed real redelivery/deduplication/acknowledgement.
|
|
42
|
-
2. **App reply headers.** The demo stored provider IDs but its outgoing replies
|
|
43
|
-
omitted `In-Reply-To` and `References`. Its follow-up change sets both from
|
|
44
|
-
the latest inbound message, and the real round trip then correlated correctly.
|
|
45
|
-
|
|
46
|
-
Follow-up automated results: **193 gem tests / 602 assertions pass**, plus all
|
|
47
|
-
13 jobs in [gem CI](https://github.com/cole-robertson/cloudflare-email/actions/runs/34527748184).
|
|
48
|
-
The updated inbox suite passes **144 examples** with eager loading (seed 38933).
|
|
49
|
-
The inbox now pins the fixed gem commit above in
|
|
50
|
-
[PR #26](https://github.com/cole-robertson/agentic-inbox-rails-full/pull/26).
|
|
51
|
-
|
|
52
|
-
## Boundaries and remaining findings
|
|
53
|
-
|
|
54
|
-
- **Inbound Cc/Bcc mailbox selection is not implemented by the demo.** All three
|
|
55
|
-
actual copies retain the same visible To header, while their SMTP recipient
|
|
56
|
-
differs. The Worker forwards raw MIME without a separately authenticated
|
|
57
|
-
envelope recipient; `AgentMailbox` selects the first visible To address.
|
|
58
|
-
Only the primary To copy was processed in this test. Correct multi-mailbox
|
|
59
|
-
Cc/Bcc routing requires an explicit envelope-aware design; do not infer it
|
|
60
|
-
from successful outbound Cc/Bcc delivery.
|
|
61
|
-
- **LLM response quality is not guaranteed.** Real replies sometimes included
|
|
62
|
-
drafting instructions/placeholders; one asked for context that was already
|
|
63
|
-
available. This pass proves live inference and persistence, not acceptable
|
|
64
|
-
autonomous reply quality. Human draft review remains necessary.
|
|
65
|
-
- Wrangler OAuth lacks DNS Read. The gem's subdomain provisioning preflight
|
|
66
|
-
correctly refused that credential before creating rules. Public DNS confirmed
|
|
67
|
-
the three Cloudflare MX records; the four isolated literal rules were then
|
|
68
|
-
created directly using the authorized Email Routing API. The complete
|
|
69
|
-
`provision_route` path still needs a suitably scoped token for live validation.
|
|
70
|
-
- Only `delivered` lifecycle events were induced live. Bounce, suppression,
|
|
71
|
-
complaint, and deferred/failure cases remain automated
|
|
72
|
-
fixture coverage. Dead-letter exhaustion was not tested. No spam complaints or deliberately harmful traffic were
|
|
73
|
-
generated. A dead-letter queue was configured but its exhaustion path was not
|
|
74
|
-
exercised.
|
|
75
|
-
- The gem's development-tunnel orchestration task was not used; this run used an
|
|
76
|
-
explicit temporary tunnel with a restricted receiver. Arbitrary external mail
|
|
77
|
-
providers/clients, SMTP submission, exhaustive browser UI behavior and every
|
|
78
|
-
agent/MCP tool remain outside this live pass.
|
|
79
|
-
- The app's existing dependency-audit/style failures and missing durable
|
|
80
|
-
ambiguous-send ledger remain as documented in its earlier report. No release
|
|
81
|
-
publication, production migration or merge occurred.
|
|
82
|
-
|
|
83
|
-
## Reproduction and evidence
|
|
84
|
-
|
|
85
|
-
Use an isolated app database and dedicated exact receiving addresses. Snapshot
|
|
86
|
-
existing rules/catch-all; deploy a uniquely named shipped Worker; set its HMAC
|
|
87
|
-
secret/receiver URL; provision only the dedicated addresses. Create an HTTP-pull
|
|
88
|
-
queue and a domain-scoped `email.sending` subscription before sending.
|
|
89
|
-
|
|
90
|
-
Exercise `Client#send`, `Client#send_raw`, the app's `DraftDelivery`, and
|
|
91
|
-
`EventConsumer#poll`. Persist event IDs under a unique index before acknowledging;
|
|
92
|
-
deliberately raise once after commit to test idempotent redelivery. Compare actual
|
|
93
|
-
received headers/attachment hashes and conversation IDs. Wait for delivered
|
|
94
|
-
events, then remove only the resources created by the run and compare the saved
|
|
95
|
-
baseline. Do not rerun live sends without confirming the active target scope.
|
|
96
|
-
|
|
97
|
-
Sanitized machine-readable observations are in
|
|
98
|
-
[2026-09-10-live-results.json](2026-09-10-live-results.json). Detailed runner
|
|
99
|
-
scripts, synthetic MIME, local database and private logs remain in
|
|
100
|
-
`/tmp/cf-email-live-20260910` on the verification host; they are not committed.
|
|
101
|
-
No tokens, OAuth callback codes, HMAC secrets, queue leases or LLM keys are in
|
|
102
|
-
the tracked artifacts.
|