fix: keep filter flags and folder targets when mail is moved

The Sieve generator wrote actions in the order they were picked in the
rule editor. `fileinto` stores the flags set at that moment, so "mark
read", "star" or "add label" placed after a move never reached the
moved message. Flag actions are now emitted first.

Moves and copies also targeted the folder by path only. Renaming the
folder silently sent the rule's mail to Inbox. Rules now store the
folder id and emit `fileinto :mailboxid "<id>" "<path>"` when the
server lists `mailboxid` (declaring `mailbox` too, which Stalwart
requires), with the path as the fallback. Saving a rule refreshes the
path from the id, so existing rules pick up the id on their next save.

Forward actions get a "Keep a copy" option (`redirect :copy`), and the
editor honours the server's `maxNumberRedirects` (1 on Stalwart):
forward is disabled once the limit is used up, with a hint when
existing rules already exceed it, because the server skips extra
redirects without telling anyone.
This commit is contained in:
Linus Rath
2026-09-22 20:09:19 +02:00
parent 198a3c0d61
commit dadfb08f9c
34 changed files with 273 additions and 43 deletions
+95 -27
View File
@@ -23,6 +23,10 @@ import { useKeywordFormat } from "@/hooks/use-keyword-format";
interface FilterRuleModalProps {
rule?: FilterRule;
mailboxes: Mailbox[];
/** Server cap on redirects per message (Sieve `maxNumberRedirects`). */
maxRedirects?: number | null;
/** Forward actions in the other enabled rules, which share that cap. */
otherForwards?: number;
onSave: (rule: FilterRule) => void;
onClose: () => void;
}
@@ -84,6 +88,8 @@ function makeEmptyAction(): FilterAction {
export function FilterRuleModal({
rule,
mailboxes,
maxRedirects,
otherForwards = 0,
onSave,
onClose,
}: FilterRuleModalProps) {
@@ -121,6 +127,20 @@ export function FilterRuleModal({
return { hierarchicalMailboxes: flattenMailboxTree(tree), mailboxPathMap: pathMap };
}, [mailboxes]);
// Rules saved before folder ids were stored only carry the path; resolve it
// so the select shows the folder and the next save adds the id.
const mailboxIdFor = useCallback((action: FilterAction): string => {
if (action.mailboxId) return action.mailboxId;
for (const [id, path] of mailboxPathMap) {
if (path === action.value) return id;
}
return "";
}, [mailboxPathMap]);
const forwardCount = actions.filter((a) => a.type === "forward").length;
const forwardLimit = typeof maxRedirects === "number" && maxRedirects > 0 ? maxRedirects : null;
const forwardOverLimit = forwardLimit !== null && forwardCount + otherForwards > forwardLimit;
const handleSave = useCallback(() => {
const trimmedName = name.trim();
if (!trimmedName) {
@@ -150,9 +170,16 @@ export function FilterRuleModal({
return;
}
const validActions = actions.filter(
(a) => !ACTIONS_WITH_VALUE.has(a.type) || a.value?.trim()
);
const validActions = actions
.map((a) => {
if (!ACTIONS_WITH_MAILBOX.has(a.type)) return a;
// Store the folder id next to the path and refresh the path from it,
// so a renamed folder keeps receiving the rule's mail.
const mailboxId = mailboxIdFor(a);
const path = mailboxId ? mailboxPathMap.get(mailboxId) : undefined;
return mailboxId ? { ...a, mailboxId, value: path ?? a.value } : a;
})
.filter((a) => !ACTIONS_WITH_VALUE.has(a.type) || a.value?.trim());
if (validActions.length === 0) {
toast.error(t("validation_empty_actions"));
return;
@@ -167,7 +194,7 @@ export function FilterRuleModal({
actions: validActions,
stopProcessing,
});
}, [name, matchType, conditions, actions, stopProcessing, rule, onSave, t]);
}, [name, matchType, conditions, actions, stopProcessing, rule, onSave, t, mailboxIdFor, mailboxPathMap]);
const updateCondition = (index: number, updates: Partial<FilterCondition>) => {
setConditions((prev) =>
@@ -213,9 +240,16 @@ export function FilterRuleModal({
if (updates.type && !ACTIONS_WITH_VALUE.has(updates.type)) {
delete updated.value;
}
if (updates.type && !ACTIONS_WITH_MAILBOX.has(updates.type)) {
delete updated.mailboxId;
}
if (updates.type && updates.type !== "forward") {
delete updated.keepCopy;
}
if (updates.type && ACTIONS_WITH_MAILBOX.has(updates.type) && !updated.value) {
const firstMb = hierarchicalMailboxes[0];
updated.value = firstMb ? (mailboxPathMap.get(firstMb.id) || firstMb.name) : "";
if (firstMb) updated.mailboxId = firstMb.id;
}
return updated;
})
@@ -429,36 +463,65 @@ export function FilterRuleModal({
aria-label={t("actions")}
>
{ALL_ACTION_TYPES.map((a) => (
<option key={a} value={a}>
<option
key={a}
value={a}
disabled={
a === "forward" && action.type !== "forward" && forwardLimit !== null &&
forwardCount + otherForwards >= forwardLimit
}
>
{t(`action_types.${a}`)}
</option>
))}
</select>
{ACTIONS_WITH_MAILBOX.has(action.type) && (
<select
value={action.value || ""}
onChange={(e) => updateAction(index, { value: e.target.value })}
className={`${selectClass} flex-1 min-w-[140px]`}
aria-label={t("move_to_folder")}
>
<option value="">{t("move_to_folder")}</option>
{hierarchicalMailboxes.map((mb) => (
<option key={mb.id} value={mailboxPathMap.get(mb.id) || mb.name}>
{"\u00A0".repeat(mb.depth * 3)}{mb.name}
</option>
))}
</select>
)}
{ACTIONS_WITH_MAILBOX.has(action.type) && (() => {
const selectedId = mailboxIdFor(action);
const missing = !!selectedId && !mailboxPathMap.has(selectedId);
return (
<select
value={selectedId}
onChange={(e) => {
const id = e.target.value;
updateAction(index, {
mailboxId: id || undefined,
value: id ? (mailboxPathMap.get(id) ?? "") : "",
});
}}
className={`${selectClass} flex-1 min-w-[140px]`}
aria-label={t("move_to_folder")}
>
<option value="">{t("move_to_folder")}</option>
{missing && <option value={selectedId}>{action.value}</option>}
{hierarchicalMailboxes.map((mb) => (
<option key={mb.id} value={mb.id}>
{"\u00A0".repeat(mb.depth * 3)}{mb.name}
</option>
))}
</select>
);
})()}
{action.type === "forward" && (
<Input
value={action.value || ""}
onChange={(e) => updateAction(index, { value: e.target.value })}
placeholder={t("forward_placeholder")}
type="email"
className="flex-1 min-w-[180px]"
/>
<>
<Input
value={action.value || ""}
onChange={(e) => updateAction(index, { value: e.target.value })}
placeholder={t("forward_placeholder")}
type="email"
className="flex-1 min-w-[180px]"
/>
<label className="flex items-center gap-1.5 text-sm text-foreground">
<input
type="checkbox"
checked={!!action.keepCopy}
onChange={(e) => updateAction(index, { keepCopy: e.target.checked || undefined })}
className="rounded border-input"
/>
{t("forward_keep_copy")}
</label>
</>
)}
{action.type === "reject" && (
@@ -496,6 +559,11 @@ export function FilterRuleModal({
</div>
))}
</div>
{forwardOverLimit && (
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400">
{t("forward_limit", { count: forwardLimit })}
</p>
)}
<button
type="button"
onClick={() => setActions((prev) => [...prev, makeEmptyAction()])}
+5
View File
@@ -174,6 +174,7 @@ export function FilterSettings() {
rawScript,
vacationSettings,
includeVacation,
sieveCapabilities,
selectAccount,
saveFilters,
addRule,
@@ -699,6 +700,10 @@ export function FilterSettings() {
<FilterRuleModal
rule={editingRule}
mailboxes={mailboxes}
maxRedirects={sieveCapabilities?.maxNumberRedirects}
otherForwards={rules
.filter((r) => r.enabled && r.id !== editingRule?.id)
.reduce((n, r) => n + r.actions.filter((a) => a.type === "forward").length, 0)}
onSave={handleSaveRule}
onClose={() => {
setShowRuleModal(false);
+9
View File
@@ -11,6 +11,8 @@ export interface SieveCapabilities {
sieveExtensions: string[];
notificationMethods: string[];
externalLists: string[];
/** RFC 9661: redirects run per message; extra ones are skipped silently. */
maxNumberRedirects?: number | null;
}
export type FilterConditionField =
@@ -53,6 +55,13 @@ export interface FilterCondition {
export interface FilterAction {
type: FilterActionType;
value?: string;
/**
* move/copy: JMAP id of the target folder. `value` keeps the path as the
* fallback; the id keeps the rule working after the folder is renamed.
*/
mailboxId?: string;
/** forward: also keep the message (`redirect :copy`). */
keepCopy?: boolean;
}
export type FilterOrigin = 'bulwark' | 'external' | 'opaque';
+45
View File
@@ -260,6 +260,51 @@ describe('generateScript', () => {
});
});
describe('action order and targets', () => {
it('sets flags before the move so the moved message keeps them', () => {
const script = generateScript([makeRule({
actions: [{ type: 'move', value: 'Archive' }, { type: 'star' }, { type: 'mark_read' }],
})]);
const flagged = script.indexOf('addflag "\\\\Flagged";');
const seen = script.indexOf('addflag "\\\\Seen";');
const move = script.indexOf('fileinto "Archive";');
expect(flagged).toBeGreaterThan(-1);
expect(flagged).toBeLessThan(move);
expect(seen).toBeLessThan(move);
});
it('targets the folder id when the server supports mailboxid', () => {
const rules = [makeRule({ actions: [{ type: 'move', value: 'Work/Old', mailboxId: 'm42' }] })];
const script = generateScript(rules, undefined, { extensions: ['fileinto', 'mailbox', 'mailboxid'] });
expect(script).toContain('fileinto :mailboxid "m42" "Work/Old";');
expect(script).toContain('require ["fileinto", "mailbox", "mailboxid"];');
});
it('falls back to the path without mailboxid support', () => {
const rules = [makeRule({ actions: [{ type: 'copy', value: 'Work', mailboxId: 'm42' }] })];
const script = generateScript(rules, undefined, { extensions: ['fileinto', 'copy'] });
expect(script).toContain('fileinto :copy "Work";');
expect(script).not.toContain('mailboxid');
});
it('keeps a copy of forwarded mail when asked', () => {
const script = generateScript([makeRule({ actions: [{ type: 'forward', value: 'a@b.c', keepCopy: true }] })]);
expect(script).toContain('redirect :copy "a@b.c";');
expect(script).toContain('require ["copy"];');
});
it('parses the new action forms from external scripts', () => {
const parsed = parseScript(
'require ["fileinto", "copy", "mailbox", "mailboxid"];\n' +
'if header :contains "From" "x" { fileinto :copy :mailboxid "m1" "A"; redirect :copy "a@b.c"; }',
);
expect(parsed.rules[0].actions).toEqual([
{ type: 'copy', value: 'A', mailboxId: 'm1' },
{ type: 'forward', value: 'a@b.c', keepCopy: true },
]);
});
});
describe('vacation include', () => {
it('includes the server vacation script before the rules', () => {
const script = generateScript([makeRule()], undefined, { includeVacation: true });
+41 -11
View File
@@ -91,15 +91,31 @@ function generateCondition(condition: FilterCondition): string {
}
}
function generateActions(actions: FilterAction[]): string[] {
return actions.map(action => {
const FLAG_ACTIONS = new Set<FilterAction['type']>(['mark_read', 'star', 'add_label']);
function fileintoTarget(action: FilterAction, useMailboxId: boolean): string {
const path = `"${escapeString(action.value || '')}"`;
// RFC 9042: the path is the fallback when the id no longer exists.
return useMailboxId && action.mailboxId
? `:mailboxid "${escapeString(action.mailboxId)}" ${path}`
: path;
}
function generateActions(actions: FilterAction[], useMailboxId: boolean): string[] {
// fileinto (and the implicit keep) store the flags set at that moment, so a
// flag action placed after a move in the UI would be lost. Set flags first.
const ordered = [
...actions.filter(a => FLAG_ACTIONS.has(a.type)),
...actions.filter(a => !FLAG_ACTIONS.has(a.type)),
];
return ordered.map(action => {
switch (action.type) {
case 'move':
return `fileinto "${escapeString(action.value || '')}";`;
return `fileinto ${fileintoTarget(action, useMailboxId)};`;
case 'copy':
return `fileinto :copy "${escapeString(action.value || '')}";`;
return `fileinto :copy ${fileintoTarget(action, useMailboxId)};`;
case 'forward':
return `redirect "${escapeString(action.value || '')}";`;
return `redirect ${action.keepCopy ? ':copy ' : ''}"${escapeString(action.value || '')}";`;
case 'mark_read':
return 'addflag "\\\\Seen";';
case 'star':
@@ -122,7 +138,7 @@ function generateActions(actions: FilterAction[]): string[] {
});
}
function computeRequires(rules: FilterRule[], vacation?: VacationSieveConfig): string[] {
function computeRequires(rules: FilterRule[], vacation: VacationSieveConfig | undefined, useMailboxId: boolean): string[] {
const extensions = new Set<string>();
const enabledRules = rules.filter(r => r.enabled);
@@ -138,12 +154,20 @@ function computeRequires(rules: FilterRule[], vacation?: VacationSieveConfig): s
for (const action of rule.actions) {
switch (action.type) {
case 'move':
case 'copy':
extensions.add('fileinto');
if (action.type === 'copy') extensions.add('copy');
if (useMailboxId && action.mailboxId) {
// Stalwart rejects :mailboxid unless "mailbox" is declared too.
extensions.add('mailbox');
extensions.add('mailboxid');
}
break;
case 'keep':
extensions.add('fileinto');
break;
case 'copy':
extensions.add('fileinto');
extensions.add('copy');
case 'forward':
if (action.keepCopy) extensions.add('copy');
break;
case 'mark_read':
case 'star':
@@ -185,6 +209,11 @@ export interface GenerateOptions {
* while the filters stay active.
*/
includeVacation?: boolean;
/**
* The server's `sieveExtensions`. Folder moves use `:mailboxid` when it
* lists "mailboxid"; without it they target the folder path only.
*/
extensions?: string[];
}
/** Name of the script a server builds for VacationResponse (RFC 9661). */
@@ -221,7 +250,8 @@ export function generateScript(
lines.push('@metadata:end */');
lines.push('');
const bulwarkRequires = computeRequires(bulwarkRules, vacation);
const useMailboxId = options.extensions?.includes('mailboxid') ?? false;
const bulwarkRequires = computeRequires(bulwarkRules, vacation, useMailboxId);
if (options.includeVacation) bulwarkRequires.push('include');
const externalRequires = options.externalRequires ?? [];
const allRequires = [...new Set([...bulwarkRequires, ...externalRequires])].sort();
@@ -272,7 +302,7 @@ export function generateScript(
conditionStr = `${wrapper}(${conditions.join(', ')})`;
}
const actionLines = generateActions(rule.actions);
const actionLines = generateActions(rule.actions, useMailboxId);
if (rule.stopProcessing) {
const lastAction = rule.actions[rule.actions.length - 1];
+15 -2
View File
@@ -525,8 +525,21 @@ function parseAction(raw: string): FilterAction | null {
m = /^fileinto\s+"((?:[^"\\]|\\.)*)"$/.exec(s);
if (m) return { type: 'move', value: unescapeSieveString(m[1]) };
m = /^redirect\s+"((?:[^"\\]|\\.)*)"$/.exec(s);
if (m) return { type: 'forward', value: unescapeSieveString(m[1]) };
m = /^fileinto\s+(:copy\s+)?:mailboxid\s+"((?:[^"\\]|\\.)*)"\s+"((?:[^"\\]|\\.)*)"$/.exec(s);
if (m) {
return {
type: m[1] ? 'copy' : 'move',
value: unescapeSieveString(m[3]),
mailboxId: unescapeSieveString(m[2]),
};
}
m = /^redirect\s+(:copy\s+)?"((?:[^"\\]|\\.)*)"$/.exec(s);
if (m) {
const action: FilterAction = { type: 'forward', value: unescapeSieveString(m[2]) };
if (m[1]) action.keepCopy = true;
return action;
}
m = /^addflag\s+"((?:[^"\\]|\\.)*)"$/.exec(s);
if (m) {
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "اختر المجلد",
"forward_to": "إعادة التوجيه إلى عنوان بريد إلكتروني",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "الاحتفاظ بنسخة",
"forward_limit": "حد إعادة التوجيه لكل رسالة على هذا الخادم: {count}. يتم تخطي عمليات إعادة التوجيه الإضافية.",
"reject_message": "رسالة الرفض",
"reject_placeholder": "تم رفض رسالتك",
"label_name": "اسم الوسم",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "Selecciona una carpeta",
"forward_to": "Reenvia a l'adreça electrònica",
"forward_placeholder": "correu@exemple.com",
"forward_keep_copy": "Conserva'n una còpia",
"forward_limit": "Límit de reenviaments per missatge en aquest servidor: {count}. Els reenviaments addicionals s'ometen.",
"reject_message": "Missatge de rebuig",
"reject_placeholder": "El vostre correu ha estat rebutjat",
"label_name": "Nom de l'etiqueta",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Vyberte složku",
"forward_to": "Přeposlat na e-mailovou adresu",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Ponechat kopii",
"forward_limit": "Limit přeposlání na zprávu na tomto serveru: {count}. Další přeposlání se přeskočí.",
"reject_message": "Zpráva o odmítnutí",
"reject_placeholder": "Váš e-mail byl odmítnut",
"label_name": "Název štítku",
+2
View File
@@ -2039,6 +2039,8 @@
"copy_to_folder": "Vælg mappe",
"forward_to": "Videresend til e-mailadresse",
"forward_placeholder": "email@eksempel.dk",
"forward_keep_copy": "Behold en kopi",
"forward_limit": "Grænse for videresendelser pr. besked på denne server: {count}. Yderligere videresendelser springes over.",
"reject_message": "Afvisningsbesked",
"reject_placeholder": "Din e-mail er blevet afvist",
"label_name": "Tag-navn",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Ordner auswählen",
"forward_to": "An E-Mail-Adresse weiterleiten",
"forward_placeholder": "email@beispiel.de",
"forward_keep_copy": "Kopie behalten",
"forward_limit": "Weiterleitungen pro Nachricht auf diesem Server: höchstens {count}. Weitere werden übersprungen.",
"reject_message": "Ablehnungsnachricht",
"reject_placeholder": "Ihre E-Mail wurde abgelehnt",
"label_name": "Label-Name",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "Select folder",
"forward_to": "Forward to email address",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Keep a copy",
"forward_limit": "Forward limit per message on this server: {count}. Extra forwards are skipped.",
"reject_message": "Rejection message",
"reject_placeholder": "Your email has been rejected",
"label_name": "Tag name",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Seleccionar carpeta",
"forward_to": "Reenviar a dirección de correo",
"forward_placeholder": "correo@ejemplo.com",
"forward_keep_copy": "Conservar una copia",
"forward_limit": "Límite de reenvíos por mensaje en este servidor: {count}. Los reenvíos adicionales se omiten.",
"reject_message": "Mensaje de rechazo",
"reject_placeholder": "Su correo ha sido rechazado",
"label_name": "Nombre de la etiqueta",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "انتخاب پوشه",
"forward_to": "ارسال به آدرس ایمیل",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "نگه‌داشتن یک نسخه",
"forward_limit": "سقف بازارسال برای هر پیام در این سرور: {count}. بازارسال‌های اضافه نادیده گرفته می‌شوند.",
"reject_message": "پیام رد",
"reject_placeholder": "ایمیل شما رد شد",
"label_name": "نام برچسب",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Sélectionner le dossier",
"forward_to": "Transférer à l'adresse e-mail",
"forward_placeholder": "email@exemple.com",
"forward_keep_copy": "Garder une copie",
"forward_limit": "Limite de transferts par message sur ce serveur : {count}. Les transferts en trop sont ignorés.",
"reject_message": "Message de rejet",
"reject_placeholder": "Votre e-mail a été rejeté",
"label_name": "Nom de l'étiquette",
+2
View File
@@ -1956,6 +1956,8 @@
"copy_to_folder": "בחר תיקייה",
"forward_to": "העבר לכתובת המייל",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "שמור עותק",
"forward_limit": "מגבלת העברות לכל הודעה בשרת זה: {count}. העברות נוספות מדולגות.",
"reject_message": "הודעת דחייה",
"reject_placeholder": "האימייל שלך נדחה",
"label_name": "שם התווית",
+2
View File
@@ -2039,6 +2039,8 @@
"copy_to_folder": "Mappa kiválasztása",
"forward_to": "Továbbítás e-mail címre",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Másolat megtartása",
"forward_limit": "Továbbítási korlát üzenetenként ezen a kiszolgálón: {count}. A további továbbítások kimaradnak.",
"reject_message": "Elutasító üzenet",
"reject_placeholder": "Az e-mailed elutasításra került",
"label_name": "Címke neve",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Seleziona cartella",
"forward_to": "Inoltra a indirizzo email",
"forward_placeholder": "email@esempio.com",
"forward_keep_copy": "Conserva una copia",
"forward_limit": "Limite di inoltri per messaggio su questo server: {count}. Gli inoltri in eccesso vengono ignorati.",
"reject_message": "Messaggio di rifiuto",
"reject_placeholder": "La tua email è stata rifiutata",
"label_name": "Nome dell'etichetta",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "フォルダーを選択",
"forward_to": "転送先メールアドレス",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "コピーを残す",
"forward_limit": "このサーバーでの1通あたりの転送上限: {count}。それを超える転送はスキップされます。",
"reject_message": "拒否メッセージ",
"reject_placeholder": "あなたのメールは拒否されました",
"label_name": "ラベル名",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "폴더 선택",
"forward_to": "전달할 이메일 주소",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "사본 보관",
"forward_limit": "이 서버의 메시지당 전달 한도: {count}. 초과된 전달은 건너뜁니다.",
"reject_message": "거부 메시지",
"reject_placeholder": "메일 수신이 거부되었습니다",
"label_name": "태그 이름",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Izvēlēties mapi",
"forward_to": "Pārsūtīt uz e-pastu",
"forward_placeholder": "lietotajs@piemers.lv",
"forward_keep_copy": "Saglabāt kopiju",
"forward_limit": "Pārsūtīšanas ierobežojums vienai ziņai šajā serverī: {count}. Papildu pārsūtīšanas tiek izlaistas.",
"reject_message": "Noraidīšanas ziņojums",
"reject_placeholder": "Jūsu e-pasts tika noraidīts",
"label_name": "Taga nosaukums",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "Фолдер сонгох",
"forward_to": "Имэйл хаяг руу шилжүүлэх",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Хуулбар хадгалах",
"forward_limit": "Энэ сервер дээрх нэг захидлын дамжуулах хязгаар: {count}. Илүү дамжуулалтыг алгасна.",
"reject_message": "Татгалзсан мессеж",
"reject_placeholder": "Таны имэйл татгалзсан байна",
"label_name": "Тагийн нэр",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "Velg mappe",
"forward_to": "Videresend til e-postadresse",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Behold en kopi",
"forward_limit": "Grense for videresendinger per melding på denne serveren: {count}. Flere videresendinger hoppes over.",
"reject_message": "Avvisningsmelding",
"reject_placeholder": "Meldingen din er avvist",
"label_name": "Etikettnavn",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Map selecteren",
"forward_to": "Doorsturen naar e-mailadres",
"forward_placeholder": "email@voorbeeld.nl",
"forward_keep_copy": "Kopie bewaren",
"forward_limit": "Maximaal aantal doorsturingen per bericht op deze server: {count}. Extra doorsturingen worden overgeslagen.",
"reject_message": "Afwijzingsbericht",
"reject_placeholder": "Uw e-mail is afgewezen",
"label_name": "Labelnaam",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Wybierz folder",
"forward_to": "Prześlij dalej na adres e-mail",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Zachowaj kopię",
"forward_limit": "Limit przekazań na wiadomość na tym serwerze: {count}. Dodatkowe przekazania są pomijane.",
"reject_message": "Wiadomość odrzucenia",
"reject_placeholder": "Twoja wiadomość e-mail została odrzucona",
"label_name": "Nazwa etykiety",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Selecionar pasta",
"forward_to": "Encaminhar para endereço de e-mail",
"forward_placeholder": "email@exemplo.com",
"forward_keep_copy": "Manter uma cópia",
"forward_limit": "Limite de reencaminhamentos por mensagem neste servidor: {count}. Os restantes são ignorados.",
"reject_message": "Mensagem de rejeição",
"reject_placeholder": "Seu e-mail foi rejeitado",
"label_name": "Nome da etiqueta",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "Selectează folderul",
"forward_to": "Redirecționează către adresa de e-mail",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Păstrează o copie",
"forward_limit": "Limita de redirecționări per mesaj pe acest server: {count}. Redirecționările în plus sunt omise.",
"reject_message": "Mesaj de respingere",
"reject_placeholder": "E-mailul dvs. a fost respins",
"label_name": "Numele etichetei",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Выбрать папку",
"forward_to": "Переслать на адрес электронной почты",
"forward_placeholder": "user@пример.рф",
"forward_keep_copy": "Сохранить копию",
"forward_limit": "Лимит пересылок на одно письмо на этом сервере: {count}. Лишние пересылки пропускаются.",
"reject_message": "Сообщение об отклонении",
"reject_placeholder": "Ваше письмо было отклонено",
"label_name": "Название тега",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "Vybrať priečinok",
"forward_to": "Preposlať na e-mailovú adresu",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Ponechať kópiu",
"forward_limit": "Limit preposlaní na správu na tomto serveri: {count}. Ďalšie preposlania sa preskočia.",
"reject_message": "Správa o odmietnutí",
"reject_placeholder": "Váš e-mail bol odmietnutý",
"label_name": "Názov štítku",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Klasör seçin",
"forward_to": "E-posta adresine ilet",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Bir kopyasını sakla",
"forward_limit": "Bu sunucuda ileti başına yönlendirme sınırı: {count}. Fazla yönlendirmeler atlanır.",
"reject_message": "Reddetme iletisi",
"reject_placeholder": "E-postanız reddedildi",
"label_name": "Etiket adı",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "Виберіть папку",
"forward_to": "Переслати на електронну адресу",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "Зберегти копію",
"forward_limit": "Ліміт пересилань на один лист на цьому сервері: {count}. Зайві пересилання пропускаються.",
"reject_message": "Повідомлення про відмову",
"reject_placeholder": "Ваш лист було відхилено",
"label_name": "Назва мітки",
+2
View File
@@ -2043,6 +2043,8 @@
"copy_to_folder": "選取信件匣",
"forward_to": "轉寄至電子郵件地址",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "保留副本",
"forward_limit": "此伺服器每封郵件的轉寄上限:{count}。多餘的轉寄將被略過。",
"reject_message": "拒絕留言",
"reject_placeholder": "您的郵件已被拒絕",
"label_name": "標籤名稱",
+2
View File
@@ -2036,6 +2036,8 @@
"copy_to_folder": "选择文件夹",
"forward_to": "转发至邮箱地址",
"forward_placeholder": "email@example.com",
"forward_keep_copy": "保留副本",
"forward_limit": "此服务器每封邮件的转发上限:{count}。多余的转发将被跳过。",
"reject_message": "拒绝留言",
"reject_placeholder": "您的邮件已被拒绝",
"label_name": "标签名称",
+9 -3
View File
@@ -156,14 +156,18 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
try {
const {
isOpaque, rawScript, rules, activeScriptId, vacationSettings, externalRequires, includeVacation,
selectedAccountId,
selectedAccountId, sieveCapabilities,
} = get();
let content: string;
if (isOpaque) {
content = rawScript;
} else {
content = generateScript(rules, vacationSettings || undefined, { externalRequires, includeVacation });
content = generateScript(rules, vacationSettings || undefined, {
externalRequires,
includeVacation,
extensions: sieveCapabilities?.sieveExtensions,
});
}
content = await applyScriptTransforms(content, selectedAccountId || null);
@@ -316,7 +320,8 @@ export async function syncVacationWithFilters(
accountId?: string,
): Promise<void> {
const sieveAccountId = accountId || client.getSieveAccountId();
if (enabled && !supportsInclude(client.getSieveCapabilities(sieveAccountId))) return;
const capabilities = client.getSieveCapabilities(sieveAccountId);
if (enabled && !supportsInclude(capabilities)) return;
const { vacationScript, target, parsed } = await loadManagedScript(client, sieveAccountId);
if (!target || !parsed) return;
@@ -332,6 +337,7 @@ export async function syncVacationWithFilters(
generateScript(parsed.rules, parsed.vacation, {
externalRequires: parsed.externalRequires,
includeVacation: enabled,
extensions: capabilities?.sieveExtensions,
}),
sieveAccountId,
);