mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
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:
@@ -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()])}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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": "اسم الوسم",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "نام برچسب",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "שם התווית",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ラベル名",
|
||||
|
||||
@@ -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": "태그 이름",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Тагийн нэр",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2036,6 +2036,8 @@
|
||||
"copy_to_folder": "Выбрать папку",
|
||||
"forward_to": "Переслать на адрес электронной почты",
|
||||
"forward_placeholder": "user@пример.рф",
|
||||
"forward_keep_copy": "Сохранить копию",
|
||||
"forward_limit": "Лимит пересылок на одно письмо на этом сервере: {count}. Лишние пересылки пропускаются.",
|
||||
"reject_message": "Сообщение об отклонении",
|
||||
"reject_placeholder": "Ваше письмо было отклонено",
|
||||
"label_name": "Название тега",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ı",
|
||||
|
||||
@@ -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": "Назва мітки",
|
||||
|
||||
@@ -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": "標籤名稱",
|
||||
|
||||
@@ -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": "标签名称",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user