fix: open a mail link from another program from the default account with no question, and drop "protocol link" from the account picker

This commit is contained in:
Linus Rath
2026-11-04 12:42:30 +01:00
parent 03ea1ab1aa
commit 98f814fced
34 changed files with 48 additions and 45 deletions
@@ -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(<MailApp />);
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(<MailApp />);
act(() => { requestInternalMailto('mailto:bob@example.com?subject=Hi'); });
+12 -14
View File
@@ -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<boolean> => {
const openMailtoInWindow = useCallback(async (pending: DraftRequest, accountId?: string): Promise<boolean> => {
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<boolean> => {
@@ -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<void> => {
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;
@@ -167,7 +167,7 @@ export function ProtocolAccountPicker({
{t("switching_account")}
</span>
) : (
<span className="text-xs text-muted-foreground">{t("select_account_note")}</span>
<span />
)}
<button
type="button"
+11
View File
@@ -357,3 +357,14 @@ export function subscribeToPendingWebcal(callback: () => void): () => void {
export function hasPendingWebcal(): boolean {
return hasPending(WEBCAL_KEY, isParsedWebcal);
}
/**
* The account a mail link from outside goes out from, with no question: the one marked
* default, else the one in use, as Outlook and Thunderbird do. The composer's From can
* change it. Only signed-in accounts count.
*/
export function mailLinkAccountId(accounts: readonly { id: string }[], defaultId: string | null, activeId: string | null): string | null {
if (defaultId && accounts.some((account) => account.id === defaultId)) return defaultId;
if (activeId && accounts.some((account) => account.id === activeId)) return activeId;
return accounts[0]?.id ?? activeId ?? null;
}
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "اختر الحساب",
"select_mailto_account": "اختر الحساب الذي سيفتح رابط البريد هذا.",
"select_webcal_account": "اختر الحساب الذي سيفتح رابط التقويم هذا.",
"select_account_note": "ينطبق هذا فقط على رابط البروتوكول هذا.",
"detail_to": "إلى",
"detail_subject": "الموضوع",
"detail_no_subject": "بلا موضوع",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Trieu un compte",
"select_mailto_account": "Trieu quin compte ha d'obrir aquest enllaç de correu.",
"select_webcal_account": "Trieu quin compte ha d'obrir aquest enllaç de calendari.",
"select_account_note": "Això només s'aplica a aquest enllaç de protocol.",
"detail_to": "Per a",
"detail_subject": "Assumpte",
"detail_no_subject": "Sense assumpte",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Vybrat účet",
"select_mailto_account": "Vyberte účet, ve kterém se má tento e-mailový odkaz otevřít.",
"select_webcal_account": "Vyberte účet, ve kterém se má tento kalendářový odkaz otevřít.",
"select_account_note": "Tato volba platí jen pro tento odkaz protokolu.",
"detail_to": "Komu",
"detail_subject": "Předmět",
"detail_no_subject": "Bez předmětu",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Vælg konto",
"select_mailto_account": "Vælg hvilken konto der skal åbne dette e-mail-link.",
"select_webcal_account": "Vælg hvilken konto der skal åbne dette kalenderlink.",
"select_account_note": "Dette gælder kun for dette protokollink.",
"detail_to": "Til",
"detail_subject": "Emne",
"detail_no_subject": "Intet emne",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Account auswählen",
"select_mailto_account": "Wählen Sie aus, mit welchem Account dieser E-Mail-Link geöffnet werden soll.",
"select_webcal_account": "Wählen Sie aus, mit welchem Account dieser Kalender-Link geöffnet werden soll.",
"select_account_note": "Diese Auswahl gilt nur für diesen Protokoll-Link.",
"detail_to": "An",
"detail_subject": "Betreff",
"detail_no_subject": "Ohne Betreff",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Choose account",
"select_mailto_account": "Choose which account should open this email link.",
"select_webcal_account": "Choose which account should open this calendar link.",
"select_account_note": "This only applies to this protocol link.",
"detail_to": "To",
"detail_subject": "Subject",
"detail_no_subject": "No subject",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Elegir cuenta",
"select_mailto_account": "Elige qué cuenta debe abrir este enlace de correo.",
"select_webcal_account": "Elige qué cuenta debe abrir este enlace de calendario.",
"select_account_note": "Esta selección solo se aplica a este enlace de protocolo.",
"detail_to": "Para",
"detail_subject": "Asunto",
"detail_no_subject": "Sin asunto",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Vali konto",
"select_mailto_account": "Vali, milline konto peaks selle e-posti lingi avama.",
"select_webcal_account": "Vali, milline konto peaks selle kalendrilingi avama.",
"select_account_note": "See kehtib ainult selle protokollilingi kohta.",
"detail_to": "Kellele",
"detail_subject": "Teema",
"detail_no_subject": "Teema puudub",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "انتخاب حساب",
"select_mailto_account": "انتخاب کنید کدام حساب این لینک ایمیل را باز کند.",
"select_webcal_account": "انتخاب کنید کدام حساب این لینک تقویم را باز کند.",
"select_account_note": "فقط برای این لینک پروتکل اعمال می‌شود.",
"detail_to": "به",
"detail_subject": "موضوع",
"detail_no_subject": "بدون موضوع",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Choisir un compte",
"select_mailto_account": "Choisissez le compte qui doit ouvrir ce lien e-mail.",
"select_webcal_account": "Choisissez le compte qui doit ouvrir ce lien de calendrier.",
"select_account_note": "Cette sélection s'applique uniquement à ce lien de protocole.",
"detail_to": "À",
"detail_subject": "Objet",
"detail_no_subject": "Sans objet",
-1
View File
@@ -5461,7 +5461,6 @@
"select_account_title": "בחר חשבון",
"select_mailto_account": "בחר איזה חשבון צריך לפתוח קישור דוא״ל זה.",
"select_webcal_account": "בחר איזה חשבון צריך לפתוח קישור לוח שנה זה.",
"select_account_note": "זה חל רק על קישור פרוטוקול זה.",
"detail_to": "אל",
"detail_subject": "נושא",
"detail_no_subject": "אין נושא",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Fiók kiválasztása",
"select_mailto_account": "Válaszd ki, melyik fiók nyissa meg ezt az e-mail hivatkozást.",
"select_webcal_account": "Válaszd ki, melyik fiók nyissa meg ezt a naptár hivatkozást.",
"select_account_note": "Ez csak erre a protokoll hivatkozásra vonatkozik.",
"detail_to": "Címzett",
"detail_subject": "Tárgy",
"detail_no_subject": "Nincs tárgy",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Scegli account",
"select_mailto_account": "Scegli quale account deve aprire questo link e-mail.",
"select_webcal_account": "Scegli quale account deve aprire questo link calendario.",
"select_account_note": "Questa scelta si applica solo a questo link di protocollo.",
"detail_to": "A",
"detail_subject": "Oggetto",
"detail_no_subject": "Senza oggetto",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "アカウントを選択",
"select_mailto_account": "このメールリンクを開くアカウントを選択してください。",
"select_webcal_account": "このカレンダーリンクを開くアカウントを選択してください。",
"select_account_note": "この選択は、このプロトコル リンクにのみ適用されます。",
"detail_to": "宛先",
"detail_subject": "件名",
"detail_no_subject": "件名なし",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "계정 선택",
"select_mailto_account": "이 이메일 링크를 열 계정을 선택하세요.",
"select_webcal_account": "이 캘린더 링크를 열 계정을 선택하세요.",
"select_account_note": "이 선택은 이 프로토콜 링크에만 적용됩니다.",
"detail_to": "받는 사람",
"detail_subject": "제목",
"detail_no_subject": "제목 없음",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Izvēlieties kontu",
"select_mailto_account": "Izvēlieties, kurā kontā atvērt šo e-pasta saiti.",
"select_webcal_account": "Izvēlieties, kurā kontā atvērt šo kalendāra saiti.",
"select_account_note": "Šī izvēle attiecas tikai uz šo protokola saiti.",
"detail_to": "Kam",
"detail_subject": "Temats",
"detail_no_subject": "Bez temata",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Бүртгэл сонгох",
"select_mailto_account": "Энэ имэйлийн холбоосыг аль бүртгэлээр нээхийг сонгоно уу.",
"select_webcal_account": "Энэ хуанлийн холбоосыг аль данс нээхийг сонгоно уу.",
"select_account_note": "Энэ нь зөвхөн энэ протоколын холбоост хамаарна.",
"detail_to": "Хэнд",
"detail_subject": "Сэдэв",
"detail_no_subject": "Сэдэв байхгүй",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Velg konto",
"select_mailto_account": "Velg hvilken konto som skal åpne denne e-postlenken.",
"select_webcal_account": "Velg hvilken konto som skal åpne denne kalenderlenken.",
"select_account_note": "Dette gjelder bare denne lenken.",
"detail_to": "Til",
"detail_subject": "Emne",
"detail_no_subject": "Uten emne",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Account kiezen",
"select_mailto_account": "Kies welk account deze e-maillink moet openen.",
"select_webcal_account": "Kies welk account deze kalenderlink moet openen.",
"select_account_note": "Deze keuze geldt alleen voor deze protocol-link.",
"detail_to": "Aan",
"detail_subject": "Onderwerp",
"detail_no_subject": "Geen onderwerp",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Wybierz konto",
"select_mailto_account": "Wybierz konto, które ma otworzyć ten link e-mail.",
"select_webcal_account": "Wybierz konto, które ma otworzyć ten link kalendarza.",
"select_account_note": "Ten wybór dotyczy tylko tego linku protokołu.",
"detail_to": "Do",
"detail_subject": "Temat",
"detail_no_subject": "Bez tematu",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Escolher conta",
"select_mailto_account": "Escolha qual conta deve abrir este link de e-mail.",
"select_webcal_account": "Escolha qual conta deve abrir este link de calendário.",
"select_account_note": "Esta escolha se aplica apenas a este link de protocolo.",
"detail_to": "Para",
"detail_subject": "Assunto",
"detail_no_subject": "Sem assunto",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Alegeți contul",
"select_mailto_account": "Alegeți contul în care să se deschidă acest link de e-mail.",
"select_webcal_account": "Alegeți contul care ar trebui să deschidă acest link către calendar.",
"select_account_note": "Aceasta se aplică numai acestui link de protocol.",
"detail_to": "Către",
"detail_subject": "Subiect",
"detail_no_subject": "Fără subiect",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Выберите аккаунт",
"select_mailto_account": "Выберите, в каком аккаунте открыть эту ссылку электронной почты.",
"select_webcal_account": "Выберите, в каком аккаунте открыть эту ссылку календаря.",
"select_account_note": "Этот выбор применяется только к этой ссылке протокола.",
"detail_to": "Кому",
"detail_subject": "Тема",
"detail_no_subject": "Без темы",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Vybrať účet",
"select_mailto_account": "Vyberte účet, v ktorom sa má tento e-mailový odkaz otvoriť.",
"select_webcal_account": "Vyberte účet, v ktorom sa má tento kalendárový odkaz otvoriť.",
"select_account_note": "Toto platí iba pre tento odkaz protokolu.",
"detail_to": "Komu",
"detail_subject": "Predmet",
"detail_no_subject": "Bez predmetu",
-1
View File
@@ -2701,7 +2701,6 @@
"select_account_title": "เลือกบัญชี",
"select_mailto_account": "เลือกบัญชีที่ต้องการให้เปิดลิงก์อีเมลนี้",
"select_webcal_account": "เลือกบัญชีที่ต้องการให้เปิดลิงก์ปฏิทินนี้",
"select_account_note": "การตั้งค่านี้มีผลเฉพาะกับลิงก์โปรโตคอลนี้เท่านั้น",
"detail_to": "ถึง",
"detail_subject": "เรื่อง",
"detail_no_subject": "ไม่มีหัวข้อเรื่อง",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Hesap seç",
"select_mailto_account": "Bu e-posta bağlantısını hangi hesabın açacağını seçin.",
"select_webcal_account": "Bu takvim bağlantısını hangi hesabın açacağını seçin.",
"select_account_note": "Bu seçim yalnızca bu protokol bağlantısı için geçerlidir.",
"detail_to": "Kime",
"detail_subject": "Konu",
"detail_no_subject": "Konu yok",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Виберіть акаунт",
"select_mailto_account": "Виберіть акаунт, у якому слід відкрити це посилання електронної пошти.",
"select_webcal_account": "Виберіть акаунт, у якому слід відкрити це посилання календаря.",
"select_account_note": "Цей вибір застосовується лише до цього посилання протоколу.",
"detail_to": "Кому",
"detail_subject": "Тема",
"detail_no_subject": "Без теми",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "Chọn tài khoản",
"select_mailto_account": "Chọn tài khoản nào sẽ mở liên kết email này.",
"select_webcal_account": "Chọn tài khoản nào sẽ mở liên kết lịch này.",
"select_account_note": "Điều này chỉ áp dụng cho liên kết giao thức này.",
"detail_to": "Đến",
"detail_subject": "Tiêu đề",
"detail_no_subject": "Không có tiêu đề",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "選擇帳號",
"select_mailto_account": "選擇用於開啟此電子郵件連結的帳號。",
"select_webcal_account": "選擇用於開啟此行事曆連結的帳號。",
"select_account_note": "此選擇僅適用於此協議連結。",
"detail_to": "收件者",
"detail_subject": "主題",
"detail_no_subject": "無主題",
-1
View File
@@ -245,7 +245,6 @@
"select_account_title": "选择账户",
"select_mailto_account": "选择用于打开此电子邮件链接的账户。",
"select_webcal_account": "选择用于打开此日历链接的账户。",
"select_account_note": "此选择仅适用于此协议链接。",
"detail_to": "收件人",
"detail_subject": "主题",
"detail_no_subject": "无主题",