Cloud Mail Postscript: R2 Attachment Verification and Resend Key Rotation
Confirm that Cloud Mail attachments are written to the private R2 storage by verifying receipt of actual emails, checking the preview, download, and anonymous access functions, and then documenting the “Resend” key rotation and sending verification for both the new and old credentials.
- 11 min read
- 2026.10.13
Both test attachments passed SHA-256 verification on the remote R2 server; they can be previewed and downloaded after logging in, while anonymous access returns a 401 error. The new key generated by “Resend” has been synchronized to D1 and KV and verified through message sending via the application and actual receipt.
In the previous post, Cloud Mail Deployment Log, I wrote about the private R2 migration. I reviewed the old data that day and found that D1 had no attachment records, and KV had no attachments or upload history, so there were no historical files to copy. After the bucket was created, it remained empty, which was consistent with the situation at the time.
However, this left one question unanswered: Once production mailboxes actually receive emails with attachments, will the files be written to R2? Will they open when accessed via the web interface? And if someone obtains the attachment URL, will they be able to read it?
On October 13, 2026, I conducted this round of verification. During the process, I also discovered that the old Resend sending key had appeared in the error output of a local tool, so I took the opportunity to handle the key rotation as well. This post is a continuation of the previous one; I will not disclose the actual email address, business domain, resource identifiers, or object paths, nor will I post credentials or raw logs.
First, Send a Real Email with an Attachment
In the previous round of verification, I used a remote R2 binding and also tested the local attachment page. This time, I had an email actually sent via Resend and then enter Cloud Mail through the production environment’s inbox workflow.
The email contained two specially generated test files: a PNG file, 31,695 bytes in size with dimensions of 800 × 480, and a TXT file, 136 bytes in size. The content did not include any personal information.
After the email was received, the corresponding inbox entry and two attachment records appeared in the application. Next, I downloaded these two objects directly from the remote R2 and calculated the full SHA-256 hash for each, comparing them to the original files before sending. Both matched.
I also checked the KV; the keys for these two attachments did not exist there either. Only by comparing them with the files read from the remote R2 could I confirm that these new attachments had been written to R2, and that the read operation succeeded without relying on a KV fallback.
Once again, there was no migration of old attachments. The new test email and the two attachments were retained in the system for future review.
The bucket overview still shows zero, but the files can already be read
During verification, the bucket summary still displayed 0 objects and 0 B . If one looked only at this line, it would be easy to continue suspecting that the objects had not been written.
The Wrangler bucket overview used at the time displayed storage analysis statistics and did not list individual objects. In a separate remote object read test I conducted, I successfully retrieved two complete files, and their hashes matched those of the originals.
These two sets of results need to be recorded separately. The files exist, with evidence from direct reads and byte-level verification; the overview figures had not been refreshed at that time. I did not determine how long it would take for them to update, nor did I wait for the statistics to become non-zero before concluding the verification.
If I encounter a similar situation in the future, I will first check whether the expected objects can be read and whether their contents match, and then review the overview statistics. If an object fails to be read, I will continue to investigate the write process and resource bindings; I cannot assume that an object has been successfully stored simply because the statistics may be delayed.
In addition to private buckets, check the application’s read entry points
The bucket’s r2.dev public access remains disabled, and no public custom domain is bound to the bucket. The application reads files via a Worker, while the browser uses the email client’s own attachment entry point.
This time, I tested both the new attachment API and the attachment path compatible with older emails. Standard HTTP requests covered two files, with the following results.
| Test Item | New Attachment API | Legacy Attachment Path |
|---|---|---|
| Reading PNG and TXT with a valid login session | HTTP 200, content matches original file | HTTP 200, content matches original file |
| Anonymous access to PNG and TXT | HTTP 401 | HTTP 401 |
| Private attachment caching policy | private, no-store | |
private, no-store | ||
Next, open the test email in a production web browser. The PNG preview decoded successfully and displayed dimensions matching the original image; both attachments were downloaded via the download buttons on the page, with filenames as expected, and the SHA-256 hashes of the downloaded files also matched those of the originals. No uncaught exceptions occurred in the browser.
For both previewing and downloading, requests were first sent with session authentication headers, and then Blob URLs were generated using the returned content. The login token was not included in the URLs for the attachment requests. A clean browser without a logged-in state accessed both entry points for the PNG and received a 401 response in both cases.
This round reused an existing valid administrator session. This round validated attachment operations while logged in; we did not re-enter the password to go through the login process again, nor did we test cross-account access using another regular account in the production environment. The ownership permission regression testing documented in the previous post and the production validation here each have their own coverage boundaries.
If an old sending key appears in the error output, replace it
During the initial local sending test, the tool treated the entire contents of a credentials documentation file as request header values. Before the request was even sent, parameter processing threw an error, and the old sending key appeared in the tool’s output along with the error message.
Correcting the reading logic only prevents the old credentials from being output in the future. The old credentials that had already been exposed still needed to be revoked.
This time, I deleted the old key in the Resend backend, recreated a new sending key, and replaced the original private credentials file. Only then did I synchronize the new values to the application. Therefore, the actual sequence was that the old key was deleted first, followed by an update to the Cloud Mail configuration.
For instances that continuously handle outgoing mail tasks, I will arrange the sequence so that a new key is created first, the configuration is verified and switched, and only then is the old key revoked. This minimizes interruptions in outgoing mail during the switchover. This record is still written in the order in which events actually occurred; I cannot rewrite a successful verification that occurred later as if it had been verified earlier.
Subsequent scripts only extract and pass the new credentials within the process; they do not print their contents, nor do they treat the entire documentation file directly as a token. The new key is not written to chat logs or tool logs.
After saving the configuration, we must verify that it is being used for live email sending
In this version of Cloud Mail, the Resend token is stored in the app settings, D1 saves the configuration, and KV caches the configuration. Only the JWT signing key is stored in Worker Secrets. When rotating the sending key, the app settings and their cache must be updated.
Before the switch, I first sent a test email using the new credentials to verify that it was received by a valid email address. Then, I saved the new token via the standard admin settings API, and the settings service updated D1 and refreshed the KV cache.
Finally, during the private verification process, I compared D1, the KV cache, and the original local credentials file—the values in all three locations matched. Credentials for other domains remained unchanged, and unrelated “Resend” keys were not modified. This time, I did not modify the Cloud Mail source code, nor was it necessary to redeploy the Worker.
Simply seeing that the settings were saved successfully does not guarantee that subsequent email sends are using the new values. If the application continues to read from the old cache, the “saved” prompt on the page won’t detect the issue for it. Therefore, in this round, we sent emails again through Cloud Mail’s normal sending interface to verify that they actually reached the sending service.
Ensuring Test Emails Reach Resend
Cloud Mail has internal delivery logic. If all recipients belong to the local domain configured in the application, emails may be delivered directly within the system. In such cases, even if delivery is successful, the Resend key cannot be verified.
For this test, we included an external recipient, used Resend’s official test address, and simultaneously sent the email to our own real email account. We then checked the service provider’s email identifier saved by the app, Resend’s delivery status, and the actual email received in our inbox.
The results showed that the application’s normal sending interface did indeed go through Resend, the service provider’s status was delivered , and the actual email account also received the message sent by the application. At that time, the old key had already been revoked, yet the new key was still able to complete this sending process.
The verification of the old credentials also focused on sending capabilities. When directly requesting the sending interface using the old key, an HTTP 401 response was returned;the old key was no longer listed in the service provider’s key list.
Since keys with only sending permissions may not be able to call management APIs in the first place, a 401 error from the management API alone is insufficient proof of successful revocation. The evidence retained in this instance was that the sending request using the old key was rejected, while the new key successfully sent the email via the application and the recipient actually received it.
After verification was complete, permissions for the original private credential file were restricted to 0600 , and the temporary copies of the new key and backups of the old key generated during the rotation process were also purged. The test emails were retained. This article documents only the results; it does not include complete fingerprints, request headers, account identifiers, or screenshots of email accounts.