From 6241ed61d66db1b328b95e3cdc0465bca8401192 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:14:42 +0200 Subject: [PATCH] fix: stop folder rules from pulling spam out of Junk Stalwart only moves spam to Junk when nothing else decided where the message goes. Any Bulwark rule that moved or copied mail to a folder therefore delivered spam there too, e.g. a "subject contains invoice" rule collected phishing. From 0.16.22 this also applies to the "Keep" allow-list action, which older versions still send to Junk. Move and copy rules now add `not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50"` to their condition when the server supports `spamtestplus` and `relational`. 50 % is Stalwart's spam threshold on the graded scale (0.16.19+), and older versions report 0 or 100. A new per-rule option "Also move messages marked as spam" drops the guard, and "Keep" rules are left unguarded as the explicit allow-list. --- components/filters/filter-rule-modal.tsx | 19 ++++++++++- lib/jmap/sieve-types.ts | 5 +++ lib/sieve/__tests__/generator.test.ts | 34 +++++++++++++++++++ lib/sieve/generator.ts | 42 ++++++++++++++++++++++-- locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nb/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh-TW/common.json | 1 + locales/zh/common.json | 1 + 31 files changed, 123 insertions(+), 4 deletions(-) diff --git a/components/filters/filter-rule-modal.tsx b/components/filters/filter-rule-modal.tsx index 564f9e8b3..7c673e1aa 100644 --- a/components/filters/filter-rule-modal.tsx +++ b/components/filters/filter-rule-modal.tsx @@ -107,6 +107,7 @@ export function FilterRuleModal({ rule?.actions.length ? [...rule.actions] : [makeEmptyAction()] ); const [stopProcessing, setStopProcessing] = useState(rule?.stopProcessing ?? false); + const [includeSpam, setIncludeSpam] = useState(rule?.includeSpam ?? false); const modalRef = useFocusTrap({ isActive: true, onEscape: onClose }); @@ -193,8 +194,9 @@ export function FilterRuleModal({ conditions: validConditions, actions: validActions, stopProcessing, + ...(includeSpam && validActions.some((a) => ACTIONS_WITH_MAILBOX.has(a.type)) ? { includeSpam: true } : {}), }); - }, [name, matchType, conditions, actions, stopProcessing, rule, onSave, t, mailboxIdFor, mailboxPathMap]); + }, [name, matchType, conditions, actions, stopProcessing, includeSpam, rule, onSave, t, mailboxIdFor, mailboxPathMap]); const updateCondition = (index: number, updates: Partial) => { setConditions((prev) => @@ -586,6 +588,21 @@ export function FilterRuleModal({ {t("stop_processing")} + + {actions.some((a) => ACTIONS_WITH_MAILBOX.has(a.type)) && ( +
+ setIncludeSpam(e.target.checked)} + className="rounded border-input" + /> + +
+ )}
diff --git a/lib/jmap/sieve-types.ts b/lib/jmap/sieve-types.ts index c2427f429..d2e78c7fb 100644 --- a/lib/jmap/sieve-types.ts +++ b/lib/jmap/sieve-types.ts @@ -74,6 +74,11 @@ export interface FilterRule { conditions: FilterCondition[]; actions: FilterAction[]; stopProcessing: boolean; + /** + * Also move/copy messages the server marked as spam. Off by default, so a + * folder rule does not pull spam out of Junk. + */ + includeSpam?: boolean; origin?: FilterOrigin; originLabel?: string; rawBlock?: string; diff --git a/lib/sieve/__tests__/generator.test.ts b/lib/sieve/__tests__/generator.test.ts index 665730b7c..6ad885628 100644 --- a/lib/sieve/__tests__/generator.test.ts +++ b/lib/sieve/__tests__/generator.test.ts @@ -305,6 +305,40 @@ describe('generateScript', () => { }); }); + describe('spam guard', () => { + const exts = ['fileinto', 'relational', 'spamtest', 'spamtestplus']; + const guard = 'not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50"'; + + it('keeps spam out of folder moves', () => { + const script = generateScript([makeRule()], undefined, { extensions: exts }); + expect(script).toContain(`if allof(header :contains "From" "test@example.com", ${guard}) {`); + expect(script).toContain('require ["comparator-i;ascii-numeric", "fileinto", "relational", "spamtestplus"];'); + }); + + it('extends an allof and wraps an anyof', () => { + const conditions = [ + { field: 'from' as const, comparator: 'contains' as const, value: 'a' }, + { field: 'subject' as const, comparator: 'contains' as const, value: 'b' }, + ]; + const all = generateScript([makeRule({ conditions })], undefined, { extensions: exts }); + expect(all).toContain(`if allof(header :contains "From" "a", header :contains "Subject" "b", ${guard}) {`); + const any = generateScript([makeRule({ conditions, matchType: 'any' })], undefined, { extensions: exts }); + expect(any).toContain(`if allof(anyof(header :contains "From" "a", header :contains "Subject" "b"), ${guard}) {`); + }); + + it('leaves keep rules, opted-in rules and servers without spamtestplus alone', () => { + const keep = makeRule({ actions: [{ type: 'keep' }] }); + const optIn = makeRule({ id: 'r2', name: 'Opt in', includeSpam: true }); + expect(generateScript([keep, optIn], undefined, { extensions: exts })).not.toContain('spamtest'); + expect(generateScript([makeRule()], undefined, { extensions: ['fileinto'] })).not.toContain('spamtest'); + }); + + it('round-trips the opt-in', () => { + const parsed = parseScript(generateScript([makeRule({ includeSpam: true })], undefined, { extensions: exts })); + expect(parsed.rules[0].includeSpam).toBe(true); + }); + }); + describe('vacation include', () => { it('includes the server vacation script before the rules', () => { const script = generateScript([makeRule()], undefined, { includeVacation: true }); diff --git a/lib/sieve/generator.ts b/lib/sieve/generator.ts index 469ad5c23..ae77d849c 100644 --- a/lib/sieve/generator.ts +++ b/lib/sieve/generator.ts @@ -130,7 +130,8 @@ function generateActions(actions: FilterAction[], useMailboxId: boolean): string // A bare `keep;` delivers to the "default place", which on Stalwart is // Junk for a message its spam filter has already classified, so an // allow-list rule made with "Keep" would change nothing. An explicit - // `fileinto "INBOX"` is honored over the spam verdict (#1027). + // `fileinto "INBOX"` is honored over the spam verdict (#1027), but + // only from Stalwart 0.16.22; older versions still move it to Junk. return 'fileinto "INBOX";'; case 'stop': return 'stop;'; @@ -138,7 +139,32 @@ function generateActions(actions: FilterAction[], useMailboxId: boolean): string }); } -function computeRequires(rules: FilterRule[], vacation: VacationSieveConfig | undefined, useMailboxId: boolean): string[] { +// Stalwart maps its spam verdict to 100 % (0.16.0) or to a graded value where +// 50 % is the spam threshold (0.16.19+), so ">= 50" matches "is spam" on both. +const SPAM_GUARD = 'not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50"'; +const SPAM_GUARD_REQUIRES = ['spamtestplus', 'relational', 'comparator-i;ascii-numeric']; + +function supportsSpamGuard(extensions: string[] | undefined): boolean { + return ['spamtestplus', 'relational'].every(e => extensions?.includes(e)); +} + +/** + * A folder move takes a message out of the Junk folder the server would have + * put it in, so move/copy rules skip spam unless the rule opts in. "Keep" + * rules are allow-lists and stay unguarded. + */ +function needsSpamGuard(rule: FilterRule, extensions: string[] | undefined): boolean { + return !rule.includeSpam && + rule.actions.some(a => a.type === 'move' || a.type === 'copy') && + supportsSpamGuard(extensions); +} + +function computeRequires( + rules: FilterRule[], + vacation: VacationSieveConfig | undefined, + useMailboxId: boolean, + serverExtensions: string[] | undefined, +): string[] { const extensions = new Set(); const enabledRules = rules.filter(r => r.enabled); @@ -147,6 +173,9 @@ function computeRequires(rules: FilterRule[], vacation: VacationSieveConfig | un } for (const rule of enabledRules) { + if (needsSpamGuard(rule, serverExtensions)) { + for (const e of SPAM_GUARD_REQUIRES) extensions.add(e); + } for (const condition of rule.conditions) { if (condition.field === 'body') extensions.add('body'); if (condition.field === 'attachment') extensions.add('mime'); @@ -193,6 +222,7 @@ function stripRuleForMetadata(r: FilterRule): Omit 1 && rule.matchType === 'all' + ? `allof(${conditions.join(', ')}, ${SPAM_GUARD})` + : `allof(${conditionStr}, ${SPAM_GUARD})`; + } + const actionLines = generateActions(rule.actions, useMailboxId); if (rule.stopProcessing) { diff --git a/locales/ar/common.json b/locales/ar/common.json index b8a34d45e..a6c736d7b 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -2002,6 +2002,7 @@ "actions": "الإجراءات", "add_action": "إضافة إجراء", "stop_processing": "إيقاف معالجة القواعد التالية", + "include_spam": "نقل الرسائل المصنفة كرسائل مزعجة أيضًا", "attachment_type_placeholder": "مثال: pdf, doc, jpg", "value_placeholder_multi": "القيمة (عدة قيم مفصولة بفواصل)", "condition_fields": { diff --git a/locales/ca/common.json b/locales/ca/common.json index 064c1d599..3c1e7c7c7 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -2002,6 +2002,7 @@ "actions": "Accions", "add_action": "Afegeix una acció", "stop_processing": "Atura el processament de les regles següents", + "include_spam": "Mou també els missatges marcats com a correu brossa", "attachment_type_placeholder": "p. ex. pdf, doc, jpg", "value_placeholder_multi": "Valor (múltiples separats per comes)", "condition_fields": { diff --git a/locales/cs/common.json b/locales/cs/common.json index 5215bb087..bf994926a 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -1995,6 +1995,7 @@ "actions": "Akce", "add_action": "Přidat akci", "stop_processing": "Zastavit zpracování dalších pravidel", + "include_spam": "Přesouvat i zprávy označené jako spam", "attachment_type_placeholder": "např. pdf, doc, jpg", "value_placeholder_multi": "Hodnota (více oddělených čárkami)", "condition_fields": { diff --git a/locales/da/common.json b/locales/da/common.json index 85f6d26e7..f04ede91b 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -1998,6 +1998,7 @@ "actions": "Handlinger", "add_action": "Tilføj handling", "stop_processing": "Stop behandling af efterfølgende regler", + "include_spam": "Flyt også beskeder, der er markeret som spam", "attachment_type_placeholder": "f.eks. pdf, doc, jpg", "value_placeholder_multi": "Værdi (flere adskilt med komma)", "condition_fields": { diff --git a/locales/de/common.json b/locales/de/common.json index c2e5049a1..0dbed8760 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -1995,6 +1995,7 @@ "actions": "Aktionen", "add_action": "Aktion hinzufügen", "stop_processing": "Verarbeitung nachfolgender Regeln stoppen", + "include_spam": "Auch als Spam markierte Nachrichten verschieben", "attachment_type_placeholder": "z.B. pdf, doc, jpg", "value_placeholder_multi": "Wert (mehrere mit Komma trennen)", "condition_fields": { diff --git a/locales/en/common.json b/locales/en/common.json index ebe06980e..851a0e4fd 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2002,6 +2002,7 @@ "actions": "Actions", "add_action": "Add Action", "stop_processing": "Stop processing subsequent rules", + "include_spam": "Also move messages marked as spam", "attachment_type_placeholder": "e.g. pdf, doc, jpg", "value_placeholder_multi": "Value (multiple separated by commas)", "condition_fields": { diff --git a/locales/es/common.json b/locales/es/common.json index a29bba8a6..aa5a72a05 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -1995,6 +1995,7 @@ "actions": "Acciones", "add_action": "Agregar acción", "stop_processing": "Detener el procesamiento de reglas posteriores", + "include_spam": "Mover también los mensajes marcados como spam", "attachment_type_placeholder": "p. ej. pdf, doc, jpg", "value_placeholder_multi": "Valor (varios separados por comas)", "condition_fields": { diff --git a/locales/fa/common.json b/locales/fa/common.json index 6c334ae40..40a96f9c8 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -2002,6 +2002,7 @@ "actions": "عملیات‌ها", "add_action": "افزودن عملیات", "stop_processing": "توقف پردازش قوانین بعدی", + "include_spam": "پیام‌های علامت‌خورده به‌عنوان هرزنامه هم منتقل شوند", "attachment_type_placeholder": "مثال: pdf, doc, jpg", "value_placeholder_multi": "مقدار (چندتا با کاما جدا شوند)", "condition_fields": { diff --git a/locales/fr/common.json b/locales/fr/common.json index 906b53369..6fd321684 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -1995,6 +1995,7 @@ "actions": "Actions", "add_action": "Ajouter une action", "stop_processing": "Arrêter le traitement des règles suivantes", + "include_spam": "Déplacer aussi les messages marqués comme spam", "attachment_type_placeholder": "p. ex. pdf, doc, jpg", "value_placeholder_multi": "Valeur (plusieurs séparées par des virgules)", "condition_fields": { diff --git a/locales/he/common.json b/locales/he/common.json index 7c342d3e7..27d3d8741 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -1917,6 +1917,7 @@ "actions": "פעולות", "add_action": "הוסף פעולה", "stop_processing": "הפסק לעבד את הכללים הבאים", + "include_spam": "להעביר גם הודעות שסומנו כדואר זבל", "condition_fields": { "from": "מ", "to": "ל", diff --git a/locales/hu/common.json b/locales/hu/common.json index 853ad2c7b..78bc4e487 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -1998,6 +1998,7 @@ "actions": "Műveletek", "add_action": "Művelet hozzáadása", "stop_processing": "További szabályok feldolgozásának leállítása", + "include_spam": "A levélszemétnek jelölt üzenetek áthelyezése is", "attachment_type_placeholder": "pl. pdf, doc, jpg", "value_placeholder_multi": "Érték (több érték vesszővel elválasztva)", "condition_fields": { diff --git a/locales/it/common.json b/locales/it/common.json index 47d793bf5..5fe5dd088 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -1995,6 +1995,7 @@ "actions": "Azioni", "add_action": "Aggiungi azione", "stop_processing": "Interrompere l'elaborazione delle regole successive", + "include_spam": "Sposta anche i messaggi contrassegnati come spam", "attachment_type_placeholder": "es. pdf, doc, jpg", "value_placeholder_multi": "Valore (più valori separati da virgole)", "condition_fields": { diff --git a/locales/ja/common.json b/locales/ja/common.json index c378f370e..83658b1a4 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1995,6 +1995,7 @@ "actions": "アクション", "add_action": "アクションを追加", "stop_processing": "以降のルールの処理を停止", + "include_spam": "迷惑メールと判定されたメッセージも移動する", "attachment_type_placeholder": "例: pdf, doc, jpg", "value_placeholder_multi": "値(カンマで複数指定可)", "condition_fields": { diff --git a/locales/ko/common.json b/locales/ko/common.json index 3521b95f6..034de1970 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -1995,6 +1995,7 @@ "actions": "동작", "add_action": "동작 추가", "stop_processing": "이후 규칙 무시하기", + "include_spam": "스팸으로 표시된 메시지도 이동", "attachment_type_placeholder": "예: pdf, doc, jpg", "value_placeholder_multi": "값 (쉼표로 여러 개 구분)", "condition_fields": { diff --git a/locales/lv/common.json b/locales/lv/common.json index ffefe303c..4e86b9867 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -1995,6 +1995,7 @@ "actions": "Darbības", "add_action": "Pievienot darbību", "stop_processing": "Pārtraukt nākamo noteikumu apstrādi", + "include_spam": "Pārvietot arī ziņas, kas atzīmētas kā mēstules", "attachment_type_placeholder": "piem. pdf, doc, jpg", "value_placeholder_multi": "Vērtība (vairākas atdalītas ar komatu)", "condition_fields": { diff --git a/locales/mn/common.json b/locales/mn/common.json index 499a78052..b5b949591 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -2002,6 +2002,7 @@ "actions": "Үйлдэл", "add_action": "Үйлдэл нэмэх", "stop_processing": "Дараагийн дүрмийг боловсруулахаа зогсоох", + "include_spam": "Спам гэж тэмдэглэсэн захидлыг мөн зөөх", "attachment_type_placeholder": "жишээ нь pdf, doc, jpg", "value_placeholder_multi": "Утга (олон тоог таслалаар тусгаарласан)", "condition_fields": { diff --git a/locales/nb/common.json b/locales/nb/common.json index 6c87e5262..2a1f89edf 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -2002,6 +2002,7 @@ "actions": "Handlinger", "add_action": "Legg til handling", "stop_processing": "Ikke behandle flere regler", + "include_spam": "Flytt også meldinger som er merket som søppelpost", "attachment_type_placeholder": "f.eks. pdf, doc, jpg", "value_placeholder_multi": "Verdi (skill flere verdier med komma)", "condition_fields": { diff --git a/locales/nl/common.json b/locales/nl/common.json index 0c240864c..5bb97790e 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -1995,6 +1995,7 @@ "actions": "Acties", "add_action": "Actie toevoegen", "stop_processing": "Verwerking van volgende regels stoppen", + "include_spam": "Ook berichten verplaatsen die als spam zijn gemarkeerd", "attachment_type_placeholder": "bijv. pdf, doc, jpg", "value_placeholder_multi": "Waarde (meerdere met komma's gescheiden)", "condition_fields": { diff --git a/locales/pl/common.json b/locales/pl/common.json index e38263341..c4ccfdae9 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -1995,6 +1995,7 @@ "actions": "Akcje", "add_action": "Dodaj akcję", "stop_processing": "Zatrzymaj przetwarzanie kolejnych reguł", + "include_spam": "Przenoś również wiadomości oznaczone jako spam", "attachment_type_placeholder": "np. pdf, doc, jpg", "value_placeholder_multi": "Wartość (kilka oddzielonych przecinkami)", "condition_fields": { diff --git a/locales/pt/common.json b/locales/pt/common.json index 9a872f385..3c8fec9af 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -1995,6 +1995,7 @@ "actions": "Ações", "add_action": "Adicionar ação", "stop_processing": "Parar o processamento das regras seguintes", + "include_spam": "Mover também as mensagens marcadas como spam", "attachment_type_placeholder": "ex.: pdf, doc, jpg", "value_placeholder_multi": "Valor (vários separados por vírgulas)", "condition_fields": { diff --git a/locales/ro/common.json b/locales/ro/common.json index 75af310de..1a2e67460 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2002,6 +2002,7 @@ "actions": "Acțiuni", "add_action": "Adăugare acțiune", "stop_processing": "Opriți procesarea regulilor următoare", + "include_spam": "Mută și mesajele marcate ca spam", "attachment_type_placeholder": "de ex. pdf, doc, jpg", "value_placeholder_multi": "Valoare (mai multe valori separate prin virgule)", "condition_fields": { diff --git a/locales/ru/common.json b/locales/ru/common.json index d6bba3b07..9a370ddfd 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -1995,6 +1995,7 @@ "actions": "Действия", "add_action": "Добавить действие", "stop_processing": "Прекратить обработку последующих правил", + "include_spam": "Перемещать также письма, помеченные как спам", "attachment_type_placeholder": "напр. pdf, doc, jpg", "value_placeholder_multi": "Значение (несколько через запятую)", "condition_fields": { diff --git a/locales/sk/common.json b/locales/sk/common.json index 960257339..475d81b7e 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -2002,6 +2002,7 @@ "actions": "Akcie", "add_action": "Pridať akciu", "stop_processing": "Zastaviť spracovanie ďalších pravidiel", + "include_spam": "Presúvať aj správy označené ako spam", "attachment_type_placeholder": "napr. pdf, doc, jpg", "value_placeholder_multi": "Hodnota (viac oddelených čiarkami)", "condition_fields": { diff --git a/locales/tr/common.json b/locales/tr/common.json index b9c644d00..acc0b446d 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -1995,6 +1995,7 @@ "actions": "İşlemler", "add_action": "İşlem Ekle", "stop_processing": "Sonraki kuralları işlemeyi durdur", + "include_spam": "İstenmeyen olarak işaretlenen iletileri de taşı", "attachment_type_placeholder": "örn. pdf, doc, jpg", "value_placeholder_multi": "Değer (birden fazla virgülle ayrılır)", "condition_fields": { diff --git a/locales/uk/common.json b/locales/uk/common.json index e4f32ef98..cc9f0abed 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -1995,6 +1995,7 @@ "actions": "Дії", "add_action": "Додати дію", "stop_processing": "Зупинити обробку наступних правил", + "include_spam": "Переміщувати також листи, позначені як спам", "attachment_type_placeholder": "напр. pdf, doc, jpg", "value_placeholder_multi": "Значення (декілька через кому)", "condition_fields": { diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index f894494a6..507bdc3f3 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -2002,6 +2002,7 @@ "actions": "動作", "add_action": "新增動作", "stop_processing": "停止處理後續規則", + "include_spam": "同時移動被標記為垃圾郵件的郵件", "attachment_type_placeholder": "例如:pdf、doc、jpg", "value_placeholder_multi": "值(多個用逗號分隔)", "condition_fields": { diff --git a/locales/zh/common.json b/locales/zh/common.json index 0d8b24d9e..c6989b8cf 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -1995,6 +1995,7 @@ "actions": "操作", "add_action": "添加操作", "stop_processing": "停止处理后续规则", + "include_spam": "同时移动被标记为垃圾邮件的邮件", "attachment_type_placeholder": "例如:pdf、doc、jpg", "value_placeholder_multi": "值(多个用逗号分隔)", "condition_fields": {