Feature: extended filter rules — attachment field + multi-value conditions

Adds an "Attachment" condition field (is present / of type <ext>) backed by
the RFC 5703 Sieve mime extension, matching the filename in both
Content-Disposition and Content-Type headers so real-world senders that only
put the name in Content-Type (Microsoft SMTPSVC, etc.) are caught. Users type
extensions (pdf, doc) not MIME types.

Also makes each text condition accept comma-separated multiple values emitted
as a Sieve string list (OR within the condition), so "(domain1 OR domain2)
AND attachment pdf/xml" is expressible in one rule. value is now string |
string[] (single-value rules stay strings -> backward compatible). New filter
locale keys in all 17 locales.
This commit is contained in:
dealerweb
2026-05-30 15:45:33 +02:00
committed by Linus Rath
parent 229992853b
commit 8353b28b33
22 changed files with 431 additions and 64 deletions
+27 -2
View File
@@ -36,7 +36,17 @@ function RuleSummary({ rule }: { rule: FilterRule }) {
const conditions = rule.conditions.slice(0, 2).map((c) => {
const field = t(`condition_fields.${c.field}`);
const comparator = t(`comparators.${c.comparator}`);
return `${field} ${comparator} "${c.value}"`;
// has_any is a no-value test ("attachment is present"); appending
// `""` would look broken in the summary line.
if (c.field === "attachment" && c.comparator === "has_any") {
return `${field} ${comparator}`;
}
// Multi-value conditions render as "a" / "b" / "c" with the locale's
// OR-glue between items so the line still reads as natural language.
const valueStr = Array.isArray(c.value)
? c.value.map((v) => `"${v}"`).join(` ${t("or")} `)
: `"${c.value}"`;
return `${field} ${comparator} ${valueStr}`;
});
const joiner = rule.matchType === "all" ? t("and") : t("or");
@@ -97,7 +107,22 @@ function VisualRuleSummary({ rule }: { rule: FilterRule }) {
<span className="inline-flex items-baseline gap-1 px-1.5 py-px rounded-sm bg-muted/60 text-foreground">
<span className="font-medium text-blue-600 dark:text-blue-400">{field}</span>
<span className="text-muted-foreground">{comparator}</span>
<span className="text-foreground">{c.value}</span>
{!(c.field === "attachment" && c.comparator === "has_any") && (
<span className="text-foreground">
{Array.isArray(c.value)
? c.value.map((v, k) => (
<span key={k}>
{k > 0 && (
<span className="text-muted-foreground/70 italic mx-0.5">
{t("or")}
</span>
)}
{v}
</span>
))
: <>{c.value}</>}
</span>
)}
</span>
</span>
);