mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: block remote content when the webmail allows no third-party traffic and hide address matching without custom From
This commit is contained in:
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user