Each mail service brings its own S3 compatible bucket. The connection is
owned by one service and holds backups, mailbox archives and exports. Every
object lives under <service-id>/, and mailbox addresses appear only as
sha256 hashes in keys.
Key layout
<service-id>/backups/<rfc3339>.tar.zst.age
<service-id>/archive/<mailbox-hash>/<yyyy-mm>.mbox.zst.age
<service-id>/exports/<id>.zip.age
<service-id>/.probe/ (throwaway connection test objects)
Objects are encrypted on the mail node before upload, so the bucket never
holds plaintext. The secret access key is sealed in the secrets store under
mail-storage/<service-id> and is never shown again after saving.
Connection test
Connecting runs four steps and reports each one: put, get, list, delete.
A key that can write but not delete is called out as such, because
retention and uninstall cleanup need delete.
Least privilege on AWS S3
Replace BUCKET and SERVICE_ID.
Cloudflare R2
R2 tokens scope to a bucket, not a prefix. Use one bucket per mail service
and an Object Read and Write token scoped to that bucket. Creating the
bucket needs an account level token, so create it in the Cloudflare
dashboard first.
Bucket creation
The bucket is created automatically only when the key is allowed to. If
not, you get the exact policy above and the instruction to create it
yourself. No account wide credentials are ever requested.
Settings
APP_MAIL_STORAGE_TIMEOUT_SECONDS: per operation timeout, default 15.
APP_MAIL_STORAGE_ALLOW_PRIVATE: set to true to allow a private
endpoint such as a self-hosted MinIO. Off by default.
APP_MAIL_UNINSTALL_RETENTION_DAYS: age before cleanup deletes data in
the “after retention” uninstall mode, default 30.
Uninstall cleanup
Deleting a service prefix needs the confirm token delete <service-id>.
“Now” removes everything under the prefix; “after retention” removes only
objects older than the retention window. Other services’ prefixes are never
listed or touched. Last modified on October 31, 2026