fix: count each plugin hook failure once and keep a plugin that kept failing turned off

This commit is contained in:
Linus Rath
2026-10-14 02:33:45 +02:00
parent a491eab317
commit f048db7b52
33 changed files with 183 additions and 70 deletions
+13
View File
@@ -10,6 +10,7 @@ import React, { useEffect, useId, useMemo, useState, useSyncExternalStore } from
import { useTranslations } from 'next-intl';
import { cancelForPlugin, head, resolveDialog, resolveHead, subscribe } from '@/lib/plugin-sandbox/host-dialog';
import { usePluginStore } from '@/stores/plugin-store';
import { toast } from '@/stores/toast-store';
import { Button } from '@/components/ui/button';
import { Puzzle, X } from '@/components/icons';
import { PluginIframeSlot } from './plugin-iframe-slot';
@@ -35,6 +36,18 @@ export function PluginDialogHost(): React.JSX.Element | null {
const pluginName = usePluginStore((s) => s.plugins.find((p) => p.id === current?.pluginId)?.name);
const disablePlugin = usePluginStore((s) => s.disablePlugin);
// Say so when a plugin was turned off for failing; otherwise it just
// stops working with no hint why.
useEffect(() => {
const onDisabled = (e: Event) => {
const id = (e as CustomEvent<{ pluginId: string }>).detail?.pluginId;
const name = usePluginStore.getState().plugins.find((p) => p.id === id)?.name ?? id;
if (id) toast.warning(t('toast_auto_disabled', { name }));
};
window.addEventListener('plugin:auto-disabled', onDisabled);
return () => window.removeEventListener('plugin:auto-disabled', onDisabled);
}, [t]);
const isPrompt = current?.kind === 'prompt';
const fields = useMemo(() => (isPrompt ? current?.fields ?? [] : []), [isPrompt, current]);
const hasPassword = fields.some((f) => f.type === 'password');
+1 -1
View File
@@ -24,7 +24,7 @@ const STATUS_COLORS: Record<PluginStatus, string> = {
};
/** Errors the store records as codes, so they can be shown translated. */
const ERROR_CODES = new Set(['user-plugins-off', 'approval-unavailable', 'approval-denied', 'approval-pending']);
const ERROR_CODES = new Set(['user-plugins-off', 'approval-unavailable', 'approval-denied', 'approval-pending', 'auto-disabled']);
export function PluginsSettings() {
const t = useTranslations('plugins.settings');
@@ -219,3 +219,27 @@ describe('setSandboxLocale', () => {
expect(mocks.setLocale).toHaveBeenCalledWith('de');
});
});
describe('failing hooks', () => {
it('turns a plugin off on its third failure, not its second', async () => {
const { emailHooks, pluginErrorTracker } = await import('../plugin-hooks');
const { setupSandboxAutoDisable } = await import('../plugin-sandbox/loader');
const markAutoDisabled = vi.fn();
setSandboxStoreAccessor({ setPluginStatus: () => {}, markAutoDisabled });
setupSandboxAutoDisable();
pluginErrorTracker.reset('managed-plugin');
mocks.getCode.mockResolvedValue(CODE);
mocks.createBackgroundInstance.mockImplementation(() => ({
...fakeBackground(),
initPromise: Promise.resolve({ hooks: ['onEmailOpen'], slots: [], shortcuts: [] }),
invokeHook: vi.fn(() => Promise.reject(new Error('boom'))),
}));
await loadSandboxedPlugin(plugin({ permissions: ['email:read'] }));
await emailHooks.onEmailOpen.emit({});
await emailHooks.onEmailOpen.emit({});
expect(markAutoDisabled).not.toHaveBeenCalled();
await emailHooks.onEmailOpen.emit({});
expect(markAutoDisabled).toHaveBeenCalledWith('managed-plugin');
});
});
+4 -1
View File
@@ -33,7 +33,10 @@ export function exposePluginExternals(): void {
// ─── Store accessor (status updates) ──────────────────────────
type StoreAccessor = { setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void };
type StoreAccessor = {
setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void;
markAutoDisabled?: (id: string) => void;
};
export function setPluginStoreAccessor(accessor: StoreAccessor): void {
setSandboxStoreAccessor(accessor);
+14 -10
View File
@@ -39,7 +39,11 @@ const HOOK_BUSES: Record<string, AnyBus> = Object.assign({},
// ─── Store accessor (status updates flow through the existing store) ──
type StoreAccessor = { setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void };
type StoreAccessor = {
setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void;
/** Turn a failing plugin off for good (until the user turns it on again). */
markAutoDisabled?: (id: string) => void;
};
let storeAccessor: StoreAccessor | null = null;
export function setSandboxStoreAccessor(a: StoreAccessor): void { storeAccessor = a; }
@@ -187,14 +191,9 @@ export async function loadSandboxedPlugin(plugin: InstalledPlugin): Promise<void
console.warn(`[plugin-sandbox] Plugin "${plugin.id}" lacks the permission for hook "${hookName}"; not registered`);
continue;
}
const proxy = async (...args: unknown[]) => {
try {
return await bg.invokeHook(hookName, args);
} catch (err) {
pluginErrorTracker.record(plugin.id, err);
throw err;
}
};
// The bus records a failure once; recording it here too made the
// three-strikes breaker trip on the second error.
const proxy = (...args: unknown[]) => bg.invokeHook(hookName, args);
hookDisposables.push(bus.register(plugin.id, proxy as (...a: unknown[]) => unknown));
}
@@ -277,7 +276,12 @@ export function setupSandboxAutoDisable(): void {
setHookTimeoutListener((pluginId) => cancelPluginDialogs(pluginId));
pluginErrorTracker.setAutoDisableCallback((pluginId) => {
unloadSandboxedPlugin(pluginId);
storeAccessor?.setPluginStatus(pluginId, 'error', 'Auto-disabled due to repeated errors');
if (storeAccessor?.markAutoDisabled) storeAccessor.markAutoDisabled(pluginId);
else storeAccessor?.setPluginStatus(pluginId, 'error', 'auto-disabled');
// PluginDialogHost turns this into a toast in the user's language.
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('plugin:auto-disabled', { detail: { pluginId } }));
}
});
}
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "أوقف المسؤول الإضافات التي يثبّتها المستخدمون، لذا لا تعمل هذه الإضافة.",
"error_approval_unavailable": "ثُبّتت هذه الإضافة قبل أن تصبح الموافقة مطلوبة. أزلها وثبّتها مرة أخرى لإرسالها للموافقة.",
"error_approval_denied": "لم يوافق المسؤول على هذه الإضافة.",
"error_approval_pending": "بانتظار موافقة المسؤول على هذه الإضافة."
"error_approval_pending": "بانتظار موافقة المسؤول على هذه الإضافة.",
"error_auto_disabled": "تم إيقافها لأنها فشلت مرارًا. شغّلها مرة أخرى للمحاولة من جديد."
},
"consent": {
"title": "هل تسمح لـ {name} بذلك؟",
@@ -4250,7 +4251,8 @@
"cancel": "إلغاء",
"close": "إغلاق",
"turn_off": "إيقاف {name}",
"password_note": "هذا النموذج من الإضافة {name} وليس من بريد الويب. لا تدخل كلمة مرور إلا إذا كنت تثق بهذه الإضافة."
"password_note": "هذا النموذج من الإضافة {name} وليس من بريد الويب. لا تدخل كلمة مرور إلا إذا كنت تثق بهذه الإضافة.",
"toast_auto_disabled": "تم إيقاف {name} لأنها فشلت مرارًا."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "L'administrador ha desactivat els connectors instal·lats pels usuaris, així que aquest connector no s'executa.",
"error_approval_unavailable": "Aquest connector es va instal·lar abans que calgués l'aprovació. Elimineu-lo i torneu-lo a instal·lar per enviar-lo a aprovar.",
"error_approval_denied": "L'administrador no ha aprovat aquest connector.",
"error_approval_pending": "S'espera que l'administrador aprovi aquest connector."
"error_approval_pending": "S'espera que l'administrador aprovi aquest connector.",
"error_auto_disabled": "S'ha desactivat perquè fallava repetidament. Torneu-lo a activar per tornar-ho a provar."
},
"consent": {
"title": "Voleu permetre que {name} faci això?",
@@ -4250,7 +4251,8 @@
"cancel": "Cancel·la",
"close": "Tanca",
"turn_off": "Desactiva {name}",
"password_note": "Aquest formulari ve del connector {name}, no del correu web. Introduïu una contrasenya només si confieu en aquest connector."
"password_note": "Aquest formulari ve del connector {name}, no del correu web. Introduïu una contrasenya només si confieu en aquest connector.",
"toast_auto_disabled": "{name} s'ha desactivat perquè fallava repetidament."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Váš správce vypnul pluginy instalované uživateli, proto tento plugin neběží.",
"error_approval_unavailable": "Tento plugin byl nainstalován dříve, než bylo vyžadováno schválení. Odstraňte ho a nainstalujte znovu, aby se odeslal ke schválení.",
"error_approval_denied": "Váš správce tento plugin neschválil.",
"error_approval_pending": "Čeká se, až správce tento plugin schválí."
"error_approval_pending": "Čeká se, až správce tento plugin schválí.",
"error_auto_disabled": "Vypnuto, protože opakovaně selhávalo. Zapněte ho znovu a zkuste to znovu."
},
"consent": {
"title": "Povolit pluginu {name} tyto akce?",
@@ -4250,7 +4251,8 @@
"cancel": "Zrušit",
"close": "Zavřít",
"turn_off": "Vypnout {name}",
"password_note": "Tento formulář pochází z pluginu {name}, ne z webmailu. Heslo zadejte jen tehdy, když tomuto pluginu důvěřujete."
"password_note": "Tento formulář pochází z pluginu {name}, ne z webmailu. Heslo zadejte jen tehdy, když tomuto pluginu důvěřujete.",
"toast_auto_disabled": "{name} byl vypnut, protože opakovaně selhával."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Din administrator har slået plugins installeret af brugere fra, så dette plugin kører ikke.",
"error_approval_unavailable": "Dette plugin blev installeret, før godkendelse var påkrævet. Fjern det, og installer det igen for at sende det til godkendelse.",
"error_approval_denied": "Din administrator har ikke godkendt dette plugin.",
"error_approval_pending": "Venter på, at din administrator godkender dette plugin."
"error_approval_pending": "Venter på, at din administrator godkender dette plugin.",
"error_auto_disabled": "Slået fra, fordi det blev ved med at fejle. Slå det til igen for at prøve igen."
},
"consent": {
"title": "Vil du tillade {name} at gøre dette?",
@@ -4250,7 +4251,8 @@
"cancel": "Annuller",
"close": "Luk",
"turn_off": "Slå {name} fra",
"password_note": "Denne formular kommer fra pluginet {name}, ikke fra webmailen. Indtast kun en adgangskode, hvis du stoler på dette plugin."
"password_note": "Denne formular kommer fra pluginet {name}, ikke fra webmailen. Indtast kun en adgangskode, hvis du stoler på dette plugin.",
"toast_auto_disabled": "{name} blev slået fra, fordi det blev ved med at fejle."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Ihr Administrator hat von Benutzern installierte Plugins ausgeschaltet. Dieses Plugin läuft daher nicht.",
"error_approval_unavailable": "Dieses Plugin wurde installiert, bevor eine Freigabe nötig war. Entfernen Sie es und installieren Sie es erneut, um es zur Freigabe vorzulegen.",
"error_approval_denied": "Ihr Administrator hat dieses Plugin nicht freigegeben.",
"error_approval_pending": "Wartet darauf, dass Ihr Administrator dieses Plugin freigibt."
"error_approval_pending": "Wartet darauf, dass Ihr Administrator dieses Plugin freigibt.",
"error_auto_disabled": "Ausgeschaltet, weil es wiederholt fehlgeschlagen ist. Schalten Sie es wieder ein, um es erneut zu versuchen."
},
"consent": {
"title": "{name} Folgendes erlauben?",
@@ -4250,7 +4251,8 @@
"cancel": "Abbrechen",
"close": "Schließen",
"turn_off": "{name} ausschalten",
"password_note": "Dieses Formular stammt vom Plugin {name}, nicht vom Webmail. Geben Sie ein Passwort nur ein, wenn Sie diesem Plugin vertrauen."
"password_note": "Dieses Formular stammt vom Plugin {name}, nicht vom Webmail. Geben Sie ein Passwort nur ein, wenn Sie diesem Plugin vertrauen.",
"toast_auto_disabled": "{name} wurde ausgeschaltet, weil es wiederholt fehlgeschlagen ist."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Your administrator turned off plugins installed by users, so this plugin does not run.",
"error_approval_unavailable": "This plugin was installed before approvals were required. Remove it and install it again to send it for approval.",
"error_approval_denied": "Your administrator did not approve this plugin.",
"error_approval_pending": "Waiting for your administrator to approve this plugin."
"error_approval_pending": "Waiting for your administrator to approve this plugin.",
"error_auto_disabled": "Turned off because it kept failing. Turn it on again to retry."
},
"consent": {
"title": "Allow {name} to do this?",
@@ -4250,7 +4251,8 @@
"cancel": "Cancel",
"close": "Close",
"turn_off": "Turn off {name}",
"password_note": "This form comes from the plugin {name}, not from the webmail. Only enter a password if you trust this plugin."
"password_note": "This form comes from the plugin {name}, not from the webmail. Only enter a password if you trust this plugin.",
"toast_auto_disabled": "{name} was turned off because it kept failing."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Tu administrador desactivó los complementos instalados por usuarios, así que este complemento no se ejecuta.",
"error_approval_unavailable": "Este complemento se instaló antes de que se exigiera aprobación. Elimínalo e instálalo de nuevo para enviarlo a aprobación.",
"error_approval_denied": "Tu administrador no aprobó este complemento.",
"error_approval_pending": "Esperando a que tu administrador apruebe este complemento."
"error_approval_pending": "Esperando a que tu administrador apruebe este complemento.",
"error_auto_disabled": "Desactivado porque fallaba una y otra vez. Actívalo de nuevo para volver a intentarlo."
},
"consent": {
"title": "¿Permitir que {name} haga esto?",
@@ -4250,7 +4251,8 @@
"cancel": "Cancelar",
"close": "Cerrar",
"turn_off": "Desactivar {name}",
"password_note": "Este formulario viene del complemento {name}, no del correo web. Introduce una contraseña solo si confías en este complemento."
"password_note": "Este formulario viene del complemento {name}, no del correo web. Introduce una contraseña solo si confías en este complemento.",
"toast_auto_disabled": "{name} se desactivó porque fallaba una y otra vez."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "مدیر سیستم افزونه‌هایی را که کاربران نصب می‌کنند خاموش کرده است، بنابراین این افزونه اجرا نمی‌شود.",
"error_approval_unavailable": "این افزونه پیش از الزامی شدن تأیید نصب شده است. آن را حذف و دوباره نصب کنید تا برای تأیید ارسال شود.",
"error_approval_denied": "مدیر سیستم این افزونه را تأیید نکرد.",
"error_approval_pending": "در انتظار تأیید این افزونه توسط مدیر سیستم."
"error_approval_pending": "در انتظار تأیید این افزونه توسط مدیر سیستم.",
"error_auto_disabled": "به‌دلیل خطاهای پی‌درپی خاموش شد. برای تلاش دوباره آن را روشن کنید."
},
"consent": {
"title": "به {name} اجازه این کار داده شود؟",
@@ -4250,7 +4251,8 @@
"cancel": "لغو",
"close": "بستن",
"turn_off": "خاموش کردن {name}",
"password_note": "این فرم از افزونه {name} است، نه از وب‌میل. فقط اگر به این افزونه اعتماد دارید گذرواژه وارد کنید."
"password_note": "این فرم از افزونه {name} است، نه از وب‌میل. فقط اگر به این افزونه اعتماد دارید گذرواژه وارد کنید.",
"toast_auto_disabled": "{name} به‌دلیل خطاهای پی‌درپی خاموش شد."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Votre administrateur a désactivé les plugins installés par les utilisateurs, ce plugin ne s'exécute donc pas.",
"error_approval_unavailable": "Ce plugin a été installé avant que l'approbation soit requise. Supprimez-le et réinstallez-le pour l'envoyer en approbation.",
"error_approval_denied": "Votre administrateur n'a pas approuvé ce plugin.",
"error_approval_pending": "En attente de l'approbation de ce plugin par votre administrateur."
"error_approval_pending": "En attente de l'approbation de ce plugin par votre administrateur.",
"error_auto_disabled": "Désactivé parce qu'il échouait sans cesse. Réactivez-le pour réessayer."
},
"consent": {
"title": "Autoriser {name} à faire ceci ?",
@@ -4250,7 +4251,8 @@
"cancel": "Annuler",
"close": "Fermer",
"turn_off": "Désactiver {name}",
"password_note": "Ce formulaire provient du plugin {name}, pas du webmail. Ne saisissez un mot de passe que si vous faites confiance à ce plugin."
"password_note": "Ce formulaire provient du plugin {name}, pas du webmail. Ne saisissez un mot de passe que si vous faites confiance à ce plugin.",
"toast_auto_disabled": "{name} a été désactivé parce qu'il échouait sans cesse."
}
},
"global_search": {
+4 -2
View File
@@ -4017,7 +4017,8 @@
"error_user_plugins_off": "מנהל המערכת כיבה תוספים שמשתמשים התקינו, ולכן התוסף הזה לא פועל.",
"error_approval_unavailable": "התוסף הזה הותקן לפני שנדרש אישור. הסירו אותו והתקינו אותו מחדש כדי לשלוח אותו לאישור.",
"error_approval_denied": "מנהל המערכת לא אישר את התוסף הזה.",
"error_approval_pending": "ממתין לאישור התוסף הזה על ידי מנהל המערכת."
"error_approval_pending": "ממתין לאישור התוסף הזה על ידי מנהל המערכת.",
"error_auto_disabled": "כובה כי נכשל שוב ושוב. הפעילו אותו מחדש כדי לנסות שוב."
},
"consent": {
"title": "לאפשר ל-{name} לעשות זאת?",
@@ -4251,7 +4252,8 @@
"cancel": "ביטול",
"close": "סגירה",
"turn_off": "כיבוי {name}",
"password_note": "הטופס הזה מגיע מהתוסף {name}, לא מהדואר האינטרנטי. הזינו סיסמה רק אם אתם סומכים על התוסף הזה."
"password_note": "הטופס הזה מגיע מהתוסף {name}, לא מהדואר האינטרנטי. הזינו סיסמה רק אם אתם סומכים על התוסף הזה.",
"toast_auto_disabled": "{name} כובה כי נכשל שוב ושוב."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "A rendszergazda kikapcsolta a felhasználók által telepített bővítményeket, ezért ez a bővítmény nem fut.",
"error_approval_unavailable": "Ezt a bővítményt még azelőtt telepítetted, hogy jóváhagyás kellett volna. Távolítsd el, majd telepítsd újra, hogy jóváhagyásra kerüljön.",
"error_approval_denied": "A rendszergazda nem hagyta jóvá ezt a bővítményt.",
"error_approval_pending": "Várakozás arra, hogy a rendszergazda jóváhagyja ezt a bővítményt."
"error_approval_pending": "Várakozás arra, hogy a rendszergazda jóváhagyja ezt a bővítményt.",
"error_auto_disabled": "Kikapcsolva, mert többször is hibát okozott. Kapcsolja be újra, ha ismét meg szeretné próbálni."
},
"consent": {
"title": "{name}: engedélyezed ezeket a műveleteket?",
@@ -4250,7 +4251,8 @@
"cancel": "Mégse",
"close": "Bezárás",
"turn_off": "{name} kikapcsolása",
"password_note": "Ez az űrlap a(z) {name} bővítménytől származik, nem a webmailtől. Csak akkor adjon meg jelszót, ha megbízik ebben a bővítményben."
"password_note": "Ez az űrlap a(z) {name} bővítménytől származik, nem a webmailtől. Csak akkor adjon meg jelszót, ha megbízik ebben a bővítményben.",
"toast_auto_disabled": "{name} ki lett kapcsolva, mert többször is hibát okozott."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Il tuo amministratore ha disattivato i plugin installati dagli utenti, quindi questo plugin non viene eseguito.",
"error_approval_unavailable": "Questo plugin è stato installato prima che fosse richiesta l'approvazione. Rimuovilo e installalo di nuovo per inviarlo in approvazione.",
"error_approval_denied": "Il tuo amministratore non ha approvato questo plugin.",
"error_approval_pending": "In attesa che il tuo amministratore approvi questo plugin."
"error_approval_pending": "In attesa che il tuo amministratore approvi questo plugin.",
"error_auto_disabled": "Disattivato perché continuava a non funzionare. Riattivalo per riprovare."
},
"consent": {
"title": "Consentire a {name} di fare questo?",
@@ -4250,7 +4251,8 @@
"cancel": "Annulla",
"close": "Chiudi",
"turn_off": "Disattiva {name}",
"password_note": "Questo modulo proviene dal plugin {name}, non dalla webmail. Inserisci una password solo se ti fidi di questo plugin."
"password_note": "Questo modulo proviene dal plugin {name}, non dalla webmail. Inserisci una password solo se ti fidi di questo plugin.",
"toast_auto_disabled": "{name} è stato disattivato perché continuava a non funzionare."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "管理者がユーザーによるプラグインを無効にしたため、このプラグインは動作しません。",
"error_approval_unavailable": "このプラグインは承認が必要になる前にインストールされました。承認を依頼するには、削除してもう一度インストールしてください。",
"error_approval_denied": "管理者はこのプラグインを承認しませんでした。",
"error_approval_pending": "管理者がこのプラグインを承認するのを待っています。"
"error_approval_pending": "管理者がこのプラグインを承認するのを待っています。",
"error_auto_disabled": "繰り返し失敗したためオフになりました。再試行するにはもう一度オンにしてください。"
},
"consent": {
"title": "{name} にこの操作を許可しますか?",
@@ -4250,7 +4251,8 @@
"cancel": "キャンセル",
"close": "閉じる",
"turn_off": "{name} をオフにする",
"password_note": "このフォームはウェブメールではなく、プラグイン {name} が表示しています。このプラグインを信頼できる場合にのみパスワードを入力してください。"
"password_note": "このフォームはウェブメールではなく、プラグイン {name} が表示しています。このプラグインを信頼できる場合にのみパスワードを入力してください。",
"toast_auto_disabled": "{name} は繰り返し失敗したためオフになりました。"
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "관리자가 사용자가 설치한 플러그인을 꺼 두어서 이 플러그인은 실행되지 않아요.",
"error_approval_unavailable": "이 플러그인은 승인이 필요해지기 전에 설치되었어요. 승인을 요청하려면 삭제한 뒤 다시 설치하세요.",
"error_approval_denied": "관리자가 이 플러그인을 승인하지 않았어요.",
"error_approval_pending": "관리자가 이 플러그인을 승인하기를 기다리고 있어요."
"error_approval_pending": "관리자가 이 플러그인을 승인하기를 기다리고 있어요.",
"error_auto_disabled": "계속 실패해서 꺼졌습니다. 다시 시도하려면 다시 켜세요."
},
"consent": {
"title": "{name}에 이 권한을 허용할까요?",
@@ -4250,7 +4251,8 @@
"cancel": "취소",
"close": "닫기",
"turn_off": "{name} 끄기",
"password_note": "이 양식은 웹메일이 아니라 플러그인 {name}에서 표시한 것입니다. 이 플러그인을 신뢰할 때만 비밀번호를 입력하세요."
"password_note": "이 양식은 웹메일이 아니라 플러그인 {name}에서 표시한 것입니다. 이 플러그인을 신뢰할 때만 비밀번호를 입력하세요.",
"toast_auto_disabled": "{name}이(가) 계속 실패해서 꺼졌습니다."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Jūsu administrators ir izslēdzis lietotāju instalētos spraudņus, tāpēc šis spraudnis nedarbojas.",
"error_approval_unavailable": "Šis spraudnis tika instalēts, pirms bija vajadzīgs apstiprinājums. Noņemiet to un instalējiet vēlreiz, lai nosūtītu apstiprināšanai.",
"error_approval_denied": "Jūsu administrators neapstiprināja šo spraudni.",
"error_approval_pending": "Gaida, kad jūsu administrators apstiprinās šo spraudni."
"error_approval_pending": "Gaida, kad jūsu administrators apstiprinās šo spraudni.",
"error_auto_disabled": "Izslēgts, jo atkārtoti neizdevās. Ieslēdziet to vēlreiz, lai mēģinātu vēlreiz."
},
"consent": {
"title": "Atļaut spraudnim {name} to darīt?",
@@ -4250,7 +4251,8 @@
"cancel": "Atcelt",
"close": "Aizvērt",
"turn_off": "Izslēgt {name}",
"password_note": "Šī veidlapa nāk no spraudņa {name}, nevis no tīmekļa pasta. Ievadiet paroli tikai tad, ja uzticaties šim spraudnim."
"password_note": "Šī veidlapa nāk no spraudņa {name}, nevis no tīmekļa pasta. Ievadiet paroli tikai tad, ja uzticaties šim spraudnim.",
"toast_auto_disabled": "{name} tika izslēgts, jo atkārtoti neizdevās."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Таны администратор хэрэглэгчийн суулгасан залгаасуудыг унтраасан тул энэ залгаас ажиллахгүй.",
"error_approval_unavailable": "Энэ залгаасыг зөвшөөрөл шаарддаг болохоос өмнө суулгасан. Зөвшөөрөлд илгээхийн тулд устгаад дахин суулгана уу.",
"error_approval_denied": "Таны администратор энэ залгаасыг зөвшөөрөөгүй.",
"error_approval_pending": "Таны администратор энэ залгаасыг зөвшөөрөхийг хүлээж байна."
"error_approval_pending": "Таны администратор энэ залгаасыг зөвшөөрөхийг хүлээж байна.",
"error_auto_disabled": "Дахин дахин алдаа гаргасан тул унтраасан. Дахин оролдохын тулд асаана уу."
},
"consent": {
"title": "{name} залгаасад үүнийг зөвшөөрөх үү?",
@@ -4250,7 +4251,8 @@
"cancel": "Цуцлах",
"close": "Хаах",
"turn_off": "{name}-г унтраах",
"password_note": "Энэ маягт вэб шуудангаас биш, {name} залгаасаас ирсэн. Энэ залгаасанд итгэдэг бол л нууц үгээ оруулна уу."
"password_note": "Энэ маягт вэб шуудангаас биш, {name} залгаасаас ирсэн. Энэ залгаасанд итгэдэг бол л нууц үгээ оруулна уу.",
"toast_auto_disabled": "{name} дахин дахин алдаа гаргасан тул унтраагдлаа."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Administratoren din har slått av tillegg som brukere har installert, så dette tillegget kjører ikke.",
"error_approval_unavailable": "Dette tillegget ble installert før godkjenning ble påkrevd. Fjern det og installer det på nytt for å sende det til godkjenning.",
"error_approval_denied": "Administratoren din godkjente ikke dette tillegget.",
"error_approval_pending": "Venter på at administratoren din skal godkjenne dette tillegget."
"error_approval_pending": "Venter på at administratoren din skal godkjenne dette tillegget.",
"error_auto_disabled": "Slått av fordi det feilet gjentatte ganger. Slå det på igjen for å prøve på nytt."
},
"consent": {
"title": "Vil du la {name} gjøre dette?",
@@ -4250,7 +4251,8 @@
"cancel": "Avbryt",
"close": "Lukk",
"turn_off": "Slå av {name}",
"password_note": "Dette skjemaet kommer fra tillegget {name}, ikke fra webmailen. Skriv bare inn et passord hvis du stoler på dette tillegget."
"password_note": "Dette skjemaet kommer fra tillegget {name}, ikke fra webmailen. Skriv bare inn et passord hvis du stoler på dette tillegget.",
"toast_auto_disabled": "{name} ble slått av fordi det feilet gjentatte ganger."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Je beheerder heeft plug-ins die door gebruikers zijn geïnstalleerd uitgezet, dus deze plug-in draait niet.",
"error_approval_unavailable": "Deze plug-in is geïnstalleerd voordat goedkeuring verplicht was. Verwijder hem en installeer hem opnieuw om hem ter goedkeuring te sturen.",
"error_approval_denied": "Je beheerder heeft deze plug-in niet goedgekeurd.",
"error_approval_pending": "Wacht tot je beheerder deze plug-in goedkeurt."
"error_approval_pending": "Wacht tot je beheerder deze plug-in goedkeurt.",
"error_auto_disabled": "Uitgezet omdat het steeds misging. Zet het weer aan om het opnieuw te proberen."
},
"consent": {
"title": "{name} dit toestaan?",
@@ -4250,7 +4251,8 @@
"cancel": "Annuleren",
"close": "Sluiten",
"turn_off": "{name} uitzetten",
"password_note": "Dit formulier komt van de plug-in {name}, niet van de webmail. Vul alleen een wachtwoord in als je deze plug-in vertrouwt."
"password_note": "Dit formulier komt van de plug-in {name}, niet van de webmail. Vul alleen een wachtwoord in als je deze plug-in vertrouwt.",
"toast_auto_disabled": "{name} is uitgezet omdat het steeds misging."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Administrator wyłączył wtyczki instalowane przez użytkowników, więc ta wtyczka nie działa.",
"error_approval_unavailable": "Ta wtyczka została zainstalowana, zanim wprowadzono zatwierdzanie. Usuń ją i zainstaluj ponownie, aby wysłać ją do zatwierdzenia.",
"error_approval_denied": "Administrator nie zatwierdził tej wtyczki.",
"error_approval_pending": "Czeka na zatwierdzenie przez administratora."
"error_approval_pending": "Czeka na zatwierdzenie przez administratora.",
"error_auto_disabled": "Wyłączono, bo wielokrotnie zawodziło. Włącz ponownie, aby spróbować jeszcze raz."
},
"consent": {
"title": "Zezwolić wtyczce {name} na te działania?",
@@ -4250,7 +4251,8 @@
"cancel": "Anuluj",
"close": "Zamknij",
"turn_off": "Wyłącz {name}",
"password_note": "Ten formularz pochodzi z wtyczki {name}, a nie z poczty. Wpisz hasło tylko wtedy, gdy ufasz tej wtyczce."
"password_note": "Ten formularz pochodzi z wtyczki {name}, a nie z poczty. Wpisz hasło tylko wtedy, gdy ufasz tej wtyczce.",
"toast_auto_disabled": "{name} wyłączono, bo wielokrotnie zawodziło."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Seu administrador desativou os plugins instalados por usuários, por isso este plugin não é executado.",
"error_approval_unavailable": "Este plugin foi instalado antes de as aprovações serem exigidas. Remova-o e instale-o novamente para enviá-lo para aprovação.",
"error_approval_denied": "Seu administrador não aprovou este plugin.",
"error_approval_pending": "Aguardando o seu administrador aprovar este plugin."
"error_approval_pending": "Aguardando o seu administrador aprovar este plugin.",
"error_auto_disabled": "Desativado porque falhava repetidamente. Ative-o de novo para tentar outra vez."
},
"consent": {
"title": "Permitir que {name} faça isto?",
@@ -4250,7 +4251,8 @@
"cancel": "Cancelar",
"close": "Fechar",
"turn_off": "Desativar {name}",
"password_note": "Este formulário vem do plugin {name}, não do webmail. Só introduza uma palavra-passe se confiar neste plugin."
"password_note": "Este formulário vem do plugin {name}, não do webmail. Só introduza uma palavra-passe se confiar neste plugin.",
"toast_auto_disabled": "{name} foi desativado porque falhava repetidamente."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Administratorul dvs. a dezactivat pluginurile instalate de utilizatori, așa că acest plugin nu rulează.",
"error_approval_unavailable": "Acest plugin a fost instalat înainte ca aprobările să fie obligatorii. Eliminați-l și instalați-l din nou pentru a-l trimite spre aprobare.",
"error_approval_denied": "Administratorul dvs. nu a aprobat acest plugin.",
"error_approval_pending": "Se așteaptă ca administratorul să aprobe acest plugin."
"error_approval_pending": "Se așteaptă ca administratorul să aprobe acest plugin.",
"error_auto_disabled": "Dezactivat pentru că a eșuat în mod repetat. Activați-l din nou pentru a reîncerca."
},
"consent": {
"title": "Permiteți ca {name} să facă acest lucru?",
@@ -4250,7 +4251,8 @@
"cancel": "Anulează",
"close": "Închide",
"turn_off": "Dezactivează {name}",
"password_note": "Acest formular vine de la pluginul {name}, nu de la webmail. Introduceți o parolă doar dacă aveți încredere în acest plugin."
"password_note": "Acest formular vine de la pluginul {name}, nu de la webmail. Introduceți o parolă doar dacă aveți încredere în acest plugin.",
"toast_auto_disabled": "{name} a fost dezactivat pentru că a eșuat în mod repetat."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Администратор отключил плагины, установленные пользователями, поэтому этот плагин не работает.",
"error_approval_unavailable": "Этот плагин был установлен до того, как появилось одобрение. Удалите и установите его снова, чтобы отправить на одобрение.",
"error_approval_denied": "Администратор не одобрил этот плагин.",
"error_approval_pending": "Ожидается одобрение этого плагина администратором."
"error_approval_pending": "Ожидается одобрение этого плагина администратором.",
"error_auto_disabled": "Отключён, потому что постоянно выдавал ошибки. Включите его снова, чтобы попробовать ещё раз."
},
"consent": {
"title": "Разрешить плагину {name} это?",
@@ -4250,7 +4251,8 @@
"cancel": "Отмена",
"close": "Закрыть",
"turn_off": "Отключить {name}",
"password_note": "Эта форма от плагина {name}, а не от веб-почты. Вводите пароль, только если доверяете этому плагину."
"password_note": "Эта форма от плагина {name}, а не от веб-почты. Вводите пароль, только если доверяете этому плагину.",
"toast_auto_disabled": "{name} отключён, потому что постоянно выдавал ошибки."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Váš správca vypol doplnky inštalované používateľmi, preto tento doplnok nebeží.",
"error_approval_unavailable": "Tento doplnok bol nainštalovaný skôr, než sa vyžadovalo schválenie. Odstráňte ho a nainštalujte znova, aby sa odoslal na schválenie.",
"error_approval_denied": "Váš správca tento doplnok neschválil.",
"error_approval_pending": "Čaká sa, kým správca tento doplnok schváli."
"error_approval_pending": "Čaká sa, kým správca tento doplnok schváli.",
"error_auto_disabled": "Vypnuté, pretože opakovane zlyhávalo. Zapnite ho znova a skúste to znova."
},
"consent": {
"title": "Povoliť doplnku {name} tieto akcie?",
@@ -4250,7 +4251,8 @@
"cancel": "Zrušiť",
"close": "Zavrieť",
"turn_off": "Vypnúť {name}",
"password_note": "Tento formulár pochádza z doplnku {name}, nie z webmailu. Heslo zadajte, len ak tomuto doplnku dôverujete."
"password_note": "Tento formulár pochádza z doplnku {name}, nie z webmailu. Heslo zadajte, len ak tomuto doplnku dôverujete.",
"toast_auto_disabled": "{name} bol vypnutý, pretože opakovane zlyhával."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Yöneticiniz kullanıcıların yüklediği eklentileri kapattı, bu yüzden bu eklenti çalışmıyor.",
"error_approval_unavailable": "Bu eklenti, onay zorunlu olmadan önce yüklendi. Onaya göndermek için kaldırıp yeniden yükleyin.",
"error_approval_denied": "Yöneticiniz bu eklentiyi onaylamadı.",
"error_approval_pending": "Bu eklentinin yöneticiniz tarafından onaylanması bekleniyor."
"error_approval_pending": "Bu eklentinin yöneticiniz tarafından onaylanması bekleniyor.",
"error_auto_disabled": "Sürekli hata verdiği için kapatıldı. Yeniden denemek için tekrar açın."
},
"consent": {
"title": "{name} eklentisine bu izin verilsin mi?",
@@ -4250,7 +4251,8 @@
"cancel": "İptal",
"close": "Kapat",
"turn_off": "{name} eklentisini kapat",
"password_note": "Bu form web postasından değil, {name} eklentisinden geliyor. Yalnızca bu eklentiye güveniyorsanız parola girin."
"password_note": "Bu form web postasından değil, {name} eklentisinden geliyor. Yalnızca bu eklentiye güveniyorsanız parola girin.",
"toast_auto_disabled": "{name} sürekli hata verdiği için kapatıldı."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "Ваш адміністратор вимкнув плагіни, встановлені користувачами, тому цей плагін не працює.",
"error_approval_unavailable": "Цей плагін встановлено ще до того, як з'явилося схвалення. Видаліть і встановіть його знову, щоб надіслати на схвалення.",
"error_approval_denied": "Ваш адміністратор не схвалив цей плагін.",
"error_approval_pending": "Очікується, що ваш адміністратор схвалить цей плагін."
"error_approval_pending": "Очікується, що ваш адміністратор схвалить цей плагін.",
"error_auto_disabled": "Вимкнено, бо постійно виникали помилки. Увімкніть його знову, щоб спробувати ще раз."
},
"consent": {
"title": "Дозволити плагіну {name} це?",
@@ -4250,7 +4251,8 @@
"cancel": "Скасувати",
"close": "Закрити",
"turn_off": "Вимкнути {name}",
"password_note": "Ця форма від плагіна {name}, а не від вебпошти. Вводьте пароль, лише якщо довіряєте цьому плагіну."
"password_note": "Ця форма від плагіна {name}, а не від вебпошти. Вводьте пароль, лише якщо довіряєте цьому плагіну.",
"toast_auto_disabled": "{name} вимкнено, бо постійно виникали помилки."
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "管理員已關閉使用者安裝的外掛程式,因此此外掛程式不會執行。",
"error_approval_unavailable": "此外掛程式是在需要核准之前安裝的。請移除後重新安裝,以送交核准。",
"error_approval_denied": "管理員未核准此外掛程式。",
"error_approval_pending": "正在等待管理員核准此外掛程式。"
"error_approval_pending": "正在等待管理員核准此外掛程式。",
"error_auto_disabled": "由於反覆出錯已關閉。重新開啟即可再試一次。"
},
"consent": {
"title": "要允許 {name} 執行這些動作嗎?",
@@ -4250,7 +4251,8 @@
"cancel": "取消",
"close": "關閉",
"turn_off": "關閉 {name}",
"password_note": "此表單來自外掛程式 {name},而不是網頁郵件。只有在您信任此外掛程式時才輸入密碼。"
"password_note": "此表單來自外掛程式 {name},而不是網頁郵件。只有在您信任此外掛程式時才輸入密碼。",
"toast_auto_disabled": "{name} 因反覆出錯已被關閉。"
}
},
"global_search": {
+4 -2
View File
@@ -4016,7 +4016,8 @@
"error_user_plugins_off": "管理员已关闭用户安装的插件,因此此插件不会运行。",
"error_approval_unavailable": "此插件是在需要审批之前安装的。请移除后重新安装,以提交审批。",
"error_approval_denied": "管理员未批准此插件。",
"error_approval_pending": "正在等待管理员批准此插件。"
"error_approval_pending": "正在等待管理员批准此插件。",
"error_auto_disabled": "由于反复出错已关闭。重新开启即可再试一次。"
},
"consent": {
"title": "允许 {name} 执行这些操作?",
@@ -4250,7 +4251,8 @@
"cancel": "取消",
"close": "关闭",
"turn_off": "关闭 {name}",
"password_note": "此表单来自插件 {name},而不是网页邮箱。只有在您信任此插件时才输入密码。"
"password_note": "此表单来自插件 {name},而不是网页邮箱。只有在您信任此插件时才输入密码。",
"toast_auto_disabled": "{name} 因反复出错已被关闭。"
}
},
"global_search": {
+19 -4
View File
@@ -207,7 +207,7 @@ export const usePluginStore = create<PluginStoreState>()(
}
// Ensure bridge is wired before loading (may not have run initializePlugins yet)
setPluginStoreAccessor({ setPluginStatus: get().setPluginStatus });
wireLoader(get, set);
set(state => ({
plugins: state.plugins.map(p =>
@@ -283,9 +283,7 @@ export const usePluginStore = create<PluginStoreState>()(
}
// Wire up bridges
setPluginStoreAccessor({
setPluginStatus: get().setPluginStatus,
});
wireLoader(get, set);
setupAutoDisable();
// Keep the sandbox locale in step with the app locale. Set it
// synchronously *before* activation so background instances get the
@@ -355,6 +353,23 @@ export const usePluginStore = create<PluginStoreState>()(
)
);
/** Give the loader its way back into the store. */
function wireLoader(
get: () => PluginStoreState,
set: (partial: Partial<PluginStoreState> | ((state: PluginStoreState) => Partial<PluginStoreState>)) => void,
): void {
setPluginStoreAccessor({
setPluginStatus: get().setPluginStatus,
markAutoDisabled: (id) => set(state => ({
plugins: state.plugins.map(p => (p.id === id
// enabled: false is persisted, so a reload doesn't start the
// failing plugin again behind the user's back.
? { ...p, enabled: false, status: 'error' as PluginStatus, error: 'auto-disabled' }
: p)),
})),
});
}
// ─── Server Plugin Sync ──────────────────────────────────────
interface ServerPluginInfo {