Be somewhere a phone can share to #307

Closed
opened 2026-09-08 05:41:01 +00:00 by jcoffey-dev · 0 comments
Owner

#306 let ihasmail hand a file to the share sheet. This puts it in one: share a photo from the gallery, a link from the browser or a document from a file manager, and ihasmail is one of the places it can go — opening a draft that holds it.

Why the service worker answers it

A share is a POST that navigates, and nothing on this side can answer one directly: the app is a client-side router with no endpoint at that address, and the server behind it would need a route that understood the composer. So the worker intercepts the POST, takes the form body, stashes files and text in its cache, and redirects to the app, which collects them on start.

Subject from the shared title; text and link become the body; files are attached and begin uploading. Nothing is addressed — a share says what to send, never who to.

Two decisions worth reviewing

The body goes in above the signature, not through open(). open() only fits a signature when it is handed no body at all, so passing the shared text to it silently drops the signature from every message that started as a share — and nothing about the resulting draft looks wrong. There's a test pinning it.

Collected on every start, not on ?share=1. A share to a signed-out ihasmail lands on the sign-in page, and addFiles needs an account before it can upload, so the payload has to outlive a redirect and a login — which a query string does not. The cost is a stash nobody comes back for, so it carries a timestamp and expires after ten minutes.

Smaller notes

  • accept lists wildcard families and explicit MIME types and extensions. A mail client attaches anything, but wildcard support is not in the spec and operating systems differ over which form they match on, so the explicit list is what holds if the families are ignored. This is the part most likely to need adjusting after a real device test.
  • The service worker's cache name now has one home on the app side (lib/swCache.ts). It was written out twice, and a drift would not fail — a push verification would simply never complete and a share would arrive at an empty composer.
  • One case is left to fail loudly: an app still installed whose worker has been cleared away POSTs to the server, which answers 405. A server route would trade a plain error for a silent nothing, and the payload is gone in both — it only ever existed in that request body. The reasoning is in the comment rather than left to be rediscovered.

Translations

No new user-visible strings — the share opens an ordinary composer, so nothing was added to the catalogues.

Verification

npm run typecheck, npm test (1144 web + 161 server, 17 of them new) and npm run build pass; i18n:check unchanged.

Not driven on a device, and this one only exists on Android — iOS does not implement share targets, and the extension driving Chrome is not connected here. The handoff is pinned from the tab's side against a cache shaped exactly as stashShare leaves it, since the worker and the app never see each other and a drift between them is silent.

This wants a real Android test before it can be called done: whether ihasmail appears in the sheet at all, and whether the accept list catches the file types people actually share.

Merged 2026-09-07 as coffey-labs/ihasmail@f42fb014c8

Rebuilt from: git history, session transcript.

#306 let ihasmail hand a file *to* the share sheet. This puts it *in* one: share a photo from the gallery, a link from the browser or a document from a file manager, and ihasmail is one of the places it can go — opening a draft that holds it. ### Why the service worker answers it A share is a POST that navigates, and nothing on this side can answer one directly: the app is a client-side router with no endpoint at that address, and the server behind it would need a route that understood the composer. So the worker intercepts the POST, takes the form body, stashes files and text in its cache, and redirects to the app, which collects them on start. Subject from the shared title; text and link become the body; files are attached and begin uploading. **Nothing is addressed** — a share says what to send, never who to. ### Two decisions worth reviewing **The body goes in above the signature, not through `open()`.** `open()` only fits a signature when it is handed no body at all, so passing the shared text to it silently drops the signature from every message that started as a share — and nothing about the resulting draft looks wrong. There's a test pinning it. **Collected on every start, not on `?share=1`.** A share to a signed-out ihasmail lands on the sign-in page, and `addFiles` needs an account before it can upload, so the payload has to outlive a redirect *and* a login — which a query string does not. The cost is a stash nobody comes back for, so it carries a timestamp and expires after ten minutes. ### Smaller notes - `accept` lists wildcard families **and** explicit MIME types and extensions. A mail client attaches anything, but wildcard support is not in the spec and operating systems differ over which form they match on, so the explicit list is what holds if the families are ignored. This is the part most likely to need adjusting after a real device test. - The service worker's cache name now has **one** home on the app side (`lib/swCache.ts`). It was written out twice, and a drift would not fail — a push verification would simply never complete and a share would arrive at an empty composer. - One case is left to fail loudly: an app still installed whose worker has been cleared away POSTs to the server, which answers 405. A server route would trade a plain error for a silent nothing, and the payload is gone in both — it only ever existed in that request body. The reasoning is in the comment rather than left to be rediscovered. ### Translations **No new user-visible strings** — the share opens an ordinary composer, so nothing was added to the catalogues. ### Verification `npm run typecheck`, `npm test` (1144 web + 161 server, 17 of them new) and `npm run build` pass; `i18n:check` unchanged. Not driven on a device, and this one only exists on Android — iOS does not implement share targets, and the extension driving Chrome is not connected here. The handoff is pinned from the tab's side against a cache shaped exactly as `stashShare` leaves it, since the worker and the app never see each other and a drift between them is silent. **This wants a real Android test before it can be called done**: whether ihasmail appears in the sheet at all, and whether the `accept` list catches the file types people actually share. **Merged** 2026-09-07 as coffey-labs/ihasmail@f42fb014c815 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.