This is from the security review. The PWA's share target is POST <base>/share: the service worker stashes the form, and the app used to open it straight into a composer. That address takes a plain form POST, so any website could make the app open a composer pre-filled with its own text and files. Nothing would be sent without the reader pressing Send, but that's still something to ask about first.
Now:
A confirmation comes first.offerShare (views/ShareOffer.tsx) shows:
the shared title;
the text and link, cut to 160 characters (shareSummary);
up to five file names;
a note that nothing is sent until Send is chosen, and to discard the share if the reader didn't just make it.
Start a message opens the composer exactly as before, with nothing addressed. Discard drops the share. Either way, the stash was already deleted when the share was collected.
Dialog change:ConfirmHost puts a message that isn't a plain string in a <div> instead of a <p>, since the summary has its own blocks.
This adds one tap to a share made from the operating system's share sheet. The page can't tell such a share apart from one a website posted.
Related issues
None.
Translations
Three new strings, translated in all nine catalogs:
"Start a new message with what was shared?"
"Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it."
"Start a message"
Existing terms reused:
the "Discard" button label (already in every catalog);
each catalog's own word for Send;
in Russian and Ukrainian, the body names the "Не сохранять" / "Не зберігати" button.
Conventions followed: formal register in de, fr and nl; full-width ? in ja and zh-Hans; a space before ? in fr.
Counts: catalog keys went from 1,625 to 1,628, and each language from 1,609 to 1,612 translated. The same 16 strings as on main still fall back to English. These translations are AI-written and not reviewed by native speakers, like the rest of the catalogs.
Testing
shareTarget.test.ts: shareSummary combines title, text and link, lists file names, and shortens long text.
New share-offer.test.tsx, rendering the real ConfirmHost: the dialog shows the title, text, link and file names before anything opens; Start a message opens the share; Discard opens nothing.
npm test -w web (1,353 tests), npm run typecheck, npm run build -w web and npm run i18n:check are clean.
Checked in Chrome against the built app served by npm run dev:mock. From a signed-in tab, I submitted a multipart form to /share with a title, text, link and a file, the way a website could:
The confirmation appeared, showing all four, and no composer opened behind it.
Discard closed it with no composer, and the stash was gone.
A second share, then Start a message, opened a composer with the subject, body and attachment, and nothing addressed.
## Summary
This is from the security review. The PWA's share target is `POST <base>/share`: the service worker stashes the form, and the app used to open it straight into a composer. That address takes a plain form POST, so **any website** could make the app open a composer pre-filled with its own text and files. Nothing would be sent without the reader pressing Send, but that's still something to ask about first.
**Now:**
- **A confirmation comes first.** `offerShare` (`views/ShareOffer.tsx`) shows:
- the shared title;
- the text and link, cut to 160 characters (`shareSummary`);
- up to five file names;
- a note that nothing is sent until Send is chosen, and to discard the share if the reader didn't just make it.
- **Start a message** opens the composer exactly as before, with nothing addressed. **Discard** drops the share. Either way, the stash was already deleted when the share was collected.
- **Dialog change:** `ConfirmHost` puts a message that isn't a plain string in a `<div>` instead of a `<p>`, since the summary has its own blocks.
This adds one tap to a share made from the operating system's share sheet. The page can't tell such a share apart from one a website posted.
## Related issues
None.
## Translations
**Three new strings, translated in all nine catalogs:**
- "Start a new message with what was shared?"
- "Something was shared with ihasmail. Nothing is sent until you choose Send. If you didn't just share this, discard it."
- "Start a message"
**Existing terms reused:**
- the "Discard" button label (already in every catalog);
- each catalog's own word for Send;
- in Russian and Ukrainian, the body names the "Не сохранять" / "Не зберігати" button.
**Conventions followed:** formal register in de, fr and nl; full-width `?` in ja and zh-Hans; a space before `?` in fr.
**Counts:** catalog keys went from 1,625 to 1,628, and each language from 1,609 to 1,612 translated. The same 16 strings as on `main` still fall back to English. These translations are AI-written and not reviewed by native speakers, like the rest of the catalogs.
## Testing
- `shareTarget.test.ts`: `shareSummary` combines title, text and link, lists file names, and shortens long text.
- New `share-offer.test.tsx`, rendering the real `ConfirmHost`: the dialog shows the title, text, link and file names before anything opens; **Start a message** opens the share; **Discard** opens nothing.
- `npm test -w web` (1,353 tests), `npm run typecheck`, `npm run build -w web` and `npm run i18n:check` are clean.
- **Checked in Chrome** against the built app served by `npm run dev:mock`. From a signed-in tab, I submitted a multipart form to `/share` with a title, text, link and a file, the way a website could:
- The confirmation appeared, showing all four, and no composer opened behind it.
- **Discard** closed it with no composer, and the stash was gone.
- A second share, then **Start a message**, opened a composer with the subject, body and attachment, and nothing addressed.
**Merged** 2026-09-16 as coffey-labs/ihasmail@5fe89d6e1559
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
This is from the security review. The PWA's share target is
POST <base>/share: the service worker stashes the form, and the app used to open it straight into a composer. That address takes a plain form POST, so any website could make the app open a composer pre-filled with its own text and files. Nothing would be sent without the reader pressing Send, but that's still something to ask about first.Now:
offerShare(views/ShareOffer.tsx) shows:shareSummary);ConfirmHostputs a message that isn't a plain string in a<div>instead of a<p>, since the summary has its own blocks.This adds one tap to a share made from the operating system's share sheet. The page can't tell such a share apart from one a website posted.
Related issues
None.
Translations
Three new strings, translated in all nine catalogs:
Existing terms reused:
Conventions followed: formal register in de, fr and nl; full-width
?in ja and zh-Hans; a space before?in fr.Counts: catalog keys went from 1,625 to 1,628, and each language from 1,609 to 1,612 translated. The same 16 strings as on
mainstill fall back to English. These translations are AI-written and not reviewed by native speakers, like the rest of the catalogs.Testing
shareTarget.test.ts:shareSummarycombines title, text and link, lists file names, and shortens long text.share-offer.test.tsx, rendering the realConfirmHost: the dialog shows the title, text, link and file names before anything opens; Start a message opens the share; Discard opens nothing.npm test -w web(1,353 tests),npm run typecheck,npm run build -w webandnpm run i18n:checkare clean.npm run dev:mock. From a signed-in tab, I submitted a multipart form to/sharewith a title, text, link and a file, the way a website could:Merged 2026-09-16 as coffey-labs/ihasmail@5fe89d6e15
Rebuilt from: git history, session transcript.