Warn about outside senders, large sends and links that mislead
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. 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. 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 making it be typed in first is a foot-gun that leaves the feature useless the moment it is enabled. Configured domains are additional, and cover their subdomains -- matched on a dot boundary, so example.com covers mail.example.com and not 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, since 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, 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 withheld: what would be trusted is the destination, and the destination is not the thing in question. Anything that is not http or https is left alone, since warning about a mailto: is noise and noise is how a warning stops being read. Both bodies are covered, because a link in a plain-text mail is linkified by us and points wherever it likes just as readily as one the sender marked up. The 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.
This commit is contained in:
@@ -10,7 +10,8 @@ import { MenuItem, MenuSep, MenuTitle, Popover, useMenu } from "@/ui/popover";
|
||||
import { confirmDialog, promptDialog } from "@/ui/dialog";
|
||||
import { formatSize, formatRelative } from "@/lib/format";
|
||||
import { htmlToText, textToHtml } from "@/lib/text";
|
||||
import { isValidEmail } from "@/lib/address";
|
||||
import { isValidEmail, uniqueAddresses } from "@/lib/address";
|
||||
import { crossesRecipientThreshold, externalRecipients, internalDomains } from "@/lib/warnings";
|
||||
import { attachmentIcon } from "../mail/MessageView";
|
||||
import { FilePicker } from "./FilePicker";
|
||||
import { RecipientPicker, type Field } from "./RecipientPicker";
|
||||
@@ -106,6 +107,37 @@ export function Composer({ draft }: { draft: Draft }) {
|
||||
const ok = await confirmDialog({ title: translate("Did you forget the attachment?"), message: translate("Your message mentions an attachment, but nothing is attached."), confirmLabel: translate("Send anyway") });
|
||||
if (!ok) return;
|
||||
}
|
||||
/*
|
||||
* The two send-time warnings, in this order because they answer different
|
||||
* questions and a message can trip both: who it is going to, then how many
|
||||
* of them. Both name the specific thing rather than warning in general --
|
||||
* "this is going outside" is a rule, "this is going to [email protected]" is
|
||||
* something the sender can check.
|
||||
*/
|
||||
if (settings.externalRecipientConfirm) {
|
||||
// allIdentities, not the visible subset: hiding an identity from the
|
||||
// picker is about the From menu, and does not make its domain
|
||||
// somebody else's.
|
||||
const outside = externalRecipients(all, internalDomains(allIdentities.map((i) => i.email), settings.internalDomains));
|
||||
if (outside.length) {
|
||||
const names = outside.slice(0, 5).map((a) => a.email).join(", ");
|
||||
const rest = outside.length > 5 ? translate(" and {count} more", { count: String(outside.length - 5) }) : "";
|
||||
const ok = await confirmDialog({
|
||||
title: translate("Send outside your organisation?"),
|
||||
message: translate("This goes to {recipients}{rest}.", { recipients: names, rest }),
|
||||
confirmLabel: translate("Send anyway"),
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
}
|
||||
if (crossesRecipientThreshold(uniqueAddresses(all).length, settings.replyAllThreshold)) {
|
||||
const ok = await confirmDialog({
|
||||
title: translate("Send to {count} people?", { count: String(uniqueAddresses(all).length) }),
|
||||
message: translate("Everyone addressed will receive this."),
|
||||
confirmLabel: translate("Send anyway"),
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
await send(key);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user