Merge branch 'fx/read2' into feat/stalwart-1.0

This commit is contained in:
Linus Rath
2026-10-29 04:44:36 +01:00
19 changed files with 506 additions and 125 deletions
+2 -8
View File
@@ -4,7 +4,7 @@ import { WebView, type WebViewMessageEvent } from 'react-native-webview';
import { useColors } from '../theme/colors';
import type { ThemePalette } from '../theme/tokens';
import { perfEndAfterPaint } from '../lib/perf-marks';
import { buildEditorHtml, MIN_EDITOR_HEIGHT } from '../lib/editor-html';
import { buildEditorHtml, isEditorNavigationAllowed, MIN_EDITOR_HEIGHT } from '../lib/editor-html';
export type RichTextCommand =
| 'bold'
@@ -276,13 +276,7 @@ const RichTextEditor = React.forwardRef<RichTextEditorHandle, Props>(function Ri
androidLayerType={Platform.OS === 'android' ? 'hardware' : undefined}
style={styles.webview}
containerStyle={styles.webviewContainer}
onShouldStartLoadWithRequest={(request) => {
// Allow only the initial about:blank doc + data URIs we emit
// ourselves. Reject `about:*` (e.g. about:srcdoc) and every other
// scheme so a paste-injected anchor can't navigate the editor.
const url = request.url;
return !url || url === 'about:blank' || url.startsWith('data:');
}}
onShouldStartLoadWithRequest={(request) => isEditorNavigationAllowed(request.url)}
/>
</View>
);
+12 -11
View File
@@ -15,10 +15,10 @@ import { SubmissionQuotaError } from '../../api/jmap-result';
import { sendFailureMessage } from '../../lib/send-failure-message';
import {
isValidUnsubscribeUrl, parseUnsubscribeMailto, isOneClickUnsubscribe, unsubscribeConfirmMessage,
unsubscribeSendFolders,
} from '../../lib/unsubscribe';
import type { ListHeaders } from '../../lib/email-headers';
import { findReceivingIdentity } from '../../lib/email-headers';
import { mailboxesOfAccount } from '../../lib/mailbox-tree';
const DISMISSED_KEY = 'webmail:unsubscribe-dismissed:v1';
const dismissed = new Set<string>();
@@ -47,16 +47,18 @@ interface Props {
list: ListHeaders;
/** Dedupe key for dismissals (bare Message-ID, else the JMAP id). */
messageKey: string;
/** The message's JMAP account. A mailto unsubscribe still goes out from the user's own. */
jmapAccountId?: string;
}
/**
* List-Unsubscribe (RFC 2369 / RFC 8058) banner. An https one-click URL is
* POSTed directly; a plain http(s) URL opens in the in-app browser; a
* mailto: URL is sent through the account itself (handing it to the OS mail
* app goes nowhere for most users). Dismissals are remembered per Message-ID.
* mailto: URL is sent through the user's own account (handing it to the OS
* mail app goes nowhere for most users). Dismissals are remembered per
* Message-ID.
*/
export function UnsubscribeBanner({ email, list, messageKey, jmapAccountId }: Props) {
export function UnsubscribeBanner({ email, list, messageKey }: Props) {
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t);
@@ -110,13 +112,12 @@ export function UnsubscribeBanner({ email, list, messageKey, jmapAccountId }: Pr
} else {
const fields = mailtoFields;
if (!fields) throw new Error('invalid mailto');
// Sent from the user's own account with one of its identities, also
// for a message in a shared account (see unsubscribeSendFolders).
const identity = findReceivingIdentity(identities, email) ?? identities[0];
if (!identity) throw new Error(t('email_viewer.unsubscribe_banner.no_identity', 'No sending identity available'));
// Sent/Drafts of the account the mail is submitted from: the message's.
const scoped = mailboxesOfAccount(mailboxes, jmapAccountId);
const sent = scoped.find((m) => m.role === 'sent');
const drafts = scoped.find((m) => m.role === 'drafts');
if (!sent) throw new Error(t('email_composer.no_sent_folder', 'No Sent folder found'));
const { sentId, draftsId } = unsubscribeSendFolders(mailboxes);
if (!sentId) throw new Error(t('email_composer.no_sent_folder', 'No Sent folder found'));
await sendEmail(
{
from: [{ name: identity.name, email: identity.email }],
@@ -125,9 +126,9 @@ export function UnsubscribeBanner({ email, list, messageKey, jmapAccountId }: Pr
textBody: fields.body ?? '',
},
identity.id,
sent.originalId ?? sent.id,
sentId,
undefined,
{ draftsMailboxId: drafts ? (drafts.originalId ?? drafts.id) : undefined, accountId: jmapAccountId },
{ draftsMailboxId: draftsId },
);
void pruneEmailSubmissions();
}