fix: block remote content when the webmail allows no third-party traffic and hide address matching without custom From

This commit is contained in:
Linus Rath
2026-10-31 16:22:11 +01:00
parent c66e6209ed
commit d254d7b5ab
9 changed files with 118 additions and 22 deletions
+13 -7
View File
@@ -20,6 +20,7 @@ import { buildMailTextSizeScript, mailTextFactor } from '../lib/mail-reading';
import { buildPauseAnimatedImagesScript, withReducedMotion } from '../lib/mail-motion'; import { buildPauseAnimatedImagesScript, withReducedMotion } from '../lib/mail-motion';
import { useHasContacts } from '../lib/capabilities'; import { useHasContacts } from '../lib/capabilities';
import { useSettingsStore } from '../stores/settings-store'; import { useSettingsStore } from '../stores/settings-store';
import { useNoThirdPartyTraffic } from '../stores/policy-store';
import { useContactsStore } from '../stores/contacts-store'; import { useContactsStore } from '../stores/contacts-store';
import { useLocaleStore } from '../stores/locale-store'; import { useLocaleStore } from '../stores/locale-store';
import { spacing, typography, type ThemePalette } from '../theme/tokens'; import { spacing, typography, type ThemePalette } from '../theme/tokens';
@@ -437,6 +438,9 @@ export default function EmailBodyView({
const t = useLocaleStore((s) => s.t); const t = useLocaleStore((s) => s.t);
const navigation = useNavigation<Nav>(); const navigation = useNavigation<Nav>();
const externalContentPolicy = useSettingsStore((s) => s.externalContentPolicy); const externalContentPolicy = useSettingsStore((s) => s.externalContentPolicy);
// No third-party traffic (webmail #903): remote content never loads,
// whatever the policy or trust says, and nothing offers to load it.
const remoteBlockedByOrg = useNoThirdPartyTraffic();
const isSenderTrusted = useSettingsStore((s) => s.isSenderTrusted); const isSenderTrusted = useSettingsStore((s) => s.isSenderTrusted);
const addTrustedSender = useSettingsStore((s) => s.addTrustedSender); const addTrustedSender = useSettingsStore((s) => s.addTrustedSender);
const trustedSendersAddressBook = useSettingsStore((s) => s.trustedSendersAddressBook); const trustedSendersAddressBook = useSettingsStore((s) => s.trustedSendersAddressBook);
@@ -523,10 +527,10 @@ export default function EmailBodyView({
// trusted, regardless of what the detection pass finds - the pass only // trusted, regardless of what the detection pass finds - the pass only
// drives the banner and the placeholder swap. // drives the banner and the placeholder swap.
const shouldBlock = const shouldBlock =
!!rawHtml && !!rawHtml && (
!trusted && remoteBlockedByOrg ||
!allowOnce && (!trusted && !allowOnce && externalContentPolicy !== 'allow')
externalContentPolicy !== 'allow'; );
// The WebView's height: what the page reported, else the height this body // The WebView's height: what the page reported, else the height this body
// had the last time it was shown, else (see `fill`) an estimate. Never a // had the last time it was shown, else (see `fill`) an estimate. Never a
@@ -736,9 +740,11 @@ export default function EmailBodyView({
{showBanner && ( {showBanner && (
<View style={styles.banner}> <View style={styles.banner}>
<Text style={styles.bannerText}> <Text style={styles.bannerText}>
{t('email_viewer.external_content_warning', 'Images and external content have been blocked')} {remoteBlockedByOrg
? t('third_party.remote_content_blocked', "Your organisation blocks content from other servers, so remote pictures in this message aren't shown.")
: t('email_viewer.external_content_warning', 'Images and external content have been blocked')}
</Text> </Text>
<View style={styles.bannerActions}> {!remoteBlockedByOrg && <View style={styles.bannerActions}>
{externalContentPolicy === 'ask' && ( {externalContentPolicy === 'ask' && (
<Pressable style={styles.bannerButton} onPress={onLoadImages} hitSlop={8}> <Pressable style={styles.bannerButton} onPress={onLoadImages} hitSlop={8}>
<ImageIcon size={14} color={c.textSecondary} /> <ImageIcon size={14} color={c.textSecondary} />
@@ -751,7 +757,7 @@ export default function EmailBodyView({
<Text style={styles.bannerButtonText}>{t('email_viewer.trust_sender', 'Always trust this sender')}</Text> <Text style={styles.bannerButtonText}>{t('email_viewer.trust_sender', 'Always trust this sender')}</Text>
</Pressable> </Pressable>
) : null} ) : null}
</View> </View>}
</View> </View>
)} )}
{showTruncated && ( {showTruncated && (
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { webmailLinksFromConfig, helpLinks } from '../../lib/org-policy';
// The organisation's customFromEnabled gate (webmail composing-settings.tsx,
// email-composer.tsx) and the webmail's "no third-party traffic" mode (#903,
// hooks/use-third-party-mode.ts, reading-settings.tsx, email-viewer.tsx). No
// React Native renderer in the test env, so this checks the wiring.
const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8');
describe('customFromEnabled', () => {
it('shows Received Address Matching only when custom From is allowed', () => {
const src = read('components/settings/ComposingSettings.tsx');
expect(src).toMatch(/const customFromEnabled = usePolicyFeature\('customFromEnabled'\);/);
expect(src).toMatch(/\{autoSelectReplyIdentity && customFromEnabled && \(/);
});
it('matches only exact identities in the composer when custom From is off', () => {
expect(read('screens/ComposeScreen.tsx')).toMatch(/matchMode: customFromEnabled \? replyIdentityMatch : 'exact',/);
});
});
describe('no third-party traffic', () => {
it('reads the mode from the webmail config, as a boolean or an env-style string', () => {
expect(webmailLinksFromConfig({ noThirdPartyTraffic: true }).noThirdPartyTraffic).toBe(true);
expect(webmailLinksFromConfig({ noThirdPartyTraffic: 'Yes' }).noThirdPartyTraffic).toBe(true);
expect(webmailLinksFromConfig({ noThirdPartyTraffic: false }).noThirdPartyTraffic).toBeUndefined();
expect(webmailLinksFromConfig({ noThirdPartyTraffic: 'no' }).noThirdPartyTraffic).toBeUndefined();
// Not a help link.
expect(helpLinks(webmailLinksFromConfig({ noThirdPartyTraffic: true }))).toEqual([]);
});
it('shows External Content as the organisation\'s, set to block', () => {
const src = read('components/settings/ContentSendersSettings.tsx');
expect(src).toMatch(/const remoteBlockedByOrg = useNoThirdPartyTraffic\(\);/);
expect(src).toMatch(/locked=\{externalContentLocked \|\| remoteBlockedByOrg\}/);
expect(src).toMatch(/value=\{remoteBlockedByOrg \? 'block' : externalContentPolicy\}/);
});
it('never loads remote content in the reader and offers nothing to load it', () => {
const src = read('components/EmailBodyView.tsx');
expect(src).toMatch(/!!rawHtml && \(\s+remoteBlockedByOrg \|\|\s+\(!trusted && !allowOnce && externalContentPolicy !== 'allow'\)\s+\)/);
expect(src).toMatch(/\{!remoteBlockedByOrg && <View style=\{styles\.bannerActions\}>/);
expect(src).toMatch(/t\('third_party\.remote_content_blocked'/);
});
it('keeps remote images blocked in booking cards and quoted originals', () => {
expect(read('components/email/structured-mail/StructuredMailBanner.tsx')).toMatch(/\(\) => !remoteBlockedByOrg && \(policy === 'allow' \|\|/);
const compose = read('screens/ComposeScreen.tsx');
expect(compose).toMatch(/externalContentPolicy: remoteBlockedByOrg \? 'block' : settings\.externalContentPolicy,/);
expect(compose).toMatch(/senderTrusted: !remoteBlockedByOrg && !!sender/);
});
});
@@ -8,6 +8,7 @@ import { isSenderContentTrusted, isTrustedSendersSyncOn } from '../../../lib/tru
import { useHasContacts } from '../../../lib/capabilities'; import { useHasContacts } from '../../../lib/capabilities';
import { useContactsStore } from '../../../stores/contacts-store'; import { useContactsStore } from '../../../stores/contacts-store';
import { useSettingsStore } from '../../../stores/settings-store'; import { useSettingsStore } from '../../../stores/settings-store';
import { useNoThirdPartyTraffic } from '../../../stores/policy-store';
import { BookingCalendarActions } from './booking-calendar-actions'; import { BookingCalendarActions } from './booking-calendar-actions';
import { StructuredMailCards } from './structured-mail-cards'; import { StructuredMailCards } from './structured-mail-cards';
import { useStructuredMail } from './use-structured-mail'; import { useStructuredMail } from './use-structured-mail';
@@ -28,6 +29,8 @@ interface Props {
export function StructuredMailBanner({ email, jmapAccountId }: Props) { export function StructuredMailBanner({ email, jmapAccountId }: Props) {
const cards = useStructuredMail(email, jmapAccountId); const cards = useStructuredMail(email, jmapAccountId);
const policy = useSettingsStore((s) => s.externalContentPolicy); const policy = useSettingsStore((s) => s.externalContentPolicy);
// No third-party traffic (webmail #903): remote images never load.
const remoteBlockedByOrg = useNoThirdPartyTraffic();
const trustedSenders = useSettingsStore((s) => s.trustedSenders); const trustedSenders = useSettingsStore((s) => s.trustedSenders);
const isSenderTrusted = useSettingsStore((s) => s.isSenderTrusted); const isSenderTrusted = useSettingsStore((s) => s.isSenderTrusted);
const trustedSendersAddressBook = useSettingsStore((s) => s.trustedSendersAddressBook); const trustedSendersAddressBook = useSettingsStore((s) => s.trustedSendersAddressBook);
@@ -35,13 +38,13 @@ export function StructuredMailBanner({ email, jmapAccountId }: Props) {
const hasContacts = useHasContacts(); const hasContacts = useHasContacts();
const sender = email.from?.[0]?.email; const sender = email.from?.[0]?.email;
const allowRemoteImages = React.useMemo( const allowRemoteImages = React.useMemo(
() => policy === 'allow' || isSenderContentTrusted(sender, { () => !remoteBlockedByOrg && (policy === 'allow' || isSenderContentTrusted(sender, {
isLocallyTrusted: isSenderTrusted, isLocallyTrusted: isSenderTrusted,
syncEnabled: isTrustedSendersSyncOn(trustedSendersAddressBook, hasContacts), syncEnabled: isTrustedSendersSyncOn(trustedSendersAddressBook, hasContacts),
trustedBookEmails, trustedBookEmails,
}), })),
// trustedSenders changes what isSenderTrusted answers. // trustedSenders changes what isSenderTrusted answers.
[policy, sender, isSenderTrusted, trustedSenders, trustedSendersAddressBook, hasContacts, trustedBookEmails], // eslint-disable-line react-hooks/exhaustive-deps [remoteBlockedByOrg, policy, sender, isSenderTrusted, trustedSenders, trustedSendersAddressBook, hasContacts, trustedBookEmails], // eslint-disable-line react-hooks/exhaustive-deps
); );
const calendarActions = React.useCallback( const calendarActions = React.useCallback(
(booking: Booking) => <BookingCalendarActions booking={booking} email={email} />, (booking: Booking) => <BookingCalendarActions booking={booking} email={email} />,
@@ -7,7 +7,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
import { useColors } from '../../theme/colors'; import { useColors } from '../../theme/colors';
import { useSettingsStore, type MentionAddsRecipientTo, type ReplyIdentityMatch, type SignaturePosition } from '../../stores/settings-store'; import { useSettingsStore, type MentionAddsRecipientTo, type ReplyIdentityMatch, type SignaturePosition } from '../../stores/settings-store';
import { useLocaleStore } from '../../stores/locale-store'; import { useLocaleStore } from '../../stores/locale-store';
import { useSettingLocked } from '../../stores/policy-store'; import { usePolicyFeature, useSettingLocked } from '../../stores/policy-store';
import { SUPPORTED_SUB_ADDRESS_DELIMITERS } from '../../lib/sub-addressing'; import { SUPPORTED_SUB_ADDRESS_DELIMITERS } from '../../lib/sub-addressing';
import { RADIO_GROUP_A11Y, radioOptionA11y } from '../radio-a11y'; import { RADIO_GROUP_A11Y, radioOptionA11y } from '../radio-a11y';
@@ -29,6 +29,9 @@ export function ComposingSettings() {
const autoSelectReplyIdentityLocked = useSettingLocked('autoSelectReplyIdentity'); const autoSelectReplyIdentityLocked = useSettingLocked('autoSelectReplyIdentity');
const replyIdentityMatchLocked = useSettingLocked('replyIdentityMatch'); const replyIdentityMatchLocked = useSettingLocked('replyIdentityMatch');
const plainTextModeLocked = useSettingLocked('plainTextMode'); const plainTextModeLocked = useSettingLocked('plainTextMode');
// Catch-all matching only rewrites From, which the organisation can turn
// off (webmail composing-settings.tsx customFromEnabled).
const customFromEnabled = usePolicyFeature('customFromEnabled');
const signaturePosition = useSettingsStore((s) => s.signaturePosition); const signaturePosition = useSettingsStore((s) => s.signaturePosition);
const signatureSeparatorEnabled = useSettingsStore((s) => s.signatureSeparatorEnabled); const signatureSeparatorEnabled = useSettingsStore((s) => s.signatureSeparatorEnabled);
const requestReadReceiptDefault = useSettingsStore((s) => s.requestReadReceiptDefault); const requestReadReceiptDefault = useSettingsStore((s) => s.requestReadReceiptDefault);
@@ -135,7 +138,7 @@ export function ComposingSettings() {
<ToggleSwitch checked={autoSelectReplyIdentity} onChange={setAutoSelectReplyIdentity} /> <ToggleSwitch checked={autoSelectReplyIdentity} onChange={setAutoSelectReplyIdentity} />
</SettingItem> </SettingItem>
{autoSelectReplyIdentity && ( {autoSelectReplyIdentity && customFromEnabled && (
<View style={styles.subBlock}> <View style={styles.subBlock}>
<Text style={styles.subLabel}>{t('settings.email_behavior.reply_identity_match.label', 'Received Address Matching')}</Text> <Text style={styles.subLabel}>{t('settings.email_behavior.reply_identity_match.label', 'Received Address Matching')}</Text>
<Text style={styles.subDescription}> <Text style={styles.subDescription}>
@@ -13,7 +13,7 @@ import { useHasContacts } from '../../lib/capabilities';
import { isTrustedSendersSyncOn } from '../../lib/trusted-senders'; import { isTrustedSendersSyncOn } from '../../lib/trusted-senders';
import { removeTrustedSenderWithUndo } from '../../lib/settings-undo'; import { removeTrustedSenderWithUndo } from '../../lib/settings-undo';
import { useLocaleStore } from '../../stores/locale-store'; import { useLocaleStore } from '../../stores/locale-store';
import { usePolicyFeature, useSettingHidden, useSettingLocked } from '../../stores/policy-store'; import { useNoThirdPartyTraffic, usePolicyFeature, useSettingHidden, useSettingLocked } from '../../stores/policy-store';
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations'; import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
import { useSheetBottomInset } from '../../lib/sheet-bottom-inset'; import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
import { ToastHost } from '../ToastHost'; import { ToastHost } from '../ToastHost';
@@ -38,6 +38,9 @@ export function ContentSendersSettings() {
const externalContentHidden = useSettingHidden('externalContentPolicy'); const externalContentHidden = useSettingHidden('externalContentPolicy');
const externalContentLocked = useSettingLocked('externalContentPolicy'); const externalContentLocked = useSettingLocked('externalContentPolicy');
const showExternalContent = externalContentAllowed && !externalContentHidden; const showExternalContent = externalContentAllowed && !externalContentHidden;
// No third-party traffic (webmail #903) blocks remote content outright:
// the setting shows as the organisation's, set to block.
const remoteBlockedByOrg = useNoThirdPartyTraffic();
const emailAlwaysLightMode = useSettingsStore((s) => s.emailAlwaysLightMode); const emailAlwaysLightMode = useSettingsStore((s) => s.emailAlwaysLightMode);
const setEmailAlwaysLightMode = useSettingsStore((s) => s.setEmailAlwaysLightMode); const setEmailAlwaysLightMode = useSettingsStore((s) => s.setEmailAlwaysLightMode);
const trustedSenders = useSettingsStore((s) => s.trustedSenders); const trustedSenders = useSettingsStore((s) => s.trustedSenders);
@@ -145,10 +148,10 @@ export function ContentSendersSettings() {
<SettingItem <SettingItem
label={t('settings.email_behavior.external_content.label', 'External Content')} label={t('settings.email_behavior.external_content.label', 'External Content')}
description={t('settings.email_behavior.external_content.description', 'How to handle images and external content')} description={t('settings.email_behavior.external_content.description', 'How to handle images and external content')}
locked={externalContentLocked} locked={externalContentLocked || remoteBlockedByOrg}
> >
<RadioGroup <RadioGroup
value={externalContentPolicy} value={remoteBlockedByOrg ? 'block' : externalContentPolicy}
onChange={(v) => setExternalContentPolicy(v as ExternalContentPolicy)} onChange={(v) => setExternalContentPolicy(v as ExternalContentPolicy)}
options={[ options={[
{ value: 'ask', label: t('settings.email_behavior.external_content.ask_mobile', 'Ask') }, { value: 'ask', label: t('settings.email_behavior.external_content.ask_mobile', 'Ask') },
@@ -40,7 +40,7 @@ describe('organisation policy on the settings pages', () => {
expect(page).toContain("const externalContentAllowed = usePolicyFeature('externalContentEnabled');"); expect(page).toContain("const externalContentAllowed = usePolicyFeature('externalContentEnabled');");
expect(page).toContain("const externalContentHidden = useSettingHidden('externalContentPolicy');"); expect(page).toContain("const externalContentHidden = useSettingHidden('externalContentPolicy');");
expect(page).toContain('const showExternalContent = externalContentAllowed && !externalContentHidden;'); expect(page).toContain('const showExternalContent = externalContentAllowed && !externalContentHidden;');
expect(page).toMatch(/\{showExternalContent && \(\s*<SettingItem\s+label=\{t\('settings\.email_behavior\.external_content\.label'[\s\S]*?locked=\{externalContentLocked\}/); expect(page).toMatch(/\{showExternalContent && \(\s*<SettingItem\s+label=\{t\('settings\.email_behavior\.external_content\.label'[\s\S]*?locked=\{externalContentLocked(?: \|\| remoteBlockedByOrg)?\}/);
expect(page).toContain("const externalContentLocked = useSettingLocked('externalContentPolicy');"); expect(page).toContain("const externalContentLocked = useSettingLocked('externalContentPolicy');");
}); });
+14 -1
View File
@@ -480,10 +480,18 @@ export interface WebmailLinks {
signLanguageUrl?: string; signLanguageUrl?: string;
/** The config's company name (`loginCompanyName`), for the organisation signature's {company}. */ /** The config's company name (`loginCompanyName`), for the organisation signature's {company}. */
companyName?: string; companyName?: string;
/**
* The webmail's "no third-party traffic" mode (#903, config
* `noThirdPartyTraffic`): remote content in mail never loads.
*/
noThirdPartyTraffic?: boolean;
} }
/** The accessibility and help links, in the webmail's fixed order (lib/help-links.ts). */ /** The accessibility and help links, in the webmail's fixed order (lib/help-links.ts). */
export const HELP_LINK_ORDER: ReadonlyArray<[HelpLinkId, keyof WebmailLinks]> = [ /** The keys of WebmailLinks that hold a link. */
export type WebmailLinkKey = Exclude<keyof WebmailLinks, 'companyName' | 'noThirdPartyTraffic'>;
export const HELP_LINK_ORDER: ReadonlyArray<[HelpLinkId, WebmailLinkKey]> = [
['statement', 'accessibilityStatementUrl'], ['statement', 'accessibilityStatementUrl'],
['feedback', 'accessibilityFeedbackUrl'], ['feedback', 'accessibilityFeedbackUrl'],
['help', 'helpUrl'], ['help', 'helpUrl'],
@@ -506,6 +514,11 @@ export function webmailLinksFromConfig(raw: unknown): WebmailLinks {
if (typeof raw.loginCompanyName === 'string' && raw.loginCompanyName.trim()) { if (typeof raw.loginCompanyName === 'string' && raw.loginCompanyName.trim()) {
out.companyName = raw.loginCompanyName.trim().slice(0, 200); out.companyName = raw.loginCompanyName.trim().slice(0, 200);
} }
// A boolean, or a string as in env (webmail lib/third-party-mode.ts parseNoThirdPartyValue).
const noThirdParty = raw.noThirdPartyTraffic;
if (noThirdParty === true || (typeof noThirdParty === 'string' && /^(true|1|yes|on)$/i.test(noThirdParty.trim()))) {
out.noThirdPartyTraffic = true;
}
return out; return out;
} }
+9 -5
View File
@@ -35,7 +35,7 @@ import { useSheetBottomInset } from '../lib/sheet-bottom-inset';
import { createAfterDismissQueue } from '../lib/after-modal-dismiss'; import { createAfterDismissQueue } from '../lib/after-modal-dismiss';
import { useLocaleStore } from '../stores/locale-store'; import { useLocaleStore } from '../stores/locale-store';
import { useSettingsStore } from '../stores/settings-store'; import { useSettingsStore } from '../stores/settings-store';
import { usePolicyFeature, usePolicyStore } from '../stores/policy-store'; import { useNoThirdPartyTraffic, usePolicyFeature, usePolicyStore } from '../stores/policy-store';
import { effectiveSignatureIdentity, finalizeOrgOutgoing, orgSignatureSetup, htmlCarriesDisclaimer, textCarriesDisclaimer } from '../lib/org-signature'; import { effectiveSignatureIdentity, finalizeOrgOutgoing, orgSignatureSetup, htmlCarriesDisclaimer, textCarriesDisclaimer } from '../lib/org-signature';
import { useHasContacts } from '../lib/capabilities'; import { useHasContacts } from '../lib/capabilities';
import { isTrustedSendersSyncOn } from '../lib/trusted-senders'; import { isTrustedSendersSyncOn } from '../lib/trusted-senders';
@@ -416,6 +416,9 @@ export default function ComposeScreen({ route, navigation }: Props) {
const autoSelectReplyIdentity = useSettingsStore((s) => s.autoSelectReplyIdentity); const autoSelectReplyIdentity = useSettingsStore((s) => s.autoSelectReplyIdentity);
const replyIdentityMatch = useSettingsStore((s) => s.replyIdentityMatch); const replyIdentityMatch = useSettingsStore((s) => s.replyIdentityMatch);
const customFromEnabled = usePolicyFeature('customFromEnabled');
// No third-party traffic (webmail #903): a quoted original's remote images stay blocked.
const remoteBlockedByOrg = useNoThirdPartyTraffic();
// Per-message format (#1022): seeded from the "plain text only" setting, // Per-message format (#1022): seeded from the "plain text only" setting,
// or the format a reopened draft was written in, and switchable from the // or the format a reopened draft was written in, and switchable from the
// toolbar for just this message. // toolbar for just this message.
@@ -518,8 +521,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
return shouldBlockEditorRemoteImages({ return shouldBlockEditorRemoteImages({
seedHtml: draft ? draft.htmlBody : replyTo?.htmlBody, seedHtml: draft ? draft.htmlBody : replyTo?.htmlBody,
isDraft: !!draft, isDraft: !!draft,
externalContentPolicy: settings.externalContentPolicy, externalContentPolicy: remoteBlockedByOrg ? 'block' : settings.externalContentPolicy,
senderTrusted: !!sender && ( senderTrusted: !remoteBlockedByOrg && !!sender && (
settings.isSenderTrusted(sender) settings.isSenderTrusted(sender)
|| useContactsStore.getState().trustedSenderEmails.includes(sender) || useContactsStore.getState().trustedSenderEmails.includes(sender)
), ),
@@ -972,7 +975,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
const resolved = resolveReplyIdentity(identities, replyTo, { const resolved = resolveReplyIdentity(identities, replyTo, {
ownEmails, ownEmails,
catchAll: autoSelectReplyIdentity && mode !== 'forward', catchAll: autoSelectReplyIdentity && mode !== 'forward',
matchMode: replyIdentityMatch, // Without the organisation's custom From, only an exact identity (webmail email-composer.tsx).
matchMode: customFromEnabled ? replyIdentityMatch : 'exact',
}); });
if (resolved) { if (resolved) {
setSelectedIdentityId(resolved.identityId); setSelectedIdentityId(resolved.identityId);
@@ -984,7 +988,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
} }
setSelectedIdentityId(defaultIdentity.id); setSelectedIdentityId(defaultIdentity.id);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [identities, autoSelectReplyIdentity, replyIdentityMatch, replyTo, draft, selectedIdentityId, preferredIdentityIds]); }, [identities, autoSelectReplyIdentity, replyIdentityMatch, customFromEnabled, replyTo, draft, selectedIdentityId, preferredIdentityIds]);
const primaryIdentity = React.useMemo(() => { const primaryIdentity = React.useMemo(() => {
if (identities.length === 0) return null; if (identities.length === 0) return null;
+9
View File
@@ -267,6 +267,15 @@ export function useSettingLocked(key: string): boolean {
return usePolicyStore((s) => isLockedSetting(s.rules, key)); return usePolicyStore((s) => isLockedSetting(s.rules, key));
} }
/**
* Whether the active account's webmail runs without third-party traffic
* (webmail hooks/use-third-party-mode.ts): remote content in mail never
* loads, whatever the setting or a trusted sender says.
*/
export function useNoThirdPartyTraffic(): boolean {
return usePolicyStore((s) => s.links.noThirdPartyTraffic === true);
}
/** Whether the active account's policy hides a setting (by the app's key). */ /** Whether the active account's policy hides a setting (by the app's key). */
export function useSettingHidden(key: string): boolean { export function useSettingHidden(key: string): boolean {
return usePolicyStore((s) => s.rules.hidden.includes(key)); return usePolicyStore((s) => s.rules.hidden.includes(key));