Warn about outside senders, large sends and links that mislead #202

Closed
opened 2026-09-02 05:45:40 +00:00 by jcoffey-dev · 0 comments
Owner

Four warnings, in Privacy & safety, and all of them start switched off.

That is not timidity. A client that begins by interrupting is one people learn to click through, and a warning clicked through without reading costs the same attention and buys nothing. These are for someone who has decided they want them.

The outside-sender warning could not be on by default in any case: it measures against the domains that count as yours, and with nothing configured every message in the mailbox is from outside.

The domain model

Your own identity domains are always internal and are not configuration. An account signed in as [email protected] warning that example.com is external would be absurd, and requiring it to be typed in first is a foot-gun that leaves the feature useless the moment it is switched on. Configured domains are additional.

Matching is on a dot boundary, not endsWith. example.com covers mail.example.com and does not cover notexample.com — which is exactly the domain somebody registers on purpose.

The four

A banner names the sender's domain on a message from outside.

Sending outside names the outside recipients and asks, rather than refusing. "This is going outside" is a rule and not something the sender can check; a list of addresses is. It reads the full identity list rather than the visible one — hiding an identity from the From menu does not make its domain somebody else's.

Sending to a large group asks once the count crosses a threshold you set, which is what catches a reply-all onto a long thread. It counts people rather than headers, so one address in To and nine in Cc is a message to ten.

Opening a link asks when the destination is not trusted, and always when the link's text names one domain while its destination is another — even where that destination is trusted, because being trusted is not the same as being the place the text claimed. On that mismatch the offer to trust the domain is deliberately withheld: what would be trusted is the destination, and the destination is not the thing in question.

Links that are not http or https are left alone — warning about a mailto: is noise, and noise is how a warning stops being read. Both message bodies are covered, because a link in a plain-text mail is linkified by ihasmail and points wherever it likes just as readily as one the sender marked up.

Implementation notes for review

The link click is cancelled and the navigation re-issued after the answer, since there is no way to hold a real navigation open across a dialog. The reopen runs in the continuation of the dialog's own click, which is still the gesture a popup blocker wants to see — but this is the part I would most want a second opinion on.

followLink lives in MessageView and is passed to HtmlBody and TextBody as a prop that is null when the warning is off, so an ordinary link keeps the browser's own handling rather than routing through a guard that always says yes.

Typed domains are normalised on entry — a leading @, a whole address pasted instead of a domain, a pasted URL — because the thing being compared is a hostname, and a list holding "@Example.com " silently matches nothing.

Testing

20 unit tests on the decision logic (the dot-boundary cases, the mismatch-beats-trust rule, prose link text claiming nothing, non-http schemes, threshold boundaries, the identity-domains-are-implicit rule) and 3 more component tests covering the defaults being off, the domain lists staying hidden until their warning is on, and entry normalisation.

Suite green: web 702, server 110, typecheck clean.

Not verified

The dialogs and the banner have not been seen in a browser — browser automation stopped cooperating for me partway through this session (row clicks and context menus), so this is tests-only. The link-warning dialog in particular is worth exercising by hand: the window.open after an await is the fragile part.

Merged 2026-09-01 as coffey-labs/ihasmail@d300107be0

Rebuilt from: git history, session transcript.

Four warnings, in Privacy & safety, and **all of them start switched off**. That is not timidity. A client that begins by interrupting is one people learn to click through, and a warning clicked through without reading costs the same attention and buys nothing. These are for someone who has decided they want them. The outside-sender warning could not be on by default in any case: it measures against the domains that count as yours, and with nothing configured *every* message in the mailbox is from outside. ## The domain model **Your own identity domains are always internal and are not configuration.** An account signed in as `[email protected]` warning that `example.com` is external would be absurd, and requiring it to be typed in first is a foot-gun that leaves the feature useless the moment it is switched on. Configured domains are additional. **Matching is on a dot boundary, not `endsWith`.** `example.com` covers `mail.example.com` and does *not* cover `notexample.com` — which is exactly the domain somebody registers on purpose. ## The four **A banner** names the sender's domain on a message from outside. **Sending outside** names the outside recipients and asks, rather than refusing. "This is going outside" is a rule and not something the sender can check; a list of addresses is. It reads the *full* identity list rather than the visible one — hiding an identity from the From menu does not make its domain somebody else's. **Sending to a large group** asks once the count crosses a threshold you set, which is what catches a reply-all onto a long thread. It counts people rather than headers, so one address in To and nine in Cc is a message to ten. **Opening a link** asks when the destination is not trusted, and *always* when the link's text names one domain while its destination is another — even where that destination is trusted, because being trusted is not the same as being the place the text claimed. On that mismatch the offer to trust the domain is deliberately withheld: what would be trusted is the destination, and the destination is not the thing in question. Links that are not http or https are left alone — warning about a `mailto:` is noise, and noise is how a warning stops being read. Both message bodies are covered, because a link in a plain-text mail is linkified by ihasmail and points wherever it likes just as readily as one the sender marked up. ## Implementation notes for review **The link click is cancelled and the navigation re-issued after the answer**, since there is no way to hold a real navigation open across a dialog. The reopen runs in the continuation of the dialog's own click, which is still the gesture a popup blocker wants to see — but this is the part I would most want a second opinion on. `followLink` lives in `MessageView` and is passed to `HtmlBody` and `TextBody` as a prop that is **null when the warning is off**, so an ordinary link keeps the browser's own handling rather than routing through a guard that always says yes. Typed domains are normalised on entry — a leading `@`, a whole address pasted instead of a domain, a pasted URL — because the thing being compared is a hostname, and a list holding `"@Example.com "` silently matches nothing. ## Testing 20 unit tests on the decision logic (the dot-boundary cases, the mismatch-beats-trust rule, prose link text claiming nothing, non-http schemes, threshold boundaries, the identity-domains-are-implicit rule) and 3 more component tests covering the defaults being off, the domain lists staying hidden until their warning is on, and entry normalisation. Suite green: web 702, server 110, typecheck clean. ## Not verified The dialogs and the banner have not been seen in a browser — browser automation stopped cooperating for me partway through this session (row clicks and context menus), so this is tests-only. The link-warning dialog in particular is worth exercising by hand: the `window.open` after an await is the fragile part. **Merged** 2026-09-01 as coffey-labs/ihasmail@d300107be060 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.