From 98f814fcedf417bc3b628f27acd518148932b98a Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 4 Nov 2026 12:42:30 +0100
Subject: [PATCH] fix: open a mail link from another program from the default
account with no question, and drop "protocol link" from the account picker
---
.../__tests__/compose-plugin-hooks.test.tsx | 24 +++++++++++++++++
components/mail/mail-app.tsx | 26 +++++++++----------
.../protocol/protocol-account-picker.tsx | 2 +-
lib/protocol-handlers/session.ts | 11 ++++++++
locales/ar/common.json | 1 -
locales/ca/common.json | 1 -
locales/cs/common.json | 1 -
locales/da/common.json | 1 -
locales/de/common.json | 1 -
locales/en/common.json | 1 -
locales/es/common.json | 1 -
locales/et/common.json | 1 -
locales/fa/common.json | 1 -
locales/fr/common.json | 1 -
locales/he/common.json | 1 -
locales/hu/common.json | 1 -
locales/it/common.json | 1 -
locales/ja/common.json | 1 -
locales/ko/common.json | 1 -
locales/lv/common.json | 1 -
locales/mn/common.json | 1 -
locales/nb/common.json | 1 -
locales/nl/common.json | 1 -
locales/pl/common.json | 1 -
locales/pt/common.json | 1 -
locales/ro/common.json | 1 -
locales/ru/common.json | 1 -
locales/sk/common.json | 1 -
locales/th/common.json | 1 -
locales/tr/common.json | 1 -
locales/uk/common.json | 1 -
locales/vi/common.json | 1 -
locales/zh-TW/common.json | 1 -
locales/zh/common.json | 1 -
34 files changed, 48 insertions(+), 45 deletions(-)
diff --git a/components/mail/__tests__/compose-plugin-hooks.test.tsx b/components/mail/__tests__/compose-plugin-hooks.test.tsx
index 8e0c18506..8645e3d3e 100644
--- a/components/mail/__tests__/compose-plugin-hooks.test.tsx
+++ b/components/mail/__tests__/compose-plugin-hooks.test.tsx
@@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mailbox } from '@/lib/jmap/types';
import { useAuthStore } from '@/stores/auth-store';
+import { useAccountStore } from '@/stores/account-store';
import { useEmailStore } from '@/stores/email-store';
import { useSettingsStore } from '@/stores/settings-store';
import { usePluginStore } from '@/stores/plugin-store';
@@ -222,6 +223,29 @@ describe('a mail link from another program in the desktop app', () => {
expect(screen.queryByText('stub composer')).not.toBeInTheDocument();
});
+ it('with two accounts it opens from the default one, in its own window, with no question', async () => {
+ const client = useAuthStore.getState().client;
+ useAuthStore.setState({ getAllConnectedClients: () => new Map([['login', client], ['second', client]]) as never });
+ useAccountStore.setState({
+ accounts: [
+ { id: 'login', email: 'me@example.com', isConnected: true, isDefault: false },
+ { id: 'second', email: 'work@example.com', isConnected: true, isDefault: true },
+ ],
+ defaultAccountId: 'second',
+ } as never);
+ try {
+ render();
+ await fromOs();
+ await waitFor(() => expect(desktopWindows.handOver).toHaveBeenCalledTimes(1));
+ expect(desktopWindows.open).toHaveBeenCalledWith('second');
+ expect(screen.queryByText('select_account_title')).not.toBeInTheDocument();
+ // The main window keeps the account it shows.
+ expect(useAuthStore.getState().activeAccountId).toBe('login');
+ } finally {
+ useAccountStore.setState({ accounts: [], defaultAccountId: null } as never);
+ }
+ });
+
it('a link inside the app still opens here', async () => {
render();
act(() => { requestInternalMailto('mailto:bob@example.com?subject=Hi'); });
diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx
index 8fd320a02..ca0f59b53 100644
--- a/components/mail/mail-app.tsx
+++ b/components/mail/mail-app.tsx
@@ -156,7 +156,7 @@ import { mailTitleContext } from "@/lib/tab-title";
import { usePluginStore } from "@/stores/plugin-store";
import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot";
import { canDownload } from "@/lib/device-policy";
-import { consumePendingMailto, subscribeToPendingMailto } from "@/lib/protocol-handlers/session";
+import { consumePendingMailto, mailLinkAccountId, subscribeToPendingMailto } from "@/lib/protocol-handlers/session";
import { setNotificationReplyOpener } from "@/lib/pwa-notify/page-actions";
import { setMailLinkOpener } from "@/lib/desktop/mail-links";
import { requestTabReply } from "@/lib/pwa-notify/tab-reply";
@@ -1716,7 +1716,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
// A mail link from another program, in the desktop app: a compose window of its own,
// as Outlook and Thunderbird do, and the main window stays where it was. Plugins get
// their say first. False when it has to open here instead.
- const openMailtoInWindow = useCallback(async (pending: DraftRequest): Promise => {
+ const openMailtoInWindow = useCallback(async (pending: DraftRequest, accountId?: string): Promise => {
if (!pending.fromOs || !hasDesktopWindows() || !popoutsAvailable()) return false;
let prefill: ComposePrefill | null = null;
if (emailHooks.onBeforeCompose.size > 0) {
@@ -1729,7 +1729,8 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
// A plugin said no: nothing opens, here or there.
if (!prefill) return true;
}
- const opened = openComposerWindow(useAuthStore.getState().activeAccountId);
+ // The account the link goes out from, in its own window: the main window keeps the one it shows.
+ const opened = openComposerWindow(accountId ?? useAuthStore.getState().activeAccountId);
if (!opened) return false;
void opened.handOver({
mode: 'compose',
@@ -1768,6 +1769,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
}, [openMailtoInWindow, openMailtoHere]);
const openMailtoForAccount = useCallback(async (pending: DraftRequest, accountId: string) => {
+ if (await openMailtoInWindow(pending, accountId)) {
+ setPendingMailtoAccountChoice(null);
+ return;
+ }
setIsProtocolAccountSwitching(true);
try {
if (useAuthStore.getState().activeAccountId !== accountId) {
@@ -1778,7 +1783,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
} finally {
setIsProtocolAccountSwitching(false);
}
- }, [openMailtoDraft, switchAccount]);
+ }, [openMailtoDraft, openMailtoInWindow, switchAccount]);
// onMailtoIntercept: a plugin may stop a mailto: from opening the composer.
const mailtoAllowed = useCallback(async (pending: ParsedMailto & { href?: string }): Promise => {
@@ -1791,17 +1796,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
});
}, []);
- // A draft that comes from outside: which account it goes out from.
+ // A draft that comes from outside goes out from the default account, as in Outlook and
+ // Thunderbird, with no question: the composer's From changes it (desktop-p2-8).
const routeOutsideDraft = useCallback(async (pending: DraftRequest): Promise => {
- const protocolAccounts = getMailtoProtocolAccounts();
- if (protocolAccounts.length > 1) {
- // The question is asked in this window: bring it up.
- if (pending.fromOs) getDesktopBridge()?.windows?.focus?.();
- setPendingMailtoAccountChoice(pending);
- return;
- }
-
- const accountId = protocolAccounts[0]?.id ?? activeAccountId;
+ const accountId = mailLinkAccountId(getMailtoProtocolAccounts(), useAccountStore.getState().defaultAccountId, activeAccountId);
if (accountId) {
await openMailtoForAccount(pending, accountId);
return;
diff --git a/components/protocol/protocol-account-picker.tsx b/components/protocol/protocol-account-picker.tsx
index 9bc672a4a..bb594ce9e 100644
--- a/components/protocol/protocol-account-picker.tsx
+++ b/components/protocol/protocol-account-picker.tsx
@@ -167,7 +167,7 @@ export function ProtocolAccountPicker({
{t("switching_account")}
) : (
- {t("select_account_note")}
+
)}