feat(email): staging-only /staging/preview/<uuid> + shared render API#171
Conversation
Factor `send_email` so the per-recipient and confirmation email bodies
are produced by `render_recipient_email` / `render_confirmation_email`
(public, pure, no SMTP). `send_email` consumes those and adds the
SMTP/MIME plumbing — keeping the email layout in one place.
Add `GET /staging/preview/<uuid>`, gated on `config.staging_mode()` and
returning 404 otherwise. Response is JSON `{ recipients: [...],
confirmation: ... }` with each entry carrying `recipient`, `subject`,
`from`, `reply_to`, `html`, `text`. The staging website calls this so
developers can read what cryptify *would* have sent without scraping
log lines.
|
Dobby has received the request! Routing to the right specialist now... |
There was a problem hiding this comment.
Code review
Clean behavior-preserving extraction of render_recipient_email/render_confirmation_email; build, clippy, and tests pass. Three minor items inline: a misplaced doc comment, no endpoint-level tests on the new gated route, and a silently-skipped confirmation path that should at least log.
Rule compliance
No issues found.
| @@ -972,6 +972,65 @@ fn usage(store: &State<Store>, api_key: ApiKey, email: String) -> Json<UsageResp | |||
|
|
|||
| /// Parsed byte range derived from an HTTP `Range` header and the resource's | |||
There was a problem hiding this comment.
[Code review] The Parsed byte range... doc comment that used to introduce struct ByteRange is now glued to StagingPreviewResponse, and ByteRange (now ~line 1034) has no doc comment. Split the block: move the staging-preview doc onto StagingPreviewResponse and put the byte-range doc back above struct ByteRange.
| confirmation: Option<RenderedEmail>, | ||
| } | ||
|
|
||
| #[get("/staging/preview/<uuid>")] |
There was a problem hiding this comment.
[Code review] No tests cover /staging/preview/<uuid> itself — the three new unit tests only exercise the renderers. A Rocket local-client test for (a) 404 when staging_mode = false, (b) 404 on unknown uuid, and (c) JSON shape when staging is on would pin the only gate protecting the endpoint from leaking PII.
| @@ -436,31 +512,31 @@ pub async fn send_email( | |||
| } | |||
|
|
|||
| if state.confirm { | |||
There was a problem hiding this comment.
[Code review] Previously state.confirm && state.sender.is_none() panicked via .unwrap(); now it silently skips and only this inline comment hints at the invariant. If the invariant ever loosens, this becomes a silent missing-confirmation bug. Add a log::warn! (or log::error!) on the else/None arm so a violation is at least observable.
- Move the "Parsed byte range…" doc back above `struct ByteRange`; it had been displaced onto `StagingPreviewResponse` by the previous edit. - `send_email`'s confirmation arm now logs at error level when `state.confirm` is set but `render_confirmation_email` returns `None`, instead of silently dropping the sender's copy if the invariant ever loosens. - Three Rocket local-client tests for `/staging/preview/<uuid>`: 404 when `staging_mode = false`, 404 on unknown uuid, and a happy-path case asserting the JSON shape (recipients + confirmation).
|
/dobby re review |
|
Dobby has received the request! Routing to the right specialist now... |
There was a problem hiding this comment.
Code review
All three prior findings addressed: ByteRange doc restored, confirmation None arm now log::error!s the invariant breach, and three Rocket route tests cover the staging_mode gate, unknown-uuid 404, and happy-path JSON shape. Build, clippy, and 119 tests pass.
Rule compliance
No issues found.
* feat(staging): in-page preview of the email cryptify would have sent On staging, cryptify runs with `staging_mode = true` and logs notification emails instead of dispatching via SMTP — handy for safety, painful for testing because the only way to get the /download link was to scrape container logs. This wires up an automatic modal on the "files sent" screen that pulls the rendered email(s) from cryptify's new `GET /staging/preview/<uuid>` endpoint and embeds the HTML body in a sandboxed iframe. A header bar shows From / Reply-To / Subject / To with a switcher when there are multiple recipients (including the sender's confirmation copy). The email body itself is rendered by cryptify, not re-mocked here, so the two cannot drift. The runtime gate is a new `STAGING` flag on `window.APP_CONFIG`, injected by the postguard-ops ConfigMap from the existing `cryptify_staging_mode` Terraform variable. Production sees no UI change. Requires encryption4all/cryptify#171. * review: dedicated empty state + focus-visible on close affordances - The "no recipients" path previously set `status = 'error'` and stuffed the empty-state copy into `errorMessage`, so the modal rendered "Could not load preview." followed by "No recipients to preview." — two conflicting messages. Add a distinct `'empty'` status and render the empty copy on its own. - Add `:focus-visible` outlines on `.close-btn` and `.backdrop-close` so keyboard users get a visible focus indicator on the only two interactive elements outside the content body.
Summary
render_recipient_email/render_confirmation_emailfromsend_email. Both returnRenderedEmail { recipient, subject, from, reply_to, html, text }and share the template plumbing.send_emailnow consumes those renderers; the SMTP/MIME plumbing stays where it was. No behavior change for production sends.GET /staging/preview/<uuid>returns the rendered notification(s) cryptify would have sent for an upload. Gated onconfig.staging_mode()— anything else returns 404. The staging postguard-website will call this so testers can grab the download link from the page itself instead ofkubectl logs cryptify | grep STAGING.Test plan
cargo test --bin cryptify email::— 27 passed (3 new: recipient render, confirmation render w/ sender, confirmation render w/o sender).curl https://storage.staging.postguard.eu/staging/preview/<uuid>returns JSON for a known upload.Pairs with the in-browser preview modal in the postguard-website PR.