PageSourceSearch

https://docs.rspamd.com/assets/js/11e81086.de73c606.js

js rspamd.com collected 2026-10-02 02:29:55 UTC 210,166 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkrspamd_docs=self.webpackChunkrspamd_docs||[]).push([[3647],{28453:(e,s,n)=>{n.d(s,{R:()=>a,x:()=>d});var r=n(96540);const i={},t=r.createContext(i);function a(e){const s=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function d(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(t.Provider,{value:s},e.children)}},94397:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>o,contentTitle:()=>d,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"tutorials/migration","title":"Upgrading","description":"This document outlines the modifications to Rspamd in recent versions, including any incompatible changes, and provides instructions for updating your rules and configuration accordingly.","source":"@site/docs/tutorials/migration.md","sourceDirName":"tutorials","slug":"/tutorials/migration","permalink":"/tutorials/migration","draft":false,"unlisted":false,"editUrl":"https://github.com/rspamd/docs.rspamd.com/edit/master/docs/tutorials/migration.md","tags":[],"version":"current","frontMatter":{"title":"Upgrading"},"sidebar":"docs","previous":{"title":"Tool Selection Guide","permalink":"/guides/configuration/tool-selection"},"next":{"title":"Migrating from SpamAssassin","permalink":"/tutorials/migrate_sa"}}');var i=n(74848),t=n(28453);const a={title:"Upgrading"},d="Updating Rspamd",o={},l=[{value:"Efficient Rspamd Cluster Upgrade Guide",id:"efficient-rspamd-cluster-upgrade-guide",level:2},{value:"Migration to Rspamd 4.0.0",id:"migration-to-rspamd-400",level:2},{value:"1. Bayes Per-User Resharding (Required for sharded Redis deployments)",id:"1-bayes-per-user-resharding-required-for-sharded-redis-deployments",level:3},{value:"2. Content URLs Included by Default",id:"2-content-urls-included-by-default",level:3},{value:"3. SSL Worker Option Removed",id:"3-ssl-worker-option-removed",level:3},{value:"4. Proxy Load Balancing Default Changed",id:"4-proxy-load-balancing-default-changed",level:3},{value:"5. SenderScore RBLs Disabled",id:"5-senderscore-rbls-disabled",level:3},{value:"6. DKIM Unknown Key Handling",id:"6-dkim-unknown-key-handling",level:3},{value:"7. Suspicious TLDs Now Map-Based",id:"7-suspicious-tlds-now-map-based",level:3},{value:"8. Neural Module Autolearn Option Renames",id:"8-neural-module-autolearn-option-renames",level:3},{value:"9. libfasttext Dependency Removed (Packagers)",id:"9-libfasttext-dependency-removed-packagers",level:3},{value:"Migration to Rspamd 3.13.0",id:"migration-to-rspamd-3130",level:2},{value:"Multi-class Bayes",id:"multi-class-bayes",level:3},{value:"Incompatibilities",id:"incompatibilities",level:4},{value:"Keeping existing spam/ham database intact",id:"keeping-existing-spamham-database-intact",level:4},{value:"Learning commands",id:"learning-commands",level:4},{value:"Upgrade checklist",id:"upgrade-checklist",level:4},{value:"Migration to Rspamd 3.9.0",id:"migration-to-rspamd-390",level:2},{value:"Migration to Rspamd 3.7.4",id:"migration-to-rspamd-374",level:2},{value:"Migration to Rspamd 3.7.2",id:"migration-to-rspamd-372",level:2},{value:"Migration to Rspamd 3.3",id:"migration-to-rspamd-33",level:2},{value:"Migration to Rspamd 3.0",id:"migration-to-rspamd-30",level:2},{value:"Migration to Rspamd 2.6",id:"migration-to-rspamd-26",level:2},{value:"Migration to Rspamd 2.0",id:"migration-to-rspamd-20",level:2},{value:"Migration to Rspamd 1.9.1",id:"migration-to-rspamd-191",level:2},{value:"Migration to Rspamd 1.9.0",id:"migration-to-rspamd-190",level:2},{value:"Migration to Rspamd 1.8.1",id:"migration-to-rspamd-181",level:2},{value:"General configuration change",id:"general-configuration-change",level:3},{value:"Fuzzy and bayes misses for large text messages",id:"fuzzy-and-bayes-misses-for-large-text-messages",level:3},{value:"Different <code>CONFDIR</code> and <code>LOCAL_CONFDIR</code> case",id:"different-confdir-and-local_confdir-case",level:3},{value:"Migration to Rspamd 1.8.0",id:"migration-to-rspamd-180",level:2},{value:"Clickhouse changes",id:"clickhouse-changes",level:3},{value:"Settings changes",id:"settings-changes",level:3},{value:"Migration to Rspamd 1.7.4",id:"migration-to-rspamd-174",level:2},{value:"Migration to Rspamd 1.7.0",id:"migration-to-rspamd-170",level:2},{value:"Migrating to Rspamd 1.6.5",id:"migrating-to-rspamd-165",level:2},{value:"Migrating to Rspamd 1.6.0",id:"migrating-to-rspamd-160",level:2},{value:"Migrating to Rspamd 1.5.3",id:"migrating-to-rspamd-153",level:2},{value:"Migrating to Rspamd 1.5.2",id:"migrating-to-rspamd-152",level:2},{value:"Migrating to Rspamd 1.5",id:"migrating-to-rspamd-15",level:2}
1,{value:"Migrating to Rmilter 1.10.0 and Rspamd 1.4.0",id:"migrating-to-rmilter-1100-and-rspamd-140",level:2},{value:"Migrating to Rmilter 1.9.1 and Rspamd 1.3.1",id:"migrating-to-rmilter-191-and-rspamd-131",level:2},{value:"Migrating from Rmilter 1.8 to Rmilter 1.9",id:"migrating-from-rmilter-18-to-rmilter-19",level:2},{value:"Migrating from Rspamd 1.2 to Rspamd 1.3",id:"migrating-from-rspamd-12-to-rspamd-13",level:2},{value:"Migrating from Rspamd 1.1 to Rspamd 1.2",id:"migrating-from-rspamd-11-to-rspamd-12",level:2},{value:"Migrating from Rspamd 1.0 to Rspamd 1.1",id:"migrating-from-rspamd-10-to-rspamd-11",level:2},{value:"Migrating from Rspamd 0.9 to Rspamd 1.0",id:"migrating-from-rspamd-09-to-rspamd-10",level:2},{value:"Migrating from Rspamd 0.6 to Rspamd 0.7",id:"migrating-from-rspamd-06-to-rspamd-07",level:2},{value:"WebUI changes",id:"webui-changes",level:3},{value:"Settings changes",id:"settings-changes-1",level:3},{value:"Lua changes",id:"lua-changes",level:3},{value:"Protocol changes",id:"protocol-changes",level:3},{value:"Migration to Rspamd 4.1.0",id:"migration-to-rspamd-410",level:2},{value:"1. Default <code>rspamd.com</code> Fuzzy Rule Now Discovered via SRV",id:"1-default-rspamdcom-fuzzy-rule-now-discovered-via-srv",level:3},{value:"2. <code>mx_check</code> Module Reworked: Symbols Renamed, Options Deprecated",id:"2-mx_check-module-reworked-symbols-renamed-options-deprecated",level:3},{value:"3. Neural ANN Digest Changes Under <code>disable_symbols_input</code>
1",id:"3-neural-ann-digest-changes-under-disable_symbols_input",level:3},{value:"4. <code>mailto:</code> URLs Stored in Canonical Slash-less Form",id:"4-mailto-urls-stored-in-canonical-slash-less-form",level:3},{value:"Migration to Rspamd 4.1.1",id:"migration-to-rspamd-411",level:2},{value:"1. <code>/checkv3</code> Reply Format Is Now Content-Negotiated",id:"1-checkv3-reply-format-is-now-content-negotiated",level:3},{value:"2. <code>mx_check</code>: Loopback-only MX Reclassified as Local",id:"2-mx_check-loopback-only-mx-reclassified-as-local",level:3},{value:"Migration to Rspamd 4.1.2",id:"migration-to-rspamd-412",level:2},{value:"1. Invalid selectors now fail configuration load",id:"1-invalid-selectors-now-fail-configuration-load",level:3},{value:"2. Glob map entries now match the whole subject",id:"2-glob-map-entries-now-match-the-whole-subject",level:3},{value:"3. Symbols flagged explicit_enable are strictly gated under settings",id:"3-symbols-flagged-explicit_enable-are-strictly-gated-under-settings",level:3},{value:"4. Alias resolution no longer rewrites From domains",id:"4-alias-resolution-no-longer-rewrites-from-domains",level:3},{value:"5. lua_cryptobox secretbox: ciphertexts produced with short nonces",id:"5-lua_cryptobox-secretbox-ciphertexts-produced-with-short-nonces",level:3},{value:"Migration to Rspamd 4.1.3",id:"migration-to-rspamd-413",level:2},{value:"1. Fuzzy Non-Exact Match Scoring Curve Changed",id:"1-fuzzy-non-exact-match-scoring-curve-changed",level:3},{value:"2. SPF DNS Limits Are Now Enforced and Yield permerror",id:"2-spf-dns-limits-are-now-enforced-and-yield-permerror",level:3},{value:"3. DKIM Signature Limit and Key Bounds",id:"3-dkim-signature-limit-and-key-bounds",level:3},{value:"4. url_redirector Blocks Local Destinations by Default",id:"4-url_redirector-blocks-local-destinations-by-default",level:3},{value:"5. New Size Ceilings on HTTP, Maps and Decompression",id:"5-new-size-ceilings-on-http-maps-and-decompression",level:3},{value:"6. MIME and URL Parser Limits Are Now Actually Enforced",id:"6-mime-and-url-parser-limits-are-now-actually-enforced",level:3},{value:"7. Fuzzy Match Reporting Format Changed",id:"7-fuzzy-match-reporting-format-changed",level:3},{value:"8. ClickHouse Schema Version 11",id:"8-clickhouse-schema-version-11",level:3},{value:"9. Fuzzy Storage: Admission Control, Rate Limits and Statistics",id:"9-fuzzy-storage-admission-control-rate-limits-and-statistics",level:3},{value:"10. Fuzzy Redis Storage Persists Shingle Sets",id:"10-fuzzy-redis-storage-persists-shingle-sets",level:3},{value:"11. Lua API Changes",id:"11-lua-api-changes",level:3},{value:"12. WebUI Rewritten Without jQuery or Font Awesome",id:"12-webui-rewritten-without-jquery-or-font-awesome",level:3},{value:"13. Duplicated Module Sections Now Warn",id:"13-duplicated-module-sections-now-warn",level:3},{value:"Migration to Rspamd 4.1.4",id:"migration-to-rspamd-414",level:2},{value:"1. Controller Authentication Now Fails Closed on Malformed Password Hashes",id:"1-controller-authentication-now-fails-closed-on-malformed-password-hashes",level:3},{value:"2. Building with jemalloc Requires the Shared Library",id:"2-building-with-jemalloc-requires-the-shared-library",level:3},{value:"3. The Regexp Input Size Limit Now Applies to Named Scopes",id:"3-the-regexp-input-size-limit-now-applies-to-named-scopes",level:3},{value:"4. Word Retention Is Now Bounded for Very Large Messages",id:"4-word-retention-is-now-bounded-for-very-large-messages",level:3},{value:"5. WebUI Read-Only Password Grants Access to Errors History and Selectors",id:"5-webui-read-only-password-grants-access-to-errors-history-and-selectors",level:3},{value:"Migration to Rspamd 4.1.5",id:"migration-to-rspamd-415",level:2},{value:"1. Upgrade Self-Hosted Fuzzy Storages Before Scanners",id:"1-upgrade-self-hosted-fuzzy-storages-before-scanners",level:3},{value:"2. Fuzzy Storage: Per-Key <code>forbidden_ids</code> Replaces the Global List Again",id:"2-fuzzy-storage-per-key-forbidden_ids-replaces-the-global-list-again",level:3},{value:"3. Fuzzy Storage: <code>encrypted_only</code> No Longer Exempts &quot;Local&quot; Peers",id:"3-fuzzy-storage-encrypted_only-no-longer-exempts-local-peers",level:3},{value:"4. GPT Plugin: <code>condition</code> Callback Returns Only a Verdict",id:"4-gpt-plugin-condition-callback-returns-only-a-verdict",level:3},{value:"5. Elastic Module: New Document Schema for Fuzzy Results",id:"5-elastic-module-new-document-schema-for-fuzzy-results",level:3},{value:"6. File and Shared-Memory Message Inputs Are Now Gated",id:"6-file-and-shared-memory-message-inputs-are-now-gated",level:3},{value:"7. Controller Rate-Limits Failed Authentications",id:"7-controller-rate-limits-failed-authentications",level:3},{value:"8. Previously Broken Checks Now Fire and Can Change Scores",id:"8-previously-broken-checks-now-fire-and-can-change-scores",level:3},{value:"Migration to Rspamd 4.2.0",id:"migration-to-rspamd-420",level:2},{value:"1. Multimap <code>top</code> Filter Returns the Full Public Suffix",id:"1-multimap-top-filter-returns-the-full-public-suffix",level:3},{value:"2. Public Suffix Resolution Reworked",id:"2-public-suffix-resolution-reworked",level:3},{value:"3. SPF: Multiple Records Now Produce a Permerror",id:"3-spf-multiple-records-now-produce-a-permerror",level:3},{value:"4. DKIM Alignment Is a Symbol, and <code>R_DKIM_ALLOW</code> Is Worth Half",id:"4-dkim-alignment-is-a-symbol-and-r_dkim_allow-is-worth-half",level:3},{value:"5. Symbols Cache: One Scheduler, Stricter Dependency Rules",id:"5-symbols-cache-one-scheduler-stricter-dependency-rules",level:3}
1,{value:"6. Fuzzy Storage: Hash Count Comes From a Periodic SCAN",id:"6-fuzzy-storage-hash-count-comes-from-a-periodic-scan",level:3},{value:"7. Fuzzy Storage: Key Policy Precedence and Per-Source Statistics",id:"7-fuzzy-storage-key-policy-precedence-and-per-source-statistics",level:3},{value:"8. UCL Parsing of Untrusted Input Is Bounded",id:"8-ucl-parsing-of-untrusted-input-is-bounded",level:3},{value:"9. ClickHouse Schema 12",id:"9-clickhouse-schema-12",level:3},{value:"10. New Attachment and Hidden-Text Scoring",id:"10-new-attachment-and-hidden-text-scoring",level:3},{value:"11. <code>url_redirector</code>: <code>redirectors_only</code> Is Now Enforced",id:"11-url_redirector-redirectors_only-is-now-enforced",level:3},{value:"12. <code>forged_recipients</code> Compares by Mailbox Identity and Fetches a New Map",id:"12-forged_recipients-compares-by-mailbox-identity-and-fetches-a-new-map",level:3}];function c(e){const s={a:"a",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.header,{children:(0,i.jsx)(s.h1,{id:"updating-rspamd",children:"Updating Rspamd"})}),"\n",(0,i.jsx)(s.p,{children:"This document outlines the modifications to Rspamd in recent versions, including any incompatible changes, and provides instructions for updating your rules and configuration accordingly."}),"\n",(0,i.jsx)(s.h2,{id:"efficient-rspamd-cluster-upgrade-guide",children:"Efficient Rspamd Cluster Upgrade Guide"}),"\n",(0,i.jsx)(s.p,{children:"Discover a reliable step-by-step process for upgrading your Rspamd cluster while maintaining stability and minimizing downtime. This guide emphasizes a cautious approach with extensive testing to ensure a seamless transition between versions."}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Ensure that you have a backup of the current stable configuration files, as well as any custom rules, maps, or other settings specific to your cluster."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Use stable packages on a stable cluster. Always use the latest stable release of Rspamd in your production environment."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Add a node or multiple nodes to an experimental cluster using the experimental repository. This will allow you to test the new version in a controlled environment without affecting the stable cluster."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsxs)(s.p,{children:["Mirror traffic from the stable cluster to the experimental cluster using the ",(0,i.jsx)(s.code,{children:"rspamd_proxy"})," module. This will help you identify any potential issues or differences between the two versions of Rspamd."]}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Monitor the experimental cluster for any discrepancies, crashes, or other issues. Address these problems as they arise."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"When a new release is cut, update the experimental cluster to the stable version."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Repeat steps 4-5 one more time to ensure that any previously identified issues have been resolved."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Once you are confident that the new version is stable and compatible with your environment, update the stable cluster to the next version."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsx)(s.p,{children:"Continue to monitor the stable cluster to ensure smooth operation and resolve any issues that may arise after the upgrade."}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsxs)(s.p,{children:["Repeat the entire process starting from ",(0,i.jsx)(s.code,{children:"step 1"})," for future updates. This approach ensures a smooth and controlled upgrade process that minimizes potential downtime and issues in your production environment."]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-400",children:"Migration to Rspamd 4.0.0"}),"\n",(0,i.jsx)(s.h3,{id:"1-bayes-per-user-resharding-required-for-sharded-redis-deployments",children:"1. Bayes Per-User Resharding (Required for sharded Redis deployments)"}),"\n",(0,i.jsxs)(s.p,{children:["Rspamd 4.0 replaces Jump Hash with Ring Hash (Ketama) for consistent upstream selection in Redis-sharded Bayes deployments (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/4ea750466",children:"4ea7504"}),"). After upgrade, per-user Bayes keys will be looked up on different shards than where they were written."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Only users with multiple ",(0,i.jsx)(s.code,{children:"write_servers"})," configured for Bayes Redis backends. Single-server deployments are not affected."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Back up all Redis Bayes databases before proceeding."}),"\n",(0,i.jsx)(s.li,{children:"While still running the old version, dump the statistics:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm statistics_dump dump -o /path/to/bayes-backup.bin\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"3",children:["\n",(0,i.jsx)(s.li,{children:"Upgrade Rspamd to 4.0."}),"\n",(0,i.jsx)(s.li,{children:"Run the migration tool to redistribute keys to the correct shards under Ring Hash:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm statistics_dump migrate\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"5",children:["\n",(0,i.jsxs)(s.li,{children:["Verify with ",(0,i.jsx)(s.code,{children:"rspamc stat"})," that token counts look reasonable."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["If you skip the migration, existing Bayes data will not be lost \u2014 it will simply be on the wrong shard and accuracy will degrade until messages are re-learned naturally. (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/36325c5c5",children:"36325c5"}),")"]}),"\n",(0,i.jsx)(s.h3,{id:"2-content-urls-included-by-default",children:"2. Content URLs Included by Default"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"include_content_urls"})," now defaults to ",(0,i.jsx)(s.code,{children:"true"}),", meaning ",(0,i.jsx)(s.code,{children:"task:get_urls()"})," returns URLs extracted from PDF and other computed parts (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/840e74db4",children:"840e74d"}),"). This may trigger new RBL or URL reputation hits on messages with PDF attachments."]}),"\n",(0,i.jsxs)(s.p,{children:["To restore the previous behavior, add to ",(0,i.jsx)(s.code,{children:"local.d/options.inc"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"include_content_urls = false;\n"})}),"\n",(0,i.jsx)(s.h3,{id:"3-ssl-worker-option-removed",children:"3. SSL Worker Option Removed"}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"ssl = true"})," option in worker configuration blocks has been removed (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/4674408f6",children:"4674408"}),"). SSL is now auto-detected from bind socket flags."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Before:"})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'worker "controller" {\n  bind_socket = "localhost:11334";\n  ssl = true;\n  ssl_cert = "/path/to/cert.pem";\n  ssl_key = "/path/to/key.pem";\n}\n'})}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"After:"})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'worker "controller" {\n  bind_socket = "localhost:11334 ssl";\n  ssl_cert = "/path/to/cert.pem";\n  ssl_key = "/path/to/key.pem";\n}\n'})}),"\n",(0,i.jsxs)(s.p,{children:["Remove ",(0,i.jsx)(s.code,{children:"ssl = true"})," from all worker sections and append the ",(0,i.jsx)(s.code,{children:"ssl"})," suffix to the relevant ",(0,i.jsx)(s.code,{children:"bind_socket"})," lines. ",(0,i.jsx)(s.code,{children:"rspamadm configtest"})," will flag any remaining ",(0,i.jsx)(s.code,{children:"ssl = true"})," occurrences."]}),"\n",(0,i.jsx)(s.h3,{id:"4-proxy-load-balancing-default-changed",children:"4. Proxy Load Balancing Default Changed"}),"\n",(0,i.jsxs)(s.p,{children:["Token bucket load balancing is now the default algorithm for proxy upstream
1s (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/728f19f20",children:"728f19f"}),"), replacing simple round-robin. The change is generally transparent but alters request distribution under burst conditions."]}),"\n",(0,i.jsxs)(s.p,{children:["To restore round-robin, remove the ",(0,i.jsx)(s.code,{children:"token_bucket"})," block from your proxy upstream configuration in ",(0,i.jsx)(s.code,{children:"local.d/rspamd_proxy.inc"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'upstream "scan" {\n  # remove token_bucket { ... } block if present\n  hosts = "backend1:11333,backend2:11333";\n}\n'})}),"\n",(0,i.jsx)(s.h3,{id:"5-senderscore-rbls-disabled",children:"5. SenderScore RBLs Disabled"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"senderscore_reputation"})," is disabled by default because it requires a MyValidity account registration and was returning blocked results for all unregistered IPs (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ce71021ae",children:"ce71021"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:["Users with a registered MyValidity account who wish to keep using SenderScore should explicitly re-enable it in ",(0,i.jsx)(s.code,{children:"local.d/reputation.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"senderscore_reputation {\n  enabled = true;\n}\n"})}),"\n",(0,i.jsx)(s.h3,{id:"6-dkim-unknown-key-handling",children:"6. DKIM Unknown Key Handling"}),"\n",(0,i.jsxs)(s.p,{children:["Unknown and broken DKIM keys are now handled strictly per RFC (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e9e6bac43",children:"e9e6bac"}),"). Messages with malformed DKIM keys may receive different DKIM result symbols than before. No configuration change is required; review DKIM scores if you notice unexpected changes in classification."]}),"\n",(0,i.jsx)(s.h3,{id:"7-suspicious-tlds-now-map-based",children:"7. Suspicious TLDs Now Map-Based"}),"\n",(0,i.jsxs)(s.p,{children:["The hardcoded suspicious TLD list has been replaced with a map file at ",(0,i.jsx)(s.code,{children:"conf/maps.d/suspicious_tlds.inc"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/614e68c8b",children:"614e68c"}),")."]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["To ",(0,i.jsx)(s.strong,{children:"override"})," the list entirely, create ",(0,i.jsx)(s.code,{children:"local.d/maps.d/suspicious_tlds.inc"})," with your own entries."]}),"\n",(0,i.jsxs)(s.li,{children:["To ",(0,i.jsx)(s.strong,{children:"extend"})," the default list, create ",(0,i.jsx)(s.code,{children:"local.d/maps.d/suspicious_tlds.inc.local"})," and add extra TLDs there."]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"Any TLDs previously maintained via hardcoded patches to the source or custom rules should be migrated to the map file."}),"\n",(0,i.jsx)(s.h3,{id:"8-neural-module-autolearn-option-renames",children:"8. Neural Module Autolearn Option Renames"}),"\n",(0,i.jsxs)(s.p,{children:["Autolearn-related options in the neural module have been renamed to align with RBL module naming conventions (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/71dac5167",children:"71dac51"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:["If you have custom neural configuration in ",(0,i.jsx)(s.code,{children:"local.d/neural.conf"})," or ",(0,i.jsx)(s.code,{children:"override.d/neural.conf"}),", review the ",(0,i.jsx)(s.a,{href:"/modules/neural",children:"neural module documentation"})," for the updated option names and update accordingly. Run ",(0,i.jsx)(s.code,{children:"rspamadm configtest"})," to surface any unknown options."]}),"\n",(0,i.jsx)(s.h3,{id:"9-libfasttext-dependency-removed-packagers",children:"9. libfasttext Dependency Removed (Packagers)"}),"\n",(0,i.jsxs)(s.p,{children:["The external libfasttext C++ shared library is no longer required or used (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d96ee3610",children:"d96ee36"}),"). The ",(0,i.jsx)(s.code,{children:"ENABLE_FASTTEXT"})," cmake option has been removed \u2014 Fasttext support is always compiled in via the built-in shim."]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Packagers"}),": Remove libfasttext from build dependencies and runtime dependencies."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Users"}
1),": No action required. Existing ",(0,i.jsx)(s.code,{children:".bin"})," and ",(0,i.jsx)(s.code,{children:".ftz"})," model files continue to work without modification."]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-3130",children:"Migration to Rspamd 3.13.0"}),"\n",(0,i.jsx)(s.h3,{id:"multi-class-bayes",children:"Multi-class Bayes"}),"\n",(0,i.jsx)(s.p,{children:"Rspamd 3.13 adds multi-class Bayes classification, allowing categories beyond spam/ham (e.g. newsletters, transactional, phishing). The recommended production setup is to keep the classic spam/ham classifier separate and add a second classifier for non-binary classes."}),"\n",(0,i.jsx)(s.h4,{id:"incompatibilities",children:"Incompatibilities"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["In a multi-class classifier you must use ",(0,i.jsx)(s.code,{children:'class = "<name>"'})," in each ",(0,i.jsx)(s.code,{children:"statfile"})," and you must not use ",(0,i.jsx)(s.code,{children:"spam = true/false"})," there."]}),"\n",(0,i.jsx)(s.li,{children:"Do not mix binary and multi-class statfiles within the same classifier block."}),"\n",(0,i.jsx)(s.li,{children:"A multi-class classifier requires at least two classes."}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"min_learns"})," applies per class; insufficiently trained classes are ignored during classification."]}),"\n"]}),"\n",(0,i.jsx)(s.h4,{id:"keeping-existing-spamham-database-intact",children:"Keeping existing spam/ham database intact"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Preserve your current binary classifier as-is (same symbols ",(0,i.jsx)(s.code,{children:"BAYES_SPAM"}),"/",(0,i.jsx)(s.code,{children:"BAYES_HAM"}),", same backend). It will continue to use your existing Redis data; no relearning is required."]}),"\n",(0,i.jsx)(s.li,{children:"Add a new classifier for multi-class categories. It will build its own dataset independently."}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"Example:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'# Keep binary classifier (uses existing DB)\nclassifier "bayes" {\n  name = "bayes_binary";\n  tokenizer { name = "osb"; }\n  backend = "redis";\n  min_tokens = 11;\n  min_learns = 200;\n  statfile { symbol = "BAYES_HAM"; spam = false; }\n  statfile { symbol = "BAYES_SPAM"; spam = true; }\n  learn_condition = \'return require("lua_bayes_learn").can_learn\';\n}\n\n# Add new multi-class classifier\nclassifier "bayes" {\n  name = "bayes_multi";\n  tokenizer { name = "osb"; }\n  backend = "redis";\n  min_tokens = 11;\n  min_learns = 200;\n  statfile { symbol = "BAYES_NEWSLETTER"; class = "newsletter"; }\n  statfile { symbol = "BAYES_TRANSACTIONAL"; class = "transactional"; }\n  statfile { symbol = "BAYES_PHISHING"; class = "phishing"; }\n  learn_condition = \'return require("lua_bayes_learn").can_learn\';\n}\n'})}),"\n",(0,i.jsx)(s.h4,{id:"learning-commands",children:"Learning commands"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"Binary (unchanged):"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamc learn_spam msg.eml\nrspamc learn_ham msg.eml\n"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"Multi-class (new):"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamc learn_class:newsletter newsletter1.eml\nrspamc learn_class:transactional order_confirmation.eml\n"})}),"\n",(0,i.jsx)(s.h4,{id:"upgrade-checklist",children:"Upgrade checklist"}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Keep your existing binary classifier unchanged to reuse data."}),"\n",(0,i.jsxs)(s.li,{children:["Add a new multi-class classifier with ",(0,i.jsx)(s.code,{children:'class = "..."'})," statfiles."]}),"\n",(0,i.jsxs)(s.li,{children:["Do not mix ",(0,i.jsx)(s.code,{children:"spam = true/false"})," with ",(0,i.jsx)(s.code,{children:'class = "..."'})," in one classifier."]}),"\n",(0,i.jsxs)(s.li,{children:["Start learning new classes using ",(0,i.jsx)(s.code,{children:"learn_class:<name>"}),"."]}),"\n",(0,i.jsxs)(s.li,{children:["Validate with ",(0,i.jsx)(s.code,{children:"rspamd -t"})," and monitor results."]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-390",children:"Migration to Rspamd 3.9.0"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"ratelimit"})," module now operates in non-dynamic mode by default. This change does not affect any existing buckets, as dynamic rates and dynamic bursts will simply be unused in this mode. To retain the old behaviour, please either set the ",(0,i.jsx)(s.code,{children:"dynamic_rate_limit"})," option to ",(0,i.jsx)(s.code,{children:"true"})," (globally for all ratelimit rules) or configure the ",(0,i.jsx)(s.code,{children:"ham_factor_rate"}),"/",(0,i.jsx)(s.code,{children:"spam_factor_rate"})," and/or ",(0,i.jsx)(s.code,{children:"ham_factor_burst"}),"/",(0,i.jsx)(s.code,{children:"spam_factor_burst"})," multipliers for individual rules as needed."]}),"\n"]}),"\n",(0,i.jsxs)(s.li,{children:["\n",(0,i.jsxs)(s.p,{children:["Bayes statistics now use a reduced window size (2 words), which has proven to be faster and more space-efficient in our tests. Existing statistics can be used without any modifications or relearning. To restore the old behaviour, one can set the following to ",(0,i.jsx)(s.code,{children:"local.d/classifier-bayes.conf"}),":"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:'tokenizer {\n  name = "osb";\n  window = 5;\n}\n'})}),"\n",(0,i.jsx)(s.p,{children:"However, it is recommended to use the default settings."}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-374",children:"Migration to Rspamd 3.7.4"}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"exclude_private_ips"})," setting in RBL module no longer exists in this release (and was broken in previous releases), it can be removed from configuration. This setting is equivalent to ",(0,i.jsx)(s.code,{children:"exclude_local"}),"."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-372",children:"Migration to Rspamd 3.7.2"}),"\n",(0,i.jsxs)(s.p,{children:["This release introduces ",(0,i.jsx)(s.a,{href:"/modules/rbl#returncodes-matchers",children:"returncodes matchers"})," in RBL module. Previously returncodes were always treated as Lua patterns, now this behaviour is enabled by setting ",(0,i.jsx)(s.code,{children:'matcher = "luapattern"'})," on the rule. For backwards-compatibility this matcher may be enabled implicitly where Lua patterns are detected but they may not be correctly detected in all cases. If you use custom RBL module configuration that makes use of Lua patterns please review it and explicitly set matcher where necessary."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-33",children:"Migration to Rspamd 3.3"}),"\n",(0,i.jsxs)(s.p,{children:["When migrating to Rspamd 3.3, exercise caution if you are utilizing custom passthrough rules, particularly those defined by plugins that utilize ",(0,i.jsx)(s.code,{children:"action"})," rather than ",(0,i.jsx)(s.code,{children:"least action"}),"). In versions prior to 3.3, these rules would still allow for processing of additional rules. However, in Rspamd 3.3 and beyond, passthrough denotes a final action and skips directly to the idempotent stage."]}),"\n",(0,i.jsxs)(s.p,{children:["Users of the ",(0,i.jsx)(s.code,{children:"neural"})," plugin may experience a significant Redis storage leak in version 3.2. This issue is resolved 
1in version 3.3 with ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f9cfbba2c84e01f18e65618587e6854681843ff1",children:"the following commit"}),", however, this fix will not remove any existing stale keys. These keys also do not have an expiration set. One solution to clean up the database is to remove all keys starting with the ",(0,i.jsx)(s.code,{children:"rn_"})," prefix. There are various options available to accomplish this, such as exploring the ",(0,i.jsx)(s.a,{href:"https://stackoverflow.com/questions/4006324/how-to-atomically-delete-keys-matching-a-pattern-using-redis",children:"following conversation"})," on Stackoverflow."]}),"\n",(0,i.jsxs)(s.p,{children:["Starting from this version, building Rspamd requires a ",(0,i.jsx)(s.strong,{children:"C++20"})," compatible compiler and toolchain. For Ubuntu Bionic users, this means adding the LLVM repository for the compatible C++20 standard library runtime. The necessary steps are outlined on the ",(0,i.jsx)(s.a,{href:"/downloads",children:"downloads page"}),"."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-30",children:"Migration to Rspamd 3.0"}),"\n",(0,i.jsxs)(s.p,{children:["The functionality for DMARC reporting is no longer included in the DMARC module. To send DMARC reports, you must now run the ",(0,i.jsx)(s.code,{children:"rspamadm dmarc_report"})," command on a regular basis, such as through a cron job. Additionally, the configuration for reporting has undergone incompatible changes, so please refer to the ",(0,i.jsx)(s.a,{href:"/modules/dmarc",children:"module documentation"})," for further information."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-26",children:"Migration to Rspamd 2.6"}),"\n",(0,i.jsx)(s.p,{children:"To ensure proper functionality of the GUI after upgrading to 2.6, it is necessary to clear the browser cache and restart the browser."}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, the ",(0,i.jsx)(s.code,{children:"Neural networks"})," plugin's training data will be lost as the internal structure of the NN has been redesigned in an incompatible manner. However, training can continue as normal."]}),"\n",(0,i.jsxs)(s.p,{children:["If you encounter complaints about ",(0,i.jsx)(s.code,{children:"SENDER_REP_HAM"})," and ",(0,i.jsx)(s.code,{children:"SENDER_REP_SPAM"})," symbols in Rspamd logs, you may need to define scores for these symbols. Refer to the ",(0,i.jsx)(s.a,{href:"/modules/reputation",children:"reputation module documentation"}),". This issue will be fixed in future Rspamd releases."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-20",children:"Migration to Rspamd 2.0"}),"\n",(0,i.jsxs)(s.p,{children:["The RBL module has replaced both the ",(0,i.jsx)(s.code,{children:"emails"})," and ",(0,i.jsx)(s.code,{children:"surbl"})," modules, consolidating all Runtime Black Lists checks in a single location. The existing rules are typically automatically converted to the",(0,i.jsx)(s.code,{children:"rbl"})," syntax, including those defined in ",(0,i.jsx)(s.code,{children:"local.d"}),". However, ",(0,i.jsx)(s.code,{children:"override.d"})," rules will only act as overrides for ",(0,i.jsx)(s.code,{children:"local.d"})," and not for RBL module. If you need to use overrides, consider converting them to ",(0,i.jsx)(s.code,{children:"override.d/rbl.conf"})," rules."]}),"\n",(0,i.jsxs)(s.p,{children:["Note that ",(0,i.jsx)(s.code,{children:"emails"})," rules utilizing maps instead of DNS RBLs are ",(0,i.jsx)(s.strong,{children:"NO LONGER SUPPORTED"}),". Instead, use ",(0,i.jsx)(s.code,{children:"multimap"})," with selectors."]}),"\n",(0,i.jsxs)(s.p,{children:["In version 2.0, the default Bayes backend has been changed to Redis. The Sqlite backend is now considered deprecated and is not recommended for use. Therefore, if you were previously using the ",(0,i.jsx)(s.code,{children:"sqlite"})," backend as default, you will need to specify its type manually or convert it to Redis (or even, start again and relearn Redis backend)."]}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"ip_score"})," module has been replaced by the ",(0,i.jsx)(s.code,{children:"reputation"})," module, and the existing rules should be automatically converted to the new syntax. The symbol name has also been changed to two symbols: ",(0,i.jsx)(s.code,{children:"SENDER_REP_SPAM"})," and ",(0,i.jsx)(s.code,{children:"SENDER_REP_HAM"}),", and the scores for ",(0,i.jsx)(s.code,{children:"IP_SCORE"})," should be automatically applied to these new symbols. However, it should be noted that any data collected by the ",(0,i.jsx)(s.code,{children:"ip_score"})," plugin will be ",(0,i.jsx)(s.strong,{children:"IRRECOVERABLY LOST"}),". This change was necessary due to a significant flaw in the old plugin that caused the reputation to never expire."]}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"neural"})," module has undergone a significant update. While no config incompatibilities have been identified, it should be noted that any existing network data will be ",(0,i.jsx)(s.strong,{children:"IRRECOVERABLY LOST"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["When building Rspamd from source, a ",(0,i.jsx)(s.strong,{children:"C++11"})," capable compiler is now required as there are bundled dependencies written in C++ (specifically, the replxx library). Additionally, the ",(0,i.jsx)(s.code,{children:"libsodium"})," is also necessary. Rspamd now only supports the ",(0,i.jsx)(s.code,{children:"clang"})," and ",(0,i.jsx)(s.code,{children:"gcc"}
1)," compilers. Other compilers may still work, but it is no longer guaranteed."]}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, Bayes expiry now always works in ",(0,i.jsx)(s.code,{children:"lazy"})," mode and the default mode has been changed to ",(0,i.jsx)(s.code,{children:"lazy"})," only."]}),"\n",(0,i.jsx)(s.p,{children:"Furthermore, the Log helper worker has been removed, although it is unlikely that it was being used by anyone."}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-191",children:"Migration to Rspamd 1.9.1"}),"\n",(0,i.jsxs)(s.p,{children:["From version 1.9.1, Rspamd supports ",(0,i.jsx)(s.a,{href:"https://jinja.palletsprojects.com",children:"Jinja2 templates"})," provided by ",(0,i.jsx)(s.a,{href:"https://foicica.com/lupa/",children:"Lupa Lua library"}),". You can learn more about the basic syntax and capabilities of these templating engines by following the links provided. Rspamd uses a specific syntax for variable tags: ",(0,i.jsx)(s.code,{children:"{="})," and ",(0,i.jsx)(s.code,{children:"=}"})," instead of the traditional `` as these tags could represent, for example, a table within a table in Lua."]}),"\n",(0,i.jsx)(s.p,{children:"Therefore, in version 1.9.1 and above, your config files must be Jinja safe, meaning that there should be no special sequences such as `"}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-190",children:"Migration to Rspamd 1.9.0"}),"\n",(0,i.jsx)(s.p,{children:"This version should not generally be incompatible with the previous one aside of the case if you build Rspamd from the sources or use a custom package. From the version 1.9, Rspamd has changed some of the default installation paths:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["There is a new ",(0,i.jsx)(s.code,{children:"${LIBDIR}/rspamd/librspamd-server.so"})," library that contains common functions for ",(0,i.jsx)(s.code,{children:"rspamd"}),", ",(0,i.jsx)(s.code,{children:"rspamadm"})," and ",(0,i.jsx)(s.code,{children:"rspamc"})," binaries"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"${PLUGINSDIR}"})," is now set to a specific path for Lua plugins and is ",(0,i.jsx)(s.strong,{children:"no longer"})," in the Lua path; it is suggested to use ",(0,i.jsx)(s.code,{children:"${LUALIBDIR}"})," for all shared Lua code"]}),"\n",(0,i.jsxs)(s.li,{children:["Here are default values for the paths used by Rspamd:\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"CONFDIR"})," = ",(0,i.jsx)(s.code,{children:"${PREFIX}/etc/rspamd"})," - main path for the configuration"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"LOCAL_CONFDIR"})," = ",(0,i.jsx)(s.code,{children:"${PREFIX}/etc/rspamd"})," - path for the user's defined configuration"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"RUNDIR"})," = OS specific (",(0,i.jsx)(s.code,{children:"/var/run/rspamd"})," on Linux) - used to store volatile runtime data (e.g. PIDs)"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"DBDIR"})," = OS specific (",(0,i.jsx)(s.code,{children:"/var/lib/rspamd"})," on Linux) - used to store static runtime data (e.g. databases or cached files)"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"SHAREDIR"})," = ",(0,i.jsx)(s.code,{children:"${PREFIX}/share/rspamd"})," - used to store shared files"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"LOGDIR"})," = OS specific (",(0,i.jsx)(s.code,{children:"/var/log/rspamd"})," on Linux) - used to store Rspamd logs in file logging mode"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"LUALIBDIR"})," = ",(0,i.jsx)(s.code,{children:"${SHAREDIR}/lualib"})," - used to store shared Lua files (included in Lua path)"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"PLUGINSDIR"})," = ",(0,i.jsx)(s.code,{children:"${SHAREDIR}/plugins"})," - used to place Lua plugins"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"RULESDIR"})," = ",(0,i.jsx)(s.code,{children:"${SHAREDIR}/rules"})," - used to place Lua rules"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"LIBDIR"})," = ",(0,i.jsx)(s.code,{children:"${PREFIX}/lib/rspamd"})," - used to place shared libraries (included in RPATH and Lua CPATH)"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"WWWDIR"})," = ",(0,i.jsx)(s.code,{children:"${SHAREDIR}/www"})," - used to store static WebUI files"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["For those who are using the default packages, there should be no changes. Even in the case if you are using old ",(0,i.jsx)(s.code,{children:"${PLUGINSDIR}"})," to store your custom plugins, Rspamd will still check the old location as a fallback during the plugin loading process."]}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, an incompatible change has been introduced for users of the ",(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"coroutines based"})})," Rspamd Lua API. Starting from this version, symbols utilizing coroutines must be registered with the ",(0,i.jsx)(s.code,{children:"coro"})," lag to function properly, otherwise, they will cause Rspamd to crash. More information is available in the following ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/2789",children:"issue"}),"."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-181",children:"Migration to Rspamd 1.8.1"}),"\n",(0,i.jsx)(s.p,{children:"This version introduces several incompatibilities that may affect your setup."}),"\n",(0,i.jsx)(s.h3,{id:"general-configuration-change",children:"General configuration change"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.a,{href:"https://github.com/vstakhov/libucl",children:"Libucl"})," the library used to parse Rspamd's configuration files, has been modified in a way that prevents loading incomplete chunks of data. This means that each include file ",(0,i.jsx)(s.strong,{children:"MUST"})," be a valid configuration snippet on its own. For example, consider the following artificial example:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'.
1include "top.conf"\nvar = "bar";\n.include "bottom.conf"\n'})}),"\n",(0,i.jsx)(s.p,{children:"Where top/bottom could have something like:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:"# top.conf\n{\n"})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:"# bottom.conf\n}\n"})}),"\n",(0,i.jsx)(s.p,{children:"The following will no longer be valid: libucl now requires that all braces are properly matched."}),"\n",(0,i.jsxs)(s.p,{children:["However, it still allows implicit braces on the top-level object. So the following file will still be ",(0,i.jsx)(s.strong,{children:"valid"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'# Some include\n\nsection "foo" {\n  key = value;\n}\n\nparam = "value";\n'})}),"\n",(0,i.jsx)(s.p,{children:"or this:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'# Implicit object\nparam = "value";\n'})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"rspamadm configtest"})," will show you if your local changes to the configuration files are incompatible with the new restrictions applied."]}),"\n",(0,i.jsx)(s.h3,{id:"fuzzy-and-bayes-misses-for-large-text-messages",children:"Fuzzy and bayes misses for large text messages"}),"\n",(0,i.jsxs)(s.p,{children:["Due to a bug introduced in version 1.8.0, the algorithm used to deterministically skip words in large text parts was not functioning as intended, resulting in different words pipelines produced by different Rspamd instances. This could affect the accuracy of classification if the ",(0,i.jsx)(s.code,{children:"words_limit"})," was reached (default: ",(0,i.jsx)(s.code,{children:"words_decay = 200"})," words). For large text parts, it was possible to miss both fuzzy and Bayes classifications. While missing Bayes classification is not significant, missing fuzzy classification could be severe, potentially breaking fuzzy detection for large text parts."]}),"\n",(0,i.jsxs)(s.p,{children:["In version 1.8.1, we have fixed this issue. Since we have already broken compatibility with version 1.7.9, we have decided to increase ",(0,i.jsx)(s.code,{children:"words_decay"}),"  to 600. Please ensure that you do not override this parameter in any local or override files, such as ",(0,i.jsx)(s.code,{children:"local.d/options.inc"})," or ",(0,i.jsx)(s.code,{children:"override.d/options.inc"}),", or else compatibility with Rspamd's fuzzy storage will be lost for messages with more than ",(0,i.jsx)(s.code,{children:"words_decay"})," threshold words."]}),"\n",(0,i.jsxs)(s.h3,{id:"different-confdir-and-local_confdir-case",children:["Different ",(0,i.jsx)(s.code,{children:"CONFDIR"})," and ",(0,i.jsx)(s.code,{children:"LOCAL_CONFDIR"})," case"]}),"\n",(0,i.jsxs)(s.p,{children:["In an extremely unlikely scenario, if your custom build uses different values for the ",(0,i.jsx)(s.code,{children:"CONFDIR"})," and ",(0,i.jsx)(s.code,{children:"LOCAL_CONFDIR"})," build/startup variables, you may experience missing custom Lua rules that were previously loaded from ",(0,i.jsx)(s.code,{children:"$CONFDIR/rspamd.local.lua"}),", as they are now loaded from ",(0,i.jsx)(s.code,{children:"$LOCAL_CONFDIR/rspamd.local.lua"}),". To the best of our knowledge, this does not affect any official packages or officially supported operating systems, such as FreeBSD or OpenBSD."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-180",children:"Migration to Rspamd 1.8.0"}),"\n",(0,i.jsx)(s.p,{children:"There are several changes that may impact your setup, particularly if you use any of the following:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Clickhouse"})," module"]}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.strong,{children:"User settings"})}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"clickhouse-changes",children:"Clickhouse changes"}),"\n",(0,i.jsxs)(s.p,{children:["Starting from version 1.8, Rspamd has stopped using multiple tables and now uses a single table, ",(0,i.jsx)(s.code,{children:"rspamd"}),", with all columns. This improves performance and eliminates the need for inefficient joins in Clickhouse. If you have used the Clickhouse module prior to version 1.8, your ",(0,i.jsx)(s.strong,{children:"schemas"})," will be automatically converted. However, ",(0,i.jsx)(s.strong,{children:"the existing data"})," will ",(0,i.jsx)(s.strong,{children:"NOT"})," be converted and will remain in the old tables. It may be necessary to use a command to enforce the new schema:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:"OPTIMIZE rspamd FINAL\n"})}),"\n",(0,i.jsx)(s.p,{children:"Please note that this command may take a significant amount of time to complete if you have stored a large amount of historical data."}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, you need to update your ",(0,i.jsx)(s.strong,{children:"queries"})," that use the additional Rspamd tables, such as ",(0,i.jsx)(s.code,{children:"rspamd_urls"}),", ",(0,i.jsx)(s.code,{children:"rspamd_asn"}),", ",(0,i.jsx)(s.code,{children:"rspamd_attachments"}),", ",(0,i.jsx)(s.code,{children:"rspamd_symbols"}),", and others. All corresponding fields are now located in the ",(0,i.jsx)(s.code,{children:"rspamd"})," table. At this time, it is not possible to migrate old data from these tables."]}),"\n",(0,i.jsx)(s.h3,{id:"settings-changes",children:"Settings changes"}),"\n",(0,i.jsxs)(s.p,{children:["Rspamd now includes a ",(0,i.jsx)(s.code,{children:"settings.conf"})," file, which incorporates ",(0,i.jsx)(s.code,{children:"local.d/settings.conf"})," and ",(0,i.jsx)(s.code,{children:"override.d/settings.conf"}),". If you have used these files to store settings, please ensure that they do not conflict with the new configuration layout."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-174",children:"Migration to Rspamd 1.7.4"}),"\n",(0,i.jsxs)(s.p,{children:["The only potential issue is that Rspamd now listens on ",(0,i.jsx)(s.strong,{children:"localhost only"})," by default. This could affect configurations that rely on the previous behavior of listening on all IP addresses (e.g. ",(0,i.jsx)(s.code,{children:"*"}),")."]}),"\n",(0,i.jsx)(s.p,{children:"However, we believe that it is important to keep the default settings as restrictive as possible to avoid potential security issues, which have occurred in other projects with 'open to all' defaults."}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-170",children:"Migration to Rspamd 1.7.0"}),"\n",(0,i.jsxs)(s.p,{children:["It is recommended to run ",(0,i.jsx)(s.code,{children:"rspamadm configwizard"})," to ensure that your configuration is compatible with version 1.7. This version no longer supports the ",(0,i.jsx)(s.code,{children:"metrics"})," concept, which was never officially supported in the past. However, you may have come across instances of ",(0,i.jsx)(s.code,{children:'metric "default"'})," in various parts of the Rspamd configuration and settings."]}
1),"\n",(0,i.jsxs)(s.p,{children:["In version 1.7, we will continue to support the old ",(0,i.jsx)(s.code,{children:"metric"})," keyword and scores defined under this section, such as in ",(0,i.jsx)(s.code,{children:"rspamd.conf.local"}),". However, it is now recommended to define symbol scores in group settings (",(0,i.jsx)(s.code,{children:"local.d/group_*.conf"}),"), which can be found in the ",(0,i.jsx)(s.code,{children:"etc/rspamd/scores.d"})," folder."]}),"\n",(0,i.jsx)(s.p,{children:"There is no need to undertake any action if you have your custom scores defined in the legacy files. Rspamd will continue support of definitions in these files."}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rspamd-165",children:"Migrating to Rspamd 1.6.5"}),"\n",(0,i.jsx)(s.p,{children:"As a result of several important fixes made to tokenization algorithms, it is possible that the statistics and fuzzy modules may lose some precision. In these cases, you may want to consider re-learning your databases to improve the accuracy of filtering."}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rspamd-160",children:"Migrating to Rspamd 1.6.0"}),"\n",(0,i.jsxs)(s.p,{children:["In this version, due to the implementation of the new milter interface, there is an important incompatible change that you may need to address if you use the ",(0,i.jsx)(s.code,{children:"rmilter_headers"})," module. This module has been renamed to ",(0,i.jsx)(s.code,{children:"milter_headers"})," and the corresponding protocol section is now named ",(0,i.jsx)(s.code,{children:"milter"})," instead of ",(0,i.jsx)(s.code,{children:"rmilter"}),". If you have configured this module inside ",(0,i.jsx)(s.code,{children:"local.d/rmilter_headers.conf"})," or in ",(0,i.jsx)(s.code,{children:"override.d/rmilter_headers.conf"}),", then no action is required, as these files will still be loaded by the renamed module. Otherwise, you will need to change the section name from ",(0,i.jsx)(s.code,{children:"rmilter_headers"})," to ",(0,i.jsx)(s.code,{children:"milter_headers"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"milter_headers"})," module now skips adding headers for local networks and authenticated users by default. This behavior can be re-enabled by setting ",(0,i.jsx)(s.code,{children:"skip_local = false"})," and/or ",(0,i.jsx)(s.code,{children:"skip_authenticated = false"})," in the module configuration. Alternatively, you can set ",(0,i.jsx)(s.code,{children:"authenticated_headers"})," and/or ",(0,i.jsx)(s.code,{children:"local_headers"})," to a list of headers that should not be skipped."]}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, a ",(0,i.jsx)(s.a,{href:"/workers/rspamd_proxy",children:"proxy worker"})," has been added to the default configuration and listens on all interfaces on TCP port 11332. If you do not need it, you can set ",(0,i.jsx)(s.code,{children:"enabled = false"})," in ",(0,i.jsx)(s.code,{children:"local.d/worker-proxy.inc"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["This release also eliminates the configuration split for systemd/sysv platforms. To ensure proper functionality, custom init scripts should utilize ",(0,i.jsx)(s.code,{children:"rspamd.conf"})," instead of ",(0,i.jsx)(s.code,{children:"rspamd.sysvinit.conf"}),". For those utilizing systemd and prefer logging to the systemd journal, the following should be added to ",(0,i.jsx)(s.code,{children:"local.d/logging.inc"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'systemd = true;\ntype = "console";\n'})}),"\n",(0,i.jsxs)(s.p,{children:["A significant overhaul of the Lua libraries has occurred in Rspamd 1.6. Some custom scripts may fail if they are loaded prior to ",(0,i.jsx)(s.code,{children:"rspamd.lua"})," or if manual modifications have been made to ",(0,i.jsx)(s.code,{children:"rspamd.lua"}),"should be loaded before all custom scripts. This is the default behavior, however, in highly customized setups it may cause issues. In general, it is crucial to ensure that the following line is present in your code (found at the very beginning of ",(0,i.jsx)(s.code,{children:"rspamd.lua"}),"):"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:'require "global_functions" ()\n'})}),"\n",(0,i.jsxs)(s.p,{children:["The Rmilter tool is now deprecated 
1in favor of milter protocol support in the ",(0,i.jsx)(s.a,{href:"/workers/rspamd_proxy",children:"rspamd proxy"}),". Examples of some specific features previously implemented in Rmilter can be found in the ",(0,i.jsx)(s.a,{href:"/modules/milter_headers",children:"milter headers module"}),". It is recommended to migrate from Rmilter as soon as possible, as Rspamd 1.6 will be the last version to support the Rmilter tool. In future major releases (starting from 1.7), there will be ",(0,i.jsx)(s.strong,{children:"no guarantees"})," of compatibility with Rmilter."]}),"\n",(0,i.jsxs)(s.p,{children:["For example, if you need the old behaviour for ",(0,i.jsx)(s.code,{children:"extended_spam_headers"})," in Rmilter is desired, the following snippet can be added to ",(0,i.jsx)(s.code,{children:"local.d/milter_headers.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/milter_headers.conf\nextended_spam_headers = true;\n"})}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rspamd-153",children:"Migrating to Rspamd 1.5.3"}),"\n",(0,i.jsxs)(s.p,{children:["The rspamd_update module has been disabled by default; if you need it please set ",(0,i.jsx)(s.code,{children:"enabled = true"})," in ",(0,i.jsx)(s.code,{children:"local.d/rspamd_update.conf"}),"."]}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rspamd-152",children:"Migrating to Rspamd 1.5.2"}),"\n",(0,i.jsxs)(s.p,{children:["New configuration files have been added for the following modules which previously missed them; if you have previously configured one of these modules in ",(0,i.jsx)(s.code,{children:"rspamd.conf.local"})," please move your configuration to ",(0,i.jsx)(s.code,{children:"rspamd.conf.override"})," to ensure that it is preserved verbatim or rework your configuration to use ",(0,i.jsx)(s.code,{children:"local.d/[module_name].conf"})," instead."]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"antivirus"}),"\n",(0,i.jsx)(s.li,{children:"dkim_signing"}),"\n",(0,i.jsxs)(s.li,{children:["mx_check (set ",(0,i.jsx)(s.code,{children:"enabled = true"})," if you use ",(0,i.jsx)(s.code,{children:"local.d"}),")"]}),"\n",(0,i.jsx)(s.li,{children:"replies"}),"\n",(0,i.jsx)(s.li,{children:"spamassassin"}),"\n",(0,i.jsx)(s.li,{children:"trie"}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rspamd-15",children:"Migrating to Rspamd 1.5"}),"\n",(0,i.jsxs)(s.p,{children:["New configuration files have been added for the following modules which previously lacked them: ",(0,i.jsx)(s.code,{children:"greylist"}),", ",(0,i.jsx)(s.code,{children:"metadata_exporter"})," and ",(0,i.jsx)(s.code,{children:"metric_exporter."})," If you have previously configured one of these modules in ",(0,i.jsx)(s.code,{children:"rspamd.conf.local"}),", it is recommended to move your configuration to ",(0,i.jsx)(s.code,{children:"rspamd.conf.override"})," to ensure that it is preserved verbatim, or to rework your configuration to use ",(0,i.jsx)(s.code,{children:"local.d/[module_name].conf"})," instead."]}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, if composites have been defined in ",(0,i.jsx)(s.code,{children:"local.d/composites.conf"})," or ",(0,i.jsx)(s.code,{children:"override.d/composites.conf"}),", these should be moved to ",(0,i.jsx)(s.code,{children:"rspamd.conf.local"})," or reworked to the new format. An example can be found in ",(0,i.jsx)(s.code,{children:"/etc/rspamd/composites.conf"}),"."]}),"\n",(0,i.jsx)(s.p,{children:"You are also suggested to disable outdated and no longer supported features of Rmilter and switch them to Rspamd:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Greylisting - provided by ",(0,i.jsx)(s.a,{href:"/modules/greylisting",children:"greylisting module"})]}),"\n",(0,i.jsxs)(s.li,{children:["Ratelimit - is done by ",(0,i.jsx)(s.a,{href:"/modules/ratelimit",children:"ratelimit module"})]}),"\n",(0,i.jsxs)(s.li,{children:["Replies whitelisting - is implemented in ",(0,i.jsx)(s.a,{href:"/modules/replies",children:"replies module"})]}),"\n",(0,i.jsxs)(s.li,{children:["Antivirus filtering - provided now by ",(0,i.jsx)(s.a,{href:"/modules/antivirus",children:"antivirus module"})]}),"\n",(0,i.jsxs)(s.li,{children:["DCC checks - are now done in ",(0,i.jsx)(s.a,{href:"/modules/dcc",children:"dcc module"})]}),"\n",(0,i.jsxs)(s.li,{children:["Dkim signing - can be done now by using of ",(0,i.jsx)(s.a,{href:"/modules/dkim#dkim-signatures",children:"dkim module"})," and also by a more simple ",(0,i.jsx)(s.a,{href:"/modules/dkim_signing",children:"dkim signing module"})]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"All duplicate features are still present in Rmilter for compatibility purposes. However, it is unlikely that any further development or bug fixes will be applied to them."}),"\n",(0,i.jsxs)(s.p,{children:["From version ",(0,i.jsx)(s.code,{children:"1.9.1"})," it is possible to specify ",(0,i.jsx)(s.code,{children:"enable"})," option in ",(0,i.jsx)(s.code,{children:"greylisting"})," and ",(0,i.jsx)(s.code,{children:"ratelimit"})," sections. It is also possible for ",(0,i.jsx)(s.code,{children:"dkim"})," section since ",(0,i.jsx)(s.code,{children:"1.9.2"}),". These options are ",(0,i.jsx)(s.code,{children:"true"})," by default. Here is an example of configuration where greylisting and ratelimit are disabled:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# /etc/rmilter.conf.local\nlimits {\
1n    enable = false;\n}\ngreylisting {\n    enable = false;\n}\ndkim {\n    enable = false;\n}\n"})}),"\n",(0,i.jsxs)(s.p,{children:["These options are enabled by default solely for compatibility reasons. In future Rmilter releases, they will be ",(0,i.jsx)(s.strong,{children:"DISABLED"})," by default."]}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rmilter-1100-and-rspamd-140",children:"Migrating to Rmilter 1.10.0 and Rspamd 1.4.0"}),"\n",(0,i.jsxs)(s.p,{children:["TThe default passwords, specifically ",(0,i.jsx)(s.code,{children:"q1"})," and ",(0,i.jsx)(s.code,{children:"q2"}),", are no longer permitted for remote authentication. This change is a result of the widespread misuse of these ",(0,i.jsx)(s.strong,{children:"example"})," passwords and the potential security risks posed by some Rspamd users."]}),"\n",(0,i.jsx)(s.h2,{id:"migrating-to-rmilter-191-and-rspamd-131",children:"Migrating to Rmilter 1.9.1 and Rspamd 1.3.1"}),"\n",(0,i.jsxs)(s.p,{children:["In these releases, systemd socket activation has been removed. Note that upon upgrading on Debian platforms, Rmilter may not restart correctly. To resolve this, please run ",(0,i.jsx)(s.code,{children:"systemctl restart rmilter"})," after installing the package. On the other hand, Rspamd is expected to restart correctly upon upgrade. Additionally, both Rspamd and Rmilter should be configured to automatically run on reboot post-upgrade."]}),"\n",(0,i.jsx)(s.h2,{id:"migrating-from-rmilter-18-to-rmilter-19",children:"Migrating from Rmilter 1.8 to Rmilter 1.9"}),"\n",(0,i.jsx)(s.p,{children:"Please note that there are a few changes to the supported features in this release:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["beanstalk support has been removed from Rmilter in honor of Redis ",(0,i.jsx)(s.a,{href:"https://redis.io/docs/interact/pubsub/",children:"pub/sub"}),", you must remove the whole ",(0,i.jsx)(s.code,{children:"beanstalk"})," section from the configuration file"]}),"\n",(0,i.jsxs)(s.li,{children:["auto whitelist for greylisting is no longer supported as it has been broken from the very beginning, you must remove all ",(0,i.jsx)(s.code,{children:"awl"})," options from the greylisting section"]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["If you have been using Beanstalk for certain purposes, you can transition to using Redis ",(0,i.jsx)(s.a,{href:"https://redis.io/docs/interact/pubsub/",children:"pub/sub"}),". The ",(0,i.jsx)(s.code,{children:"redis"})," section includes settings such as ",(0,i.jsx)(s.code,{children:"spam_servers"})," and ",(0,i.jsx)(s.code,{children:"spam_channel"})," for sending spam, and ",(0,i.jsx)(s.code,{children:"copy_servers"}),", ",(0,i.jsx)(s.code,{children:"copy_prob"}),", and ",(0,i.jsx)(s.code,{children:"copy_channel"})," for sending message copies, which can help you reproduce Beanstalk functions using Redis."]}),"\n",(0,i.jsxs)(s.p,{children:["Rmilter now provides additional options for configuring your local settings. You can now use ",(0,i.jsx)(s.code,{children:"rmilter.conf.local"})," and ",(0,i.jsx)(s.code,{children:"rmilter.conf.d/*.conf"})," files to override the default configuration."]}),"\n",(0,i.jsxs)(s.p,{children:["Additionally, Rmilter no longer includes several SpamAssassin-compatible headers such as ",(0,i.jsx)(s.code,{children:"X-Spam-Status"}),", ",(0,i.jsx)(s.code,{children:"X-Spam-Level"}),", and ",(0,i.jsx)(s.code,{children:"X-Spamd-Bar"}),". Instead, Rmilter now supports adding and removing custom headers as instructed by Rspamd (version 1.3.0 or higher). To restore the removed headers, you can use the example script provided below, which should be added to ",(0,i.jsx)(s.code,{children:"/etc/rspamd/rspamd.local.lua"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"rspamd_config:register_symbol({\n  name = 'RMILTER_HEADERS',\n  type = 'postfilter',\n  priority = 10,\n  callback = function(task)\n    local metric_score = task:get_metric_score('default')\n    local score = metric_score[1]\n    local required_score = metric_score[2]\n    -- X-Spamd-Bar & X-Spam-Level\n    local spambar\n    local spamlevel = ''\n    if score <= -1 then\n      spambar = string.rep('-', score*-1)\n    elseif score >= 1 then\n      spambar = string.rep('+', score)\n      spamlevel = string.rep('*', score)\n    else\n      spambar = '/'\n    end\n    -- X-Spam-Status\n    local is_spam\n    local spamstatus\n    local action = task:get_metric_action('default')\n    if action ~= 'no action' and action ~= 'greylist' then\n      is_spam = 'Yes'\n    else\n      is_spam = 'No'\n    end\n    spamstatus = is_spam .. ', score=' .. string.format('%.2f', score)\n    -- Add headers\n    task:set_rmilter_reply({\n      add_headers = {\n        ['X-Spamd-Bar'] = spambar,\n        ['X-Spam-Level'] = spamlevel,\n        ['X-Spam-Status'] = spamstatus\n      },\n      remove_headers = {\n        ['X-Spamd-Bar'] = 1,\n        ['X-Spam-Level'] = 1,\n        ['X-Spam-Status'] = 1\n      }\n    })\n  end\n})\n"})}),"\n",(0,i.jsx)(s.h2,{id:"migrating-from-rspamd-12-to-rspamd-13",children:"Migrating from Rspamd 1.2 to Rspamd 1.3"}),"\n",(0,i.jsx)(s.p,{children:"Rspamd version 1.3 does not introduce any incompatible changes"}),"\n",(0,i.jsx)(s.h2,{id:"migrating-from-rspamd-11-to-rspamd-12",children:"Migrating from Rspamd 1.1 to Rspamd 1.2"}),"\n",(0,i.jsx)(s.p,{children:"Rspamd version 1.2 does not introduce any incompatible changes"}),"\n",(0,i.jsx)(s.h2,{id:"migrating-from-rspamd-10-to-rspamd-11",children:"Migrating from Rspamd 1.0 to Rspamd 1.1"}),"\n",(0,i.jsx)(s.p,{children:"Please note that there is an incompatible change in the per-user statistics behavior for users with per-user statistics enabled."}),"\n",(0,i.jsxs)(s.p,{children:["Both ",(0,i.jsx)(s.code,{children:"redis"})," and ",(0,i.jsx)(s.code,{children:"sqlite3"})," now follow a consistent approach for per-user statistics:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["If per-user statistics is enabled check per-user tokens ",(0,i.jsx)(s.strong,{children:"ONLY"})]}),"\n",(0,i.jsxs)(s.li,{children:["If per-user statistics is not enabled then check common tokens ",(0,i.jsx)(s.strong,{children:"ONLY"})]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"If the previous behavior is desired, a separate classifier for per-user statistics must be implemented, for example:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'    classifier {\n        tokenizer {\n            name = "osb";\n        }\n        name = "bayes_user";\n        min_tokens = 11;\n        backend = "sqlite3";\n        per_language = true;\n        per_user = true;\n        statfile {\n            path = "/tmp/bayes.spam.sqlite";\n            symbol = "BAYES_SPAM_USER";\n        }\n        statfile {\n            path = "/tmp/bayes.ham.sqlite";\n            symbol = "BAYES_HAM_USER";\n        }\n    }\n    classifier {\n        tokenizer {\n            name = "osb";\n        }\n        name = "bayes";\n        min_tokens = 11;\n        backend = "sqlite3";\n        per_language = true;\n        statfile {\n            path = "/tmp/bayes.spam.sqlite";\n            symbol = "BAYES_SPAM";\n        }\n        statfile {\n            path = "/tmp/bayes.ham.sqlite";\n            symbol = "BAYES_HAM";\n        }\n    }\n'})}),"\n",(0,i.jsx)(s.h2,{id:"migrating-from-rspamd-09-to-rspamd-10",children:"Migrating from Rspamd 0.9 to Rspamd 1.0"}),"\n",(0,i.jsxs)(s.p,{children:["Rspamd 1.0 has introduced changes to the default settings for statistics tokenization. The new default setting is ",(0,i.jsx)(s.code,{children:"modern"}),", which generates tokens from normalized words and includes various improvements. However, these changes are not compatible with the statistics model used in pre-1.0 versions. To use these new features you should either ",(0,i.jsx)(s.strong,{children:"relearn"}
1)," your statistics or continue using your old statistics ",(0,i.jsx)(s.strong,{children:"without"})," new features by adding a ",(0,i.jsx)(s.code,{children:"compat"})," parameter:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"classifier {\n...\n    tokenizer {\n        compat = true;\n    }\n...\n}\n"})}),"\n",(0,i.jsxs)(s.p,{children:["The recommended way to store statistics now is the ",(0,i.jsx)(s.code,{children:"sqlite3"})," backend (which is incompatible with the old mmap backend):"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'classifier {\n    type = "bayes";\n    tokenizer {\n        name = "osb";\n    }\n    cache {\n        path = "${DBDIR}/learn_cache.sqlite";\n    }\n    min_tokens = 11;\n    backend = "sqlite3";\n    languages_enabled = true;\n    statfile {\n        symbol = "BAYES_HAM";\n        path = "${DBDIR}/bayes.ham.sqlite";\n        spam = false;\n    }\n    statfile {\n        symbol = "BAYES_SPAM";\n        path = "${DBDIR}/bayes.spam.sqlite";\n        spam = true;\n    }\n}\n'})}),"\n",(0,i.jsx)(s.h2,{id:"migrating-from-rspamd-06-to-rspamd-07",children:"Migrating from Rspamd 0.6 to Rspamd 0.7"}),"\n",(0,i.jsx)(s.h3,{id:"webui-changes",children:"WebUI changes"}),"\n",(0,i.jsx)(s.p,{children:"The Rspamd web interface is now a part of the Rspamd distribution. Additionally, Rspamd itself now serves all static files, eliminating the need for a separate web server. As a result, the WebUI worker has been removed, and the controller now acts as both a web browser and the rspamc client. However, it is still recommended to set up a full-featured HTTP server in front of Rspamd for added security features such as TLS and access controls."}),"\n",(0,i.jsxs)(s.p,{children:["Furthermore, there are now two levels of password protection for Rspamd: ",(0,i.jsx)(s.code,{children:"password"})," for read-only commands and ",(0,i.jsx)(s.code,{children:"enable_password"})," for commands that change data. If ",(0,i.jsx)(s.code,{children:"enable_password"})," is not specified, ",(0,i.jsx)(s.code,{children:"password"})," is used for both types of commands."]}),"\n",(0,i.jsx)(s.p,{children:"Here is an example of the full configuration of the Rspamd controller worker to serve the WebUI:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'worker {\n\ttype = "controller";\n\tbind_socket = "localhost:11334";\n\tcount = 1;\n\tpassword = "q1";\n\tenable_password = "q2";\n\tsecure_ip = "127.0.0.1"; # Allows to use *all* commands from this IP\n\tstatic_dir = "${WWWDIR}";\n}\n'})}),"\n",(0,i.jsx)(s.h3,{id:"settings-changes-1",children:"Settings changes"}),"\n",(0,i.jsxs)(s.p,{children:["The settings system in Rspamd has undergone a complete overhaul. It is now implemented as a Lua plugin that registers pre-filters and assigns settings based on dynamic maps or a static configuration. To use the new settings system, please refer to the updated ",(0,i.jsx)(s.a,{href:"/configuration/settings",children:"documentation"}),". The previous settings system has been entirely removed from Rspamd."]}),"\n",(0,i.jsx)(s.h3,{id:"lua-changes",children:"Lua changes"}),"\n",(0,i.jsx)(s.p,{children:"Please be aware that there have been significant changes to the Lua API in this release, some of which may result in compatibility issues."}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["many superglobals are removed: now Rspamd modules need to be loaded explicitly,\nthe only global remaining is ",(0,i.jsx)(s.code,{children:"rspamd_config"}),". This affects the following modules:\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"rspamd_logger"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"rspamd_ip"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"rspamd_http"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"rspamd_cdb"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"rspamd_regexp"})}),"\n",(0,i.jsx)(s.li,{children:(0,i.jsx)(s.code,{children:"rspamd_trie"})}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:'local rspamd_logger = require "rspamd_logger"\nlocal rspamd_trie = require "rspamd_trie"\nlocal rspamd_cdb = require "rspamd_cdb"\nlocal rspamd_ip = require "rspamd_ip"\nlocal rspamd_regexp = require "rspamd_regexp"\n'})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["new system of symbols registration: now symbols can be registered by adding new indices to ",(0,i.jsx)(s.code,{children:"rspamd_config"})," object. Old version:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"local reconf = config['regexp']\nreconf['SYMBOL'] = function(task)\n...\nend\n"})}),"\n",(0,i.jsx)(s.p,{children:"new one:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"rspamd_config.SYMBOL = function(task)\n...\nend\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"rspamd_message"})," has been ",(0,i.jsx)(s.strong,{children:"removed"})," completely. Instead, task methods should be used to access message data. This includes methods such as:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"get_date"})," - this method now returns a date for the task and message based on the provided arguments:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"local dm = task:get_date{format = 'message'} -- MIME message date\nlocal dt = task:get_date{format = 'connect'} -- check date\n"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"get_header"})," - this function has undergone significant changes. The new version of ",(0,i.jsx)(s.code,{children:"get_header"})," returns a decoded string, ",(0,i.jsx)(s.code,{children:"get_header_raw"})," returns an undecoded string, and ",(0,i.jsx)(s.code,{children:"get_header_full"})," returns a full list of tables. For more information, please refer to the updated ",(0,i.jsx)(s.a,{href:"/lua/rspamd_task",children:"documentation"}),". You may need to update your existing code that uses the ",(0,i.jsx)(s.code,{children:"task:get_header"}
1)," method.\nOld version:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"function kmail_msgid (task)\n\tlocal msg = task:get_message()\n\tlocal header_msgid = msg:get_header('Message-Id')\n\tif header_msgid then\n\t\t-- header_from and header_msgid are tables\n\t\tfor _,header_from in ipairs(msg:get_header('From')) do\n\t    \t...\n\t\tend\n\tend\n\treturn false\nend\n"})}),"\n",(0,i.jsx)(s.p,{children:"new one:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"function kmail_msgid (task)\n\tlocal header_msgid = task:get_header('Message-Id')\n\tif header_msgid then\n\t\tlocal header_from = task:get_header('From')\n\t\t-- header_from and header_msgid are strings\n\tend\n\treturn false\nend\n"})}),"\n",(0,i.jsx)(s.p,{children:"or with the full version:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"rspamd_config.FORGED_GENERIC_RECEIVED5 = function (task)\n\tlocal headers_recv = task:get_header_full('Received')\n\tif headers_recv then\n\t\t-- headers_recv is now the list of tables\n\t\tfor _,header_r in ipairs(headers_recv) do\n\t\t\tif re:match(header_r['value']) then\n\t\t\t\treturn true\n\t\t\tend\n\t\tend\n\tend\n    return false\nend\n"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"get_from"})," and ",(0,i.jsx)(s.code,{children:"get_recipients"})," now accept optional numeric arguments that determine where to retrieve the sender and recipients for a message. By default, this argument is set to ",(0,i.jsx)(s.code,{children:"0"}),", which means that data is initially checked in the SMTP envelope (i.e., ",(0,i.jsx)(s.code,{children:"MAIL FROM"})," and ",(0,i.jsx)(s.code,{children:"RCPT TO"})," SMTP commands) and if the envelope data is not available, it is then obtained from MIME headers. A value of ",(0,i.jsx)(s.code,{children:"1"})," means that data is checked in the envelope only, while ",(0,i.jsx)(s.code,{children:"2"})," switches the mode to MIME headers. Here is an example from the ",(0,i.jsx)(s.code,{children:"forged_recipients"})," module:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"-- Check sender\nlocal smtp_from = task:get_from(1)\nif smtp_from then\n\tlocal mime_from = task:get_from(2)\n\tif not mime_from or\n\t\t\tnot (string.lower(mime_from[1]['addr']) ==\n\t\t\tstring.lower(smtp_from[1]['addr'])) then\n\t\ttask:insert_result(symbol_sender, 1)\n\tend\nend\n"})}),"\n",(0,i.jsx)(s.h3,{id:"protocol-changes",children:"Protocol changes"}),"\n",(0,i.jsxs)(s.p,{children:["Rspamd now exclusively uses the HTTP protocol for all operations, making the use of additional client libraries unnecessary. Additionally, the fallback to the older ",(0,i.jsx)(s.code,{children:"spamc"})," protocol has been implemented to ensure automatic compatibility with software such as ",(0,i.jsx)(s.code,{children:"rmilter"})," and other programs that use the ",(0,i.jsx)(s.code,{children:"rspamc"})," protocol."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-410",children:"Migration to Rspamd 4.1.0"}),"\n",(0,i.jsxs)(s.h3,{id:"1-default-rspamdcom-fuzzy-rule-now-discovered-via-srv",children:["1. Default ",(0,i.jsx)(s.code,{children:"rspamd.com"})," Fuzzy Rule Now Discovered via SRV"]}),"\n",(0,i.jsxs)(s.p,{children:["The bundled default fuzzy rule no longer carries a hardcoded round-robin host list. It now uses ",(0,i.jsx)(s.code,{children:"service=fuzzy+rspamd.com"}),", which makes the upstream parser resolve the ",(0,i.jsx)(s.code,{children:"_fuzzy._tcp.rspamd.com"})," SRV record and manage backends and ports entirely in DNS (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/b912c67a3",children:"b912c67a3"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," All installs that rely on the shipped ",(0,i.jsx)(s.code,{children:"rspamd.com"})," fuzzy rule. Most deployments need ",(0,i.jsx)(s.strong,{children:"no action"})," \u2014 the resolver handles SRV transparently, the legacy ",(0,i.jsx)(s.code,{children:"fuzzy1"}),"/",(0,i.jsx)(s.code,{children:"fuzzy2"})," hostnames still resolve to every live backend, and installs that already pinned an explicit server string are unchanged. You only need to act if your resolver cannot serve SRV records (split-horizon DNS, a restrictive forwarder, or a firewall that drops SRV)."]}),"\n",(0,i.jsxs)(s.blockquote,{children:["\n",(0,i.jsxs)(s.p,{children:["An earlier regression made ",(0,i.jsx)(s.code,{children:"rspamadm configtest"})," reject an SRV-only rule with ",(0,i.jsx)(s.code,{children:"no servers defined for fuzzy rule with name: rspamd.com"}),"; that is fixed in ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/64e1a6b5e",children:"64e1a6b5e"}),". A resolver that silently fails to return SRV records will still leave the rule with no usable backends at runtime."]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"Verify SRV resolution works in your environment:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"dig SRV _fuzzy._tcp.rspamd.com +short\n"})}),"\n",(0,i.jsxs)(s.p,{children:["If SRV cannot be resolved, pin explicit servers back in ",(0,i.jsx)(s.code,{children:"local.d/fuzzy_check.conf"})," (the legacy hostnames remain valid):"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'rule "rspamd.com" {\n  # restore the pre-4.1.0 hostname-based selection\n  servers = "fuzzy1.rspamd.com:11335,fuzzy2.rspamd.com:11335";\n}\n'})}),"\n",(0,i.jsxs)(s.h3,{id:"2-mx_check-module-reworked-symbols-renamed-options-deprecated",children:["2. ",(0,i.jsx)(s.code,{children:"mx_check"})," Module Reworked: Symbols Renamed, Options Deprecated"]}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"mx_check"})," module was rewritten with a three-layer Redis cache, finer-grained outcomes, and IP-class classification (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/219fd9818",children:"219fd9818"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f70285614",children:"f70285614"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/0eb4c443e",children:"0eb4c443e"}),"). The symbol surface and several option names changed."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Only deployments that explicitly enabled and configured ",(0,i.jsx)(s.code,{children:"mx_check"}),". The module is not active in a stock configuration."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"a) Renamed symbols."})," ",(0,i.jsx)(s.code,{children:"MX_NXDOMAIN"})," and ",(0,i.jsx)(s.code,{children:"MX_MISSING"})," no longer exist \u2014 both collapse into ",(0,i.jsx)(s.code,{children:"MX_NONE"}),". A large set of new symbols was added (",(0,i.jsx)(s.code,{children:"MX_GOOD"}),", ",(0,i.jsx)(s.code,{children:"MX_INVALID"}),", ",(0,i.jsx)(s.code,{children:"MX_A_*"}),", ",(0,i.jsx)(s.code,{children:"MX_LOCAL_ONLY"}),"/",(0,i.jsx)(s.code,{children:"MX_LOCAL_MIX"}),", ",(0,i.jsx)(s.code,{children:"MX_BOGON_ONLY"}),"/",(0,i.jsx)(s.code,{children:"MX_BOGON_MIX"}),", ",(0,i.jsx)(s.code,{children:"MX_NULL"}),", ",(0,i.jsx)(s.code,{children:"MX_BAD"}),", ",(0,i.jsx)(s.code,{children:"MX_IP_BAD"}),", ",(0,i.jsx)(s.code,{children:"MX_INFLIGHT"}),", ",(0,i.jsx)(s.code,{children:"MX_TIMEOUT_*"}),", ",(0,i.jsx)(s.code,{children:"MX_REFUSED"}),", ",(0,i.jsx)(s.code,{children:"MX_ERROR"}),", ",(0,i.jsx)(s.code,{children:"MX_REDIS_ERROR"}),", \u2026). Update any custom scores, composites, ",(0,i.jsx)(s.code,{children:"force_actions"}),", or rules that referenced ",(0,i.jsx)(s.code,{children:"MX_NXDOMAIN"})," / ",(0,i.jsx)(s.code,{children:"MX_MISSING"}),". Scores are now exposed through a dedicated ",(0,i.jsx)(s.code,{children:"mx"})," group; tune them in ",(0,i.jsx)(s.code,{children:"local.d/mx_group.conf"})," or ",(0,i.jsx)(s.code,{children:"override.d/mx_group.conf"})," instead of hardcoding per-symbol overrides:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'# local.d/mx_group.conf\nsymbols {\n  "MX_NONE"   { score = 0.5; }\n  "MX_INVALID" { score = 2.0; }\n}\n'})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"b) Renamed options."})," ",(0,i.jsx)(s.code,{children:"timeout"})," \u2192 ",(0,i.jsx)(s.code,{children:"connect_timeout"}),", and ",(0,i.jsx)(s.code,{children:"wait_for_greeting"})," \u2192 ",(0,i.jsx)(s.code,{children:"verify_greeting"}),". The legacy names are still accepted but log a deprecation warning, and the module warns if a legacy key and its replacement are both set (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/37027b0ee",children:"37027b0ee"}),"). Update ",(0,i.jsx)(s.code,{children:"local.d/mx_check.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# before:\n#   timeout = 5s;\n#   wait_for_greeting = true;\n# after:\nconnect_timeout = 5s;\nverify_greeting  = true;\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"c) Removed option."})," ",(0,i.jsx)(s.code,{children:"reject_nxdomain_mx"})," was removed. Null-MX rejection is now handled by ",(0,i.jsx)(s.code,{children:"reject_null_mx"})," (gated by ",(0,i.jsx)(s.code,{children:"reject_authorized"})," / ",(0,i.jsx)(s.code,{children:"reject_local"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"d) Redis cache layout."})," The single domain-keyed cache is replaced by three namespaces (",(0,i.jsx)(s.code,{children:"<key_prefix>:d:"}),", ",(0,i.jsx)(s.code,{children:":m:"}),", ",(0,i.jsx)(s.code,{children:":i:"}),"). Old single-layer entries are treated as a cache miss and overwritten in place \u2014 no manual flush is required, but expect a cold cache (extra DNS/TCP work) for a 
1while after the upgrade."]}),"\n",(0,i.jsxs)(s.h3,{id:"3-neural-ann-digest-changes-under-disable_symbols_input",children:["3. Neural ANN Digest Changes Under ",(0,i.jsx)(s.code,{children:"disable_symbols_input"})]}),"\n",(0,i.jsxs)(s.p,{children:["When ",(0,i.jsx)(s.code,{children:"disable_symbols_input = true"}),", the profile digest that forms part of the trained ANN's Redis key (",(0,i.jsx)(s.code,{children:"rn_<rule>_<settings>_<digest>_<v>"}),") is now derived from the providers configuration instead of the symbol list (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/1323fc5fb",children:"1323fc5fb"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ed97ec8a7",children:"ed97ec8a7"}),"). This makes the digest stable across unrelated symbol changes (a new RBL, multimap, or SA-style rule), but it rotates ",(0,i.jsx)(s.strong,{children:"once"})," on the first start after upgrading."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Only neural deployments running with ",(0,i.jsx)(s.code,{children:"disable_symbols_input = true"})," ",(0,i.jsx)(s.strong,{children:"and"})," an already-trained ANN stored in Redis. On first 4.1.0 start the existing ANN is orphaned under its old key, and inference drops to zero until a new model trains (potentially weeks under realistic class imbalance)."]}),"\n",(0,i.jsx)(s.p,{children:"You have two options:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Let it retrain"})," \u2014 accept reduced neural coverage until enough samples accumulate."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Migrate the trained model once."})," Locate the orphaned key and copy it to the new digest. After this one migration the digest stays stable across future config changes:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"# inspect existing neural keys to find the old/new digest pair\nredis-cli --scan --pattern 'rn_*'\n\n# copy the trained model from the old digest to the new one\nredis-cli COPY rn_<rule>_<settings>_<old>_<v> rn_<rule>_<settings>_<new>_<v>\n"})}),"\n",(0,i.jsxs)(s.h3,{id:"4-mailto-urls-stored-in-canonical-slash-less-form",children:["4. ",(0,i.jsx)(s.code,{children:"mailto:"})," URLs Stored in Canonical Slash-less Form"]}),"\n",(0,i.jsxs)(s.p,{children:["Bare email addresses and explicit ",(0,i.jsx)(s.code,{children:"mailto:"})," URLs are now canonicalised to the same slash-less ",(0,i.jsx)(s.code,{children:"mailto:user@host"})," string, per RFC 6068 (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a4ae51536",children:"a4ae51536"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/6512946b2",children:"6512946b2"}),"). Previously a bare email was injected as a literal ",(0,i.jsx)(s.code,{children:"mailto://user@host"})," prefix, so the two forms landed in different dedup buckets and the same address could appear twice."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Only custom rules, multimaps, or selectors that string-match the literal ",(0,i.jsx)(s.code,{children:"mailto://"})," form of an email URL returned by ",(0,i.jsx)(s.code,{children:"task:get_urls()"}),". Those patterns now need to match ",(0,i.jsx)(s.code,{children:"mailto:"})," (no ",(0,i.jsx)(s.code,{children:"//"}),"). As a side effect, a message containing both a bare address and the same explicit ",(0,i.jsx)(s.code,{children:"mailto:"})," link now yields a single deduplicated URL rather than two, which can change URL counts in custom logic."]}),"\n",(0,i.jsx)(s.p,{children:"Update any such patterns, for example:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'# before:  /^mailto:\\/\\//\n# after:\nmailto_url = "mailto:";\n'})}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-411",children:"Migration to Rspamd 4.1.1"}),"\n",(0,i.jsxs)(s.h3,{id:"1-checkv3-reply-format-is-now-content-negotiated",children:["1. ",(0,i.jsx)(s.code,{children:"/checkv3"})," Reply Format Is Now Content-Negotiated"]}),"\n",(0,i.jsxs)(s.p,{children:["Before 4.1.1 the ",(0,i.jsx)(s.code,{children:"/checkv3"})," endpoint ignored the ",(0,i.jsx)(s.code,{children:"Accept"})," header (beyond a JSON-vs-msgpack toggle) and always returned a hard-coded ",(0,i.jsx)(s.code,{children:"multipart/mixed"})," body. As of 4.1.1 the reply representation is negotiated from ",(0,i.jsx)(s.code,{children:"Accept"}),", and compression from ",(0,i.jsx)(s.code,{children:"Accept-Encoding"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/6fa991b5f",children:"6fa991b5f"}),", PR ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6083",children:"#6083"}),"). The ",(0,i.jsx)(s.strong,{children:"default reply format has changed"}),", so existing clients that assumed the old fixed ",(0,i.jsx)(s.code,{children:"multipart/mixed"})," output will receive a different body."]}),"\n",(0,i.jsx)(s.p,{children:"New negotiation rules:"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.th,{children:[(0,i.jsx)(s.code,{children:"Accept"})," request header"]}),(0,i.jsx)(s.th,{children:"Reply"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:["absent / ",(0,i.jsx)(s.code,{children:"*/*"})]}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"multipart/form-data"})," (",(0,i.jsx)(s.strong,{children:"new default"}),")"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"message/rfc822"})}),(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"multipart/mixed"})," (the previous format)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:[(0,i.jsx)(s.code,{children:"application/json"})," / ",(0,i.jsx)(s.code,{children:"application/msgpack"})]}),(0,i.jsx)(s.td,{children:"single-body v2-style reply"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"multipart/form-data"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"multipart/form-data"})})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsxs)(s.td,{children:["only unsupported types (e.g. ",(0,i.jsx)(s.code,{children:"application/xml"}),")"]}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"406 Not Acceptable"})})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"Accept-Encoding: zstd"})," is honoured and defaults to ",(0,i.jsx)(s.code,{children:"identity"}),"; ",(0,i.jsx)(s.code,{children:"Vary: Accept, Accept-Encoding"})," is always advertised."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Operators with custom HTTP clients that POST to ",(0,i.jsx)(s.code,{children:"/checkv3"})," and parse the reply. ",(0,i.jsx)(s.code,{children:"rspamc"})," itself is already updated and needs no action. Clients that only use ",(0,i.jsx)(s.code,{children:"/check"})," or ",(0,i.jsx)(s.code,{children:"/checkv2"})," are not affected."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Migration:"})," Add the appropriate ",(0,i.jsx)(s.code,{children:"Accept"})," header to your existing ",(0,i.jsx)(s.code,{children:"/checkv3"})," request (the request body itself is unchanged; ",(0,i.jsx)(s.code,{children:"request.v3"})," below is a placeholder for your current v3 multipart body):"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"# Restore the previous multipart/mixed reply by requesting it explicitly:\ncurl -H 'Accept: message/rfc822' --data-binary @request.v3 http://localhost:11333/checkv3\n\n# Or ask for a plain single-body JSON result instead:\ncurl -H 'Accept: application/json' --data-binary @request.v3 http://localhost:11333/checkv3\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Do not send an ",(0,i.jsx)(s.code,{children:"Accept"})," header listing only unsupported media types, or the request will be rejected with ",(0,i.jsx)(s.code,{children:"406 Not Acceptable"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["Note: ",(0,i.jsx)(s.code,{children:"/checkv3"})," is now also routed on the controller (port ",(0,i.jsx)(s.code,{children:"11334"}),"), so ",(0,i.jsx)(s.code,{children:"rspamc --protocol-v3"})," against localhost works without targeting a scan worker directly (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/2de2ea9f0",children:"2de2ea9f0"}),")."]}),"\n",(0,i.jsxs)(s.h3,{id:"2-mx_check-loopback-only-mx-reclassified-as-local",children:["2. ",(0,i.jsx)(s.code,{children:"mx_check"}),": Loopback-only MX Reclassified as Local"]}),"\n",(0,i.jsxs)(s.p,{children:["A domain whose MX resolves only to loopback (",(0,i.jsx)(s.code,{children:"127.0.0.0/8"})," or ",(0,i.jsx)(s.code,{children:"::1/128"}),") is now treated as a self-hosted/local MX and emits ",(0,i.jsx)(s.code,{children:"MX_LOCAL_ONLY"})," (score ",(0,i.jsx)(s.code,{children:"3.0"}),") instead of ",(0,i.jsx)(s.code,{children:"MX_BOGON_ONLY"})," (score ",(0,i.jsx)(s.code,{children:"8.0"}),") (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e7a8f3002",children:"e7a8f3002"}),", closes ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6101",children:"#6101"}),"). This stops fully DMARC-aligned self-hosted mail from being hit with the strong ",(0,i.jsx)(s.code,{children:"MX_BOGON_ONLY"})," penalty."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Hosts where the scanning machine's own MX maps to loopback (e.g. the host FQDN pointed at ",(0,i.jsx)(s.code,{children:"127.0.0.1"})," in ",(0,i.jsx)(s.code,{children:"/etc/hosts"}),"), and anyone with composites, score overrides, or ",(0,i.jsx)(s.code,{children:"force_actions"})," keyed on ",(0,i.jsx)(s.code,{children:"MX_BOGON_ONLY"})," for that scenario."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Migration:"})," Repoint any configuration that referenced ",(0,i.jsx)(s.code,{children:"MX_BOGON_ONLY"})," for the loopback-only case to ",(0,i.jsx)(s.code,{children:"MX_LOCAL_ONLY"}),". To adjust the new symbol's weight, override it in ",(0,i.jsx)(s.code,{children:"local.d/groups.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'symbols {\n  "MX_LOCAL_ONLY" {\n    score = 0.0;\n  }\n}\n'})}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-412",children:"Migration to Rspamd 4.1.2"}),"\n",(0,i.jsx)(s.h3,{id:"1-invalid-selectors-now-fail-configuration-load",children:"1. Invalid selectors now fail configuration load"}),"\n",(0,i.jsxs)(s.p,{children:["The selector grammar is now anchored to the end of the input (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5eb40ae63",children:"5eb40ae"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6127",children:"#6127"}),"). Previously the parser silently accepted the longest valid prefix and dropped the rest: ",(0,i.jsx)(s.code,{children:'from("smtp"):domain:lower'})," (a second ",(0,i.jsx)(s.code,{children:":"})," cannot parse) was accepted and evaluated as ",(0,i.jsx)(s.code,{children:'from("smtp"):domain'}),", and any trailing garbage after a selector or a ",(0,i.jsx)(s.code,{children:";"})," list element was ignored. Such selectors now fail configuration load with an error pointing at the offending token."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," any configuration using selectors \u2014 multimap, rbl, ratelimit, settings, reputation, or selectors checked through the WebUI \u2014 that contains a selector with a syntax error after a valid prefix. These selectors were never evaluating as written, so the config was already not doing what it said."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration steps:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Validate the configuration with the new version before restarting the service:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm configtest\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"2",children:["\n",(0,i.jsxs)(s.li,{children:["Fix each rejected selector at the reported position. The most common mistake is using method syntax (",(0,i.jsx)(s.code,{children:":"}),") where transform syntax (",(0,i.jsx)(s.code,{children:"."}),") was intended:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:'from("smtp"):domain:lower    # rejected in 4.1.2\nfrom("smtp"):domain.lower    # correct\n'})}),"\n",(0,i.jsxs)(s.ol,{start:"3",children:["\n",(0,i.jsx)(s.li,{children:"Review the affected rules after fixing: since the full chain now actually evaluates (instead of the silently truncated prefix), the rule's matching behaviour may change."}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"2-glob-map-entries-now-match-the-whole-subject",children:"2. Glob map entries now match the whole subject"}),"\n",(0,i.jsxs)(s.p,{children:["Glob map patterns are compiled anchored (",(0,i.jsx)(s.code,{children:"^(?:...)$"}),") at map load time (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/06cf98f2f",children:"06cf98f"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6125",children:"#6125"}),"). Previously glob entries were matched with substring semantics, so a ",(0,i.jsx)(s.code,{children:"t.co"})," entry matched ",(0,i.jsx)(s.code,{children:"walmart.com"})," and ",(0,i.jsx)(s.code,{children:"*.bit.ly"})," matched ",(0,i.jsx)(s.code,{children:"foo.bit.ly.evil.com"}),". Now bare names match exactly and wildcards match only what they s
1ay."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," all glob-based maps: multimap ",(0,i.jsx)(s.code,{children:"glob"}),"/",(0,i.jsx)(s.code,{children:"glob_multi"})," types, url_redirector ",(0,i.jsx)(s.code,{children:"redirector_hosts_map"}),", dkim_signing and arc ",(0,i.jsx)(s.code,{children:"signing_table"}),"/",(0,i.jsx)(s.code,{children:"key_table"}),", mx_check exclusions and rbl glob ",(0,i.jsx)(s.code,{children:"returncodes"})," matchers. Entries that relied on the accidental substring behaviour stop matching after the upgrade."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Migration steps:"})," review your glob maps and add explicit wildcards where substring matching was intended:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:"# Before 4.1.2 this single entry matched bit.ly anywhere in the subject:\nbit.ly\n\n# In 4.1.2 a bare entry matches exactly; list the subdomain wildcard explicitly:\nbit.ly\n*.bit.ly\n"})}),"\n",(0,i.jsxs)(s.p,{children:["A pattern that must match anywhere inside the subject needs wildcards on both sides (",(0,i.jsx)(s.code,{children:"*fragment*"}),"). Note that the stricter matching is also the point of the fix: ",(0,i.jsx)(s.code,{children:"*.bit.ly"})," no longer matching ",(0,i.jsx)(s.code,{children:"foo.bit.ly.evil.com"})," closes an evasion vector, so do not blindly restore the old reach."]}),"\n",(0,i.jsx)(s.h3,{id:"3-symbols-flagged-explicit_enable-are-strictly-gated-under-settings",children:"3. Symbols flagged explicit_enable are strictly gated under settings"}),"\n",(0,i.jsxs)(s.p,{children:["Symbols carrying the ",(0,i.jsx)(s.code,{children:"explicit_enable"})," flag now actually require explicit enabling (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/011794c9f",children:"011794c"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6144",children:"#6144"}),"). Previously such symbols were executed whenever ",(0,i.jsx)(s.em,{children:"any"})," settings were applied to a task, regardless of content. Now they run only when listed in ",(0,i.jsx)(s.code,{children:"symbols_enabled"})," of the applied settings (or force-enabled via ",(0,i.jsx)(s.code,{children:"task:enable_symbol()"}),"), including settings delivered via the ",(0,i.jsx)(s.code,{children:"Settings"})," header."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," deployments that apply settings (settings ids, the ",(0,i.jsx)(s.code,{children:"Settings"}),"/",(0,i.jsx)(s.code,{children:"Settings-ID"})," headers, or map-driven settings) and expect symbols registered with the ",(0,i.jsx)(s.code,{children:"explicit_enable"})," flag to keep running under those settings."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration steps:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["Find which symbols carry the flag \u2014 configdump now shows a per-symbol ",(0,i.jsx)(s.code,{children:"flags"})," array (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/05a7c92c9",children:"05a7c92"}),"):"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm configdump --symbol-details | grep -B 4 explicit_enable\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"2",children:["\n",(0,i.jsxs)(s.li,{children:["Add the required symbols to ",(0,i.jsx)(s.code,{children:"symbols_enabled"})," of the relevant settings entries. With the default policy this switches the entry to whitelist mode (only listed symbols run); to keep all symbols enabled while merely unlocking the listed ones, use the new ",(0,i.jsx)(s.code,{children:"policy"})," option:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'# local.d/settings.conf\nmy_entry {\n  # ... match conditions ...\n  apply {\n    policy = "implicit_allow";\n    symbols_enabled = ["MY_EXPLICIT_SYM"]; # additive: unlocks, does not whitelist\n    symbols_disabled = ["UNWANTED_SYM"];   # subtractive\n  }\n}\n'})}),"\n",(0,i.jsx)(s.h3,{id:"4-alias-resolution-no-longer-rewrites-from-domains",children:"4. Alias resolution no longer rewrites From domains"}),"\n",(0,i.jsxs)(s.p,{children:["The aliases module now refuses any rewrite that would change the domain of the From address, since SPF, DKIM and DMARC all key on it (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/9fe6cb2aa",children:"9fe6cb2"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6137",children:"#6137"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6141",children:"#6141"}),"). In particular, ",(0,i.jsx)(s.code,{children:"googlemail.com"})," senders are no longer rewritten to ",(0,i.jsx)(s.code,{children:"gmail.com"})," \u2014 only the gmail-style user-part canonicalization (dots and plus tags) is kept (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/1b0cdd779",children:"1b0cdd7"}),"). DMARC, SPF and forged-recipients checks now evaluate the sender addresses as they were transmitted."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Who is affected:"})}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Maps, rules or statistics keyed on the rewritten ",(0,i.jsx)(s.code,{children:"gmail.com"})," From domain will now see ",(0,i.jsx)(s.code,{children:"googlemail.com"}),". Add ",(0,i.jsx)(s.code,{children:"googlemail.com"})," alongside ",(0,i.jsx)(s.code,{children:"gmail.com"})," in from-domain maps. Email ",(0,i.jsx)(s.em,{children:"hash"})," lookups (RBL email checks) still dedup via ",(0,i.jsx)(s.code,{children:"lua_util.remove_email_aliases"}),", so they are unaffected."]}),"\n",(0,i.jsx)(s.li,{children:"Configurations with cross-domain virtual aliases applied to the From address: those rewrites are silently discarded now (recipient rewriting is unchanged)."}),"\n",(0,i.jsxs)(s.li,{children:["Lua rules using the ",(0,i.jsx)(s.code,{children:"orig"})," address flavour: ",(0,i.jsx)(s.code,{children:"task:get_from({'mime', 'orig'})"})," now returns ",(0,i.jsx)(s.em,{children:"only"})," the wire addresses when a rewrite preserved originals, instead of the originals plus the rewritten entries (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a7cbff984",children:"a7cbff9"}),"). Code that relied on the combined list must query both flavours separately."]}),"\n",(0,i.jsx)(s.li,{children:"DMARC/SPF verdicts change for mail whose sender was rewritten by aliases \u2014 this is the fix itself (alignment is checked against the wire From), and no action is needed."}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"5-lua_cryptobox-secretbox-ciphertexts-produced-with-short-nonces",children:"5. lua_cryptobox secretbox: ciphertexts produced with short nonces"}),"\n",(0,i.jsxs)(s.p,{children:["Secretbox encrypt and decrypt used to pass the caller's short nonce pointer to libsodium instead of the zero-padded 24-byte buffer, reading up to 23 bytes out of bounds and producing ciphertexts that depended on adjacent memory contents (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/efb4d5cd5",children:"efb4d5c"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6121",children:"#6121"}),"). Short nonces are now correctly zero-padded, so a short nonce and its explicitly padded 24-byte form produce identical, stable ciphertexts."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," only custom Lua code using ",(0,i.jsx)(s.code,{children:"rspamd_cryptobox"})," secretbox with nonces shorter than 24 bytes. Ciphertexts persisted by earlier versions under short nonces were never reliable and will most likely fail to decrypt after the upgrade."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Migration steps:"})," after upgrading, re-encrypt any persisted secretbox data, and prefer generating full 24-byte nonces going forward."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-413",children:"Migration to Rspamd 4.1.3"}),"\n",(0,i.jsx)(s.p,{children:"Rspamd 4.1.3 tightens several resource limits that were previously unbounded, corrects a number of limits that were configured but never actually enforced, and changes how fuzzy matches are scored and reported. Most deployments are affected by at least one of the items below."}),"\n",(0,i.jsx)(s.h3,{id:"1-fuzzy-non-exact-match-scoring-curve-changed",children:"1. Fuzzy Non-Exact Match Scoring Curve Changed"}),"\n",(0,i.jsxs)(s.p,{children:["The score multiplier for non-exact (shingle) fuzzy matches was ",(0,i.jsx)(s.code,{children:"sqrt(prob)"}),", a curve anchored at zero (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/c45ab97a4",children:"c45ab97"}),"). Since the storage never returns a probability below the shingle match threshold of ",(0,i.jsx)(s.code,{children:"0.5"}),", the entire reachable range collapsed into roughly ",(0,i.jsx)(s.code,{children:"0.73 .. 1.0"}),": a marginal 17/32 shingle overlap scored almost as high as an exact match, which combined with high-weight deny lists turned weak matches into instant rejects."]}),"\n",(0,i.jsxs)(s.p,{children:["The multiplier is now ",(0,i.jsx)(s.code,{children:"((prob - prob_bias) / (1 - prob_bias)) ^ prob_power"}),", anchored at the match threshold:"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Shingle overlap"}),(0,i.jsx)(s.th,{children:"Old multiplier"}),(0,i.jsx)(s.th,{children:"New multiplier"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"19/32"}),(0,i.jsx)(s.td,{children:"~0.77"}),(0,i.jsx)(s.td,{children:"0.19"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"24/32"}),(0,i.jsx)(s.td,{children:"~0.87"}),(0,i.jsx)(s.td,{children:"0.50"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:"28/32"}),(0,i.jsx)(s.td,{children:"~0.94"}),(0,i.jsx)(s.td,{children:"0.75"})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Every deployment using fuzzy rules that match on text or HTML hashes. Small image hashes keep their existing normalized curve, and exact matches are unchanged."]}),"\n",(0,i.jsx)(s.p,{children:"Weak matches now contribute much less score, so messages previously rejected on a marginal fuzzy hit may now pass. Review the weights of your fuzzy symbols before assuming the rule stopped working. The old curve is not preserved and cannot be restored; it was simply broken."}),"\n",(0,i.jsxs)(s.p,{children:["Two per-rule knobs are available in ",(0,i.jsx)(s.code,{children:"local.d/fuzzy_check.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'rule "local" {\n  # anch
1or of the curve, defaults to the 0.5 match threshold\n  prob_bias = 0.5;\n  # 1.0 by default; raise it to discount weak matches more harshly\n  prob_power = 2.0;\n}\n'})}),"\n",(0,i.jsx)(s.h3,{id:"2-spf-dns-limits-are-now-enforced-and-yield-permerror",children:"2. SPF DNS Limits Are Now Enforced and Yield permerror"}),"\n",(0,i.jsx)(s.p,{children:"Three SPF limit bugs are fixed at once, and all three change verification results:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"max_dns_nesting"})," was checked but the nesting counter was never incremented, so the limit had no effect at all and include/redirect chains were bounded only by the DNS request counter. Nesting depth is now tracked per resolved element and enforced when an include or redirect is about to be followed (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/b80fd076d",children:"b80fd076"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:["The DNS request counter was compared with ",(0,i.jsx)(s.code,{children:">"})," while being incremented after the check, so a limit of 30 evaluated 31 elements. The comparison is now ",(0,i.jsx)(s.code,{children:">="}),", so exactly ",(0,i.jsx)(s.code,{children:"max_dns_requests"})," elements are evaluated: one fewer than before."]}),"\n",(0,i.jsxs)(s.li,{children:["Exceeding ",(0,i.jsx)(s.code,{children:"max_dns_requests"})," or ",(0,i.jsx)(s.code,{children:"max_dns_nesting"})," used to leave only the offending element unparsed, and the record was still evaluated with whatever fitted in the budget, usually ending as a definitive fail via its trailing ",(0,i.jsx)(s.code,{children:"-all"}),". RFC 7208 4.6.4 requires ",(0,i.jsx)(s.code,{children:"permerror"})," instead, and that is what Rspamd now returns (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/66cdc8bfa",children:"66cdc8bf"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:["Names returned by ",(0,i.jsx)(s.code,{children:"mx"})," and ",(0,i.jsx)(s.code,{children:"ptr"})," were expanded into A/AAAA requests against a counter that never changed, and SPF uses the forced resolver API that bypasses ",(0,i.jsx)(s.code,{children:"dns_max_requests"}),", so a single large RRset produced an unbounded number of queries. Both the RFC limit of 10 address lookups per ",(0,i.jsx)(s.code,{children:"mx"}),"/",(0,i.jsx)(s.code,{children:"ptr"})," element and a new per-record expansion budget are now enforced (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ea20c2f59",children:"ea20c2f5"}),")."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone acting on ",(0,i.jsx)(s.code,{children:"R_SPF_FAIL"})," (rejection policies, DMARC alignment, ",(0,i.jsx)(s.code,{children:"settings"})," conditioned on SPF symbols). Senders with large or deeply nested SPF records will flip from ",(0,i.jsx)(s.code,{children:"R_SPF_FAIL"})," to ",(0,i.jsx)(s.code,{children:"R_SPF_PERMFAIL"}),", which usually carries a very different weight in your policy."]}),"\n",(0,i.jsx)(s.p,{children:"Records carrying any flag are not cached in the LRU, so a domain that trips a limit is re-resolved on every message. Watch your resolver load after the upgrade for domains that now permerror."}),"\n",(0,i.jsxs)(s.p,{children:["Review the limits and the new ",(0,i.jsx)(s.code,{children:"max_dns_expansions"})," option in ",(0,i.jsx)(s.code,{children:"local.d/spf.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# maximum SPF elements requiring a DNS lookup, per RFC 7208\nmax_dns_requests = 30;\n# include/redirect nesting depth, enforced for the first time in 4.1.3\nmax_dns_nesting = 10;\n# new in 4.1.3: per-record budget for A/AAAA lookups spawned by mx/ptr\nmax_dns_expansions = 100;\n"})}),"\n",(0,i.jsx)(s.p,{children:"Setting a limit to zero disables it, but flattening a record still recurses over the same chain and remains bounded by the hard nesting cap compiled into Rspamd."}),"\n",(0,i.jsx)(s.p,{children:"Check for newly appearing permerrors before and after the upgrade:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamc stat | grep -i spf\ngrep -c R_SPF_PERMFAIL /var/log/rspamd/rspamd.log\n"})}),"\n",(0,i.jsx)(s.h3,{id:"3-dkim-signature-limit-and-key-bounds",children:"3. DKIM Signature Limit and Key Bounds"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"max_sigs"})," now counts every signature header examined"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d560f9e8e",children:"d560f9e8"}),"). Previously the check sat at the bottom of the loop, incremented before comparing with ",(0,i.jsx)(s.code,{children:">"}),", and three ",(0,i.jsx)(s.code,{children:"continue"})," paths (parse failure, ",(0,i.jsx)(s.code,{children:"trusted_only"})," skip, failed key request) jumped over the increment entirely. Consequences:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["The default of ",(0,i.jsx)(s.code,{children:"5"})," used to permit six signatures;
1 it now permits exactly five."]}),"\n",(0,i.jsxs)(s.li,{children:["Malformed or skipped signatures now count against the limit. Five junk ",(0,i.jsx)(s.code,{children:"DKIM-Signature"})," headers ahead of a valid one will push the valid one out, and ",(0,i.jsx)(s.code,{children:"R_DKIM_ALLOW"})," will not be inserted."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Deployments that see messages with many DKIM signatures, in particular mailing lists and forwarders that re-sign, and anyone whose policy depends on ",(0,i.jsx)(s.code,{children:"R_DKIM_ALLOW"})," for such traffic."]}),"\n",(0,i.jsxs)(s.p,{children:['If the "stopped after N signatures" line starts appearing in your logs for legitimate mail, raise the limit in ',(0,i.jsx)(s.code,{children:"local.d/dkim_check.conf"})," (and ",(0,i.jsx)(s.code,{children:"local.d/arc.conf"})," if you tune ARC separately):"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"max_sigs = 8;\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Oversized public keys are now rejected"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/871dc8a12",children:"871dc8a1"}),"). The ",(0,i.jsx)(s.code,{children:"p="})," tag is capped at 4096 base64 characters before any allocation, and parsed keys wider than 8192 bits are refused. Real keys are far below both bounds: 44 characters for ed25519, 736 for a 4096 bit RSA key. Signers using a modulus above 8192 bits now produce ",(0,i.jsx)(s.code,{children:"R_DKIM_PERMFAIL"})," instead of a verification result. RFC 8301 requires verifiers to handle 1024 to 4096 bits, so this leaves twice the required headroom. Only verification is affected; signing keys are loaded through a different path."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:["The ",(0,i.jsx)(s.code,{children:"h="})," header list is capped at 1000 items"]})," for both signing and verification (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/318d31837",children:"318d3183"}),"). The shipped ",(0,i.jsx)(s.code,{children:"default_sign_headers"})," list has 27 entries, so no realistic signing policy is affected."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Ed25519 verification now works on more builds"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/89b9d5a08",children:"89b9d5a0"}),"). All Ed25519 DKIM crypto is libsodium, which is a mandatory dependency, but it was gated behind a probe for OpenSSL's ",(0,i.jsx)(s.code,{children:"EVP_PKEY_ED25519"}),". On an OpenSSL build lacking that NID, ed25519 signatures were previously not verified at all and will now produce ",(0,i.jsx)(s.code,{children:"R_DKIM_ALLOW"})," or ",(0,i.jsx)(s.code,{children:"R_DKIM_REJECT"}),". If you build Rspamd yourself, expect new DKIM (and therefore DMARC) results on ed25519-signed mail."]}),"\n",(0,i.jsx)(s.h3,{id:"4-url_redirector-blocks-local-destinations-by-default",children:"4. url_redirector Blocks Local Destinations by Default"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"rspamd_http.request()"})," connected to whatever a URL or its DNS resolution yielded, including loopback, link-local and RFC1918 addresses. A new ",(0,i.jsx)(s.code,{children:"forbid_local"})," option is enforced at the connection chokepoint, and it is enabled by default in ",(0,i.jsx)(s.code,{children:"url_redirector"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e57fb6472",children:"e57fb647"}),"). Message URLs and their redirect targets are attacker controlled, and hop limits alone do not stop a crafted redirect from probing cloud metadata endpoints or internal services."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Operators who resolve redirects through an internal service, or whose ",(0,i.jsx)(s.code,{children:"local_addrs"})," radix covers the hosts that ",(0,i.jsx)(s.code,{children:"url_redirector"})," needs to reach. Those lookups will now fail."]}),"\n",(0,i.jsxs)(s.p,{children:["To restore the previous behaviour, add to ",(0,i.jsx)(s.code,{children:"local.d/url_redirector.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"forbid_local = false;\n"})}),"\n",(0,i.jsxs)(s.p,{children:["The same option is available to any Lua plugin calling ",(0,i.jsx)(s.code,{children:"rspamd_http.request()"})," and is off by default there. It uses the address class check plus your configurable ",(0,i.jsx)(s.code,{children:"local_addrs"}),", so review ",(0,i.jsx)(s.code,{children:"local_addrs"})," in ",(0,i.jsx)(s.code,{children:"local.d/options.inc"})," if the result is not what you expect."]}),"\n",(0,i.jsx)(s.h3,{id:"5-new-size-ceilings-on-http-maps-and-decompression",children:"5. New Size Ceilings on HTTP, Maps and Decompression"}),"\n",(0,i.jsx)(s.p,{children:"Several paths that were entirely unbounded now enforce a limit. Eac
1h of these can reject input that Rspamd previously accepted."}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:["Controller, proxy and control sockets now cap request bodies at ",(0,i.jsx)(s.code,{children:"max_message"})]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/1ae11c4db",children:"1ae11c4d"}),"). Only the normal worker did so before, while the controller router, the proxy client connection and the main control socket accepted a declared ",(0,i.jsx)(s.code,{children:"Content-Length"})," of any size, causing an unbounded preallocation before routing or authentication. Oversized requests are now rejected with ",(0,i.jsx)(s.code,{children:"413"})," by the controller and by closing the connection in the proxy."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone scanning messages larger than ",(0,i.jsx)(s.code,{children:"max_message"})," (50Mb by default) through ",(0,i.jsx)(s.code,{children:"rspamd_proxy"})," or the controller ",(0,i.jsx)(s.code,{children:"/checkv2"})," endpoint. These used to be accepted; they now fail."]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/options.inc\nmax_message = 100Mb;\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:["Remote HTTP maps are bounded by ",(0,i.jsx)(s.code,{children:"max_map_size"}),", 256Mb by default"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/95d51df4a",children:"95d51df4"}),"). Map client connections never set a body limit, so a map server could feed an arbitrarily large body that is fully retained in shared memory, and the zstd path allocated its output buffer straight from the frame-advertised decompressed size and then doubled it without limit. Frames advertising more than the ceiling are now rejected upfront. The same limit bounds the map shm-cache and static map paths."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Sites serving very large maps (large hash or domain lists). Set a global ceiling, or override it per map:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/options.inc\nmax_map_size = 512Mb;\n"})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'maps {\n  "https://example.com/huge.map" {\n    max_size = 1Gb;\n    timeout = 30s;\n  }\n}\n'})}),"\n",(0,i.jsxs)(s.p,{children:["Setting ",(0,i.jsx)(s.code,{children:"max_map_size = 0"})," disables the limit."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:["Lua HTTP responses are bounded by ",(0,i.jsx)(s.code,{children:"max_lua_http_response"}),", 256Mb by default"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/9fe7ae5f5",children:"9fe7ae5f"}),"). It applies whenever a request does not pass an explicit ",(0,i.jsx)(s.code,{children:"max_size"}),"; an explicit ",(0,i.jsx)(s.code,{children:"max_size = 0"})," still disables the limit for that request. A negative ",(0,i.jsx)(s.code,{children:"max_size"})," used to be converted straight to an unsigned value, silently producing an effectively unlimited cap, and is now rejected with a logged error. Third-party plugins passing a negative value will start logging errors and must be fixed."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"zstd decompression is bounded everywhere"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/aaa53f54f",children:"aaa53f54"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/bd1c9a277",children:"bd1c9a27"}),"). The streaming loop was copy-pasted in seven places with inconsistent bounding: the proxy had no output ceiling at all, and the task and protocol v3 paths could overshoot ",(0,i.jsx)(s.code,{children:"max_message"})," up to twofold. All sites now share one helper bounded by ",(0,i.jsx)(s.code,{children:"max_message"})," (task, protocol v3, proxy) or ",(0,i.jsx)(s.code,{children:"max_map_size"})," (map cache and static paths). Compressed traffic that previously decompressed to more than ",(0,i.jsx)(s.code,{children:"max_message"})," is now refused."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Shared memory protocol semantics changed"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/898c21aa1",children:"898c21aa"}),"). ",(0,i.jsx)(s.code,{children:"Shm-Offset"})," and ",(0,i.jsx)(s.code,{children:"Shm-Length"})," were each validated on their own but never together, so an offset near the end combined with a full length produced a body running past the mapping. Both protocol versions now share one validated helper, and ",(0,i.jsx)(s.code,{children:"Shm-Offset"})," without ",(0,i.jsx)(s.code,{children:"Shm-Length"}),' means "up to the end of the segment" rather than "the whole segment". Segment names must now refer to a regular, non-empty object, and payloads are capped at ',(0,i.jsx)(s.code,{children:"max_message"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Third-party clients or milters that talk the Rspamd HTTP protocol using shared memory bodies. Clients sending an offset without a length, or a segment that is not a regular object, need to be updated."]}),"\n",(0,i.jsx)(s.h3,{id:"6-mime-and-url-parser-limits-are-now-actually-enforced",children:"6. MIME and URL Parser Limits Are Now Actually Enforced"}),"\n",(0,i.jsx)(s.p,{children:"A series of parser hardening changes bounds resources that a single message could previously amplify. These affect scan results, not just memory:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"max_urls"})," is enforced at every insertion point"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/898b5e56c",children:"898b5e56"}),"). Previously only plain-text callbacks respected it; HTML links, query URLs, images, displayed URLs, subject URLs and Lua-injected URLs were unbounded. The text pre-check also moved from ",(0,i.jsx)(s.code,{children:">"})," to ",(0,i.jsx)(s.code,{children:">="}),", so the cap is now exact. Messages with very many URLs will have fewer of them available to RBL, reputation and multimap rules than before. Raise ",(0,i.jsx)(s.code,{children:"max_urls"})," in ",(0,i.jsx)(s.code,{children:"local.d/options.inc"})," if you rely on exhaustive extraction."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Content-Type and Content-Disposition parameters are capped at 1024 per header"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/4ecae17a7",children:"4ecae17a"}),"). Content-Type flags the truncation as broken; Content-Disposition simply stops. The RFC 2231 continuation ordering fix in the same commit can change how a split parameter value is reconstructed."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Per-part newline metadata is capped at 100k positions"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/1ad069fa8",children:"1ad069fa"}),"), exposed as ",(0,i.jsx)(s.code,{children:"newlines_truncated"})," in ",(0,i.jsx)(s.code,{children:"textpart:get_stats()"}),". Text normalization and line counters are unaffected."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"HTML attributes are bounded per tag and per task"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/b6111378f",children:"b6111378"}),"), synthetic tags are capped (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/68eb633b5",children:"68eb633b"}),"), and DOM traversal no longer recurses on the native stack (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/86e1b7787",children:"86e1b7787"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Alternative-part linking now shares a single visit budget per task"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/285d201df",children:"285d201d"}),"). When it is exhausted, the remaining parts are left unlinked and a warning is logged. Fasttext language detection now skips overlong words and caps the total tokens fed to the model, which can change the detected language on pathological bodies."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Archive metadata, 7zip folder counts and MIME header parsing are bounded"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/adcb86001",children:"adcb8600"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/bc409ba39",children:"bc409ba3"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/c653253b3",children:"c653253b"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/6d5f4fcbf",children:"6d5f4fcb"}),"). Deeply pathological archives that used to be fully parsed are now reported as truncated or broken."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Nested comment depth in the Content-Disposition and SMTP date grammars is capped at 8"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e36060c3f",children:"e36060c3"}),"). Headers nesting comments deeper than that now fail to parse instead of consuming memory proportional to their own length."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Everyone, but visibly only for messages that were previously exercising these unbounded paths. If you have custom rules that count URLs, headers or archive entries, re-validate them against your corpus."]}),"\n",(0,i.jsxs)(s.p,{children:["Two parser fixes in the same release change extraction results for ordinary mail: PDF ",(0,i.jsx)(s.code,{children:"Td"}),"/",(0,i.jsx)(s.code,{children:"TD"})," operators now emit newlines, so consecutive text lines are no longer concatenated into false URLs such as ",(0,i.jsx)(s.code,{children:"http://2026.you"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ddcfc4b08",children:"ddcfc4b0"}),"), and HTML image style dimensions are parsed correctly instead of being read from an uninitialized variable (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/800aaf45f",children:"800aaf45"}),"). Rules keyed on image dimensions may fire differently."]}),"\n",(0,i.jsx)(s.h3,{id:"7-fuzzy-match-reporting-format-changed",children:"7. Fuzzy Match Reporting Format Changed"}),"\n",(0,i.jsxs)(s.p,{children:["Fuzzy results are now structured throughout (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a4bc4de00",children:"a4bc4de0"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f00d108ad",children:"f00d108a"}),", PR ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6150",children:"#6150"}),"), and two externally visible formats changed with it:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Symbol options for non-exact matches"})," now carry both hash prefixes: ",(0,i.jsx)(s.code,{children:"flag:found:prob:type:queried"}),". Previously only one hash was present."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsxs)(s.strong,{children:["The ",(0,i.jsx)(s.code,{children:"X-Rspamd-Fuzzy"})," header"]})," produced by ",(0,i.jsx)(s.code,{children:"milter_headers"})," now annotates every matched hash with the rule, flag, probability, the queried hash for non-exact matches and the storage timestamp. It falls back to the legacy ",(0,i.jsx)(s.code,{children:"fuzzy_hashes"})," pool variable only when no structured results exist."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Log scrapers, history exporters, downstream MTA rules and any Lua code matching on fuzzy symbol options or parsing ",(0,i.jsx)(s.code,{children:"X-Rspamd-Fuzzy"}),". Update the parsers before upgrading, or pin the consumers until they are updated."]}),"\n",(0,i.jsx)(s.p,{children:"New interfaces are available instead of string parsing:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"task:get_fuzzy_results()"})," returns the structured records (rule, symbol, upstream, stored and queried digests, type, probability, score, flag, value, hash timestamp) from the ",(0,i.jsx)(s.code,{children:"fuzzy_matches"})," mempool variable; all queried hashes including misses are in ",(0,i.jsx)(s.code,{children:"fuzzy_checked"}),"."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"fuzzy_check.check(task, cb, rule, timeout[, hashes][, server])"})," queries a storage directly."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"rspamadm fuzzy_hash"})," computes the hashes of a message per configured rule, checks them against the storage with ",(0,i.jsx)(s.code,{children:"-C"})," and queries explicit hex digests with ",(0,i.jsx)(s.code,{children:"-H"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/90b27228e",children:"90b27228"}),")."]}),"\n"]}
1),"\n",(0,i.jsxs)(s.p,{children:["A storage running 4.1.3 sets a flag bit in the first reserved byte of the reply when the digest field holds the digest actually resolved by the backend; the client exposes this as the ",(0,i.jsx)(s.code,{children:"confirmed"})," field. Against a legacy storage the field is absent rather than guessed."]}),"\n",(0,i.jsx)(s.h3,{id:"8-clickhouse-schema-version-11",children:"8. ClickHouse Schema Version 11"}),"\n",(0,i.jsxs)(s.p,{children:["The ClickHouse module adds ",(0,i.jsx)(s.code,{children:"Fuzzy.Hash"}),", ",(0,i.jsx)(s.code,{children:"Fuzzy.Queried"}),", ",(0,i.jsx)(s.code,{children:"Fuzzy.Rule"}),", ",(0,i.jsx)(s.code,{children:"Fuzzy.Prob"})," and ",(0,i.jsx)(s.code,{children:"Fuzzy.Flag"})," nested columns, filled from the structured fuzzy results (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/cae30bbf6",children:"cae30bbf"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Every deployment with the ",(0,i.jsx)(s.code,{children:"clickhouse"})," module enabled."]}),"\n",(0,i.jsxs)(s.p,{children:["The module applies the schema upgrade itself on the first connection after the upgrade, so the ClickHouse user configured in ",(0,i.jsx)(s.code,{children:"local.d/clickhouse.conf"})," must be allowed to run ",(0,i.jsx)(s.code,{children:"ALTER TABLE"}),". Verify after starting:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:'clickhouse-client --query "SELECT * FROM rspamd_version"\nclickhouse-client --query "DESCRIBE TABLE rspamd" | grep -i fuzzy\n'})}),"\n",(0,i.jsx)(s.p,{children:"If you feed the same tables from several Rspamd nodes, upgrade them together or expect the pre-4.1.3 nodes to keep writing rows with the new columns empty. Roll the schema change out during a maintenance window if your table is large."}),"\n",(0,i.jsx)(s.h3,{id:"9-fuzzy-storage-admission-control-rate-limits-and-statistics",children:"9. Fuzzy Storage: Admission Control, Rate Limits and Statistics"}),"\n",(0,i.jsxs)(s.p,{children:["Fuzzy storage now performs admission checks before parsing a datagram (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d2cf061b1",children:"d2cf061b"}),"). Previously every datagram was parsed, and decrypted when encrypted, before any check ran: the blocklist and the rate limit were only consulted well after a session allocation, an ECDH, a MAC verification and the Lua pre-handlers. Four behaviour changes follow:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Blocklisted peers get no reply on UDP."})," The blocklist is checked in the read loop, before a session is allocated, and building a reply would require exactly the parse being avoided. ",(0,i.jsx)(s.code,{children:"accept_tcp_socket"})," already dropped blocked peers without replying, so both transports now behave alike. Anything that expected a ",(0,i.jsx)(s.code,{children:"403"})," from a blocked source will now see a timeout."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"PING"})," and ",(0,i.jsx)(s.code,{children:"STAT"})," are rate limited."]})," Both are answered unauthenticated and were previously unmetered. A rate-limited ",(0,i.jsx)(s.code,{children:"PING"})," or ",(0,i.jsx)(s.code,{children:"STAT"})," is dropped rather than answered with ",(0,i.jsx)(s.code,{children:"403"}),", since the reply is the entire cost of those commands. This stays inert unless ",(0,i.jsx)(s.code,{children:"ratelimit_rate"})," and ",(0,i.jsx)(s.code,{children:"ratelimit_burst"})," are set, and the existing whitelist and local address exemptions still apply."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Key lookup and MAC verification failures are logged at debug level"}),", not error. Both are reachable by anyone who can send a datagram, before any rate limit applies, so an error line per packet turned a spoofed source flood into a log volume attack. Alerting keyed on those log lines will stop firing."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsxs)(s.strong,{children:["The rate limit bucket is no longer allocated when ",(0,i.jsx)(s.code,{children:"rate"})," or ",(0,i.jsx)(s.code,{children:"burst"})," are unset"]}),", so unconfigured storages stop accumulating an LRU entry per masked 
1source."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Operators of fuzzy storage with ",(0,i.jsx)(s.code,{children:"ratelimit_rate"})," and ",(0,i.jsx)(s.code,{children:"ratelimit_burst"})," configured, and anyone monitoring storage health with ",(0,i.jsx)(s.code,{children:"PING"})," or ",(0,i.jsx)(s.code,{children:"STAT"})," probes."]}),"\n",(0,i.jsx)(s.p,{children:"Monitoring probes now share the same per-source budget as scan traffic. Exempt them explicitly in the fuzzy worker configuration:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'worker "fuzzy" {\n  ratelimit_whitelist = "/etc/rspamd/fuzzy_ratelimit_whitelist.map";\n}\n'})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"fuzzystat"})," output gained new fields."]})," ",(0,i.jsx)(s.code,{children:"blocked_requests"}),", ",(0,i.jsx)(s.code,{children:"decrypt_errors"})," and ",(0,i.jsx)(s.code,{children:"ratelimited_requests"})," are reported so that the now-silent drops stay observable, and clients without a key (for example those permitted by ",(0,i.jsx)(s.code,{children:"allow_update"}),") are tracked under a dedicated ",(0,i.jsx)(s.code,{children:"unkeyed"})," pseudo-key with the same aggregate and per-IP counters as a real key (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/56157dc6b",children:"56157dc6"}),"). Monitoring code that iterates the key list must tolerate ",(0,i.jsx)(s.code,{children:"unkeyed"}),"; anything keying on a fixed set of keys will need updating."]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm control fuzzystat\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Note that ",(0,i.jsx)(s.code,{children:"errors_ips"})," remains telemetry only. It is incremented when parsing failed, which takes no key and no handshake, so on UDP the recorded address is forgeable and must never be used as a ban source."]}),"\n",(0,i.jsx)(s.h3,{id:"10-fuzzy-redis-storage-persists-shingle-sets",children:"10. Fuzzy Redis Storage Persists Shingle Sets"}),"\n",(0,i.jsxs)(s.p,{children:["The Redis backend now stores the shingle key suffixes as the ",(0,i.jsx)(s.code,{children:"S"})," field of the digest hash on every ",(0,i.jsx)(s.code,{children:"ADD"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/cb8de0c34",children:"cb8de0c3"}),", PR ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6151",children:"#6151"}),"). This fixes two long-standing blind spots: ",(0,i.jsx)(s.code,{children:"DEL"})," on a digest-only deletion (a periodic deletion by hash list, for instance) can now reconstruct and remove the orphaned shingle slots, and ",(0,i.jsx)(s.code,{children:"REFRESH"})," refreshes the slots the digest still owns instead of the slots of whichever message happened to trigger it."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Operators of Redis-backed fuzzy storage. The sqlite backend needs no changes, since its shingles table cascades on delete."]}),"\n",(0,i.jsx)(s.p,{children:"Two operational consequences:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Redis memory grows."})," Each digest now carries its shingle set. Check your ",(0,i.jsx)(s.code,{children:"maxmemory"})," headroom before upgrading a large storage."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Only hashes added or refreshed by 4.1.3 carry the field."})," Existing digests remain without it until they are re-added, so the ",(0,i.jsx)(s.code,{children:"DEL"})," and ",(0,i.jsx)(s.code,{children:"REFRESH"})," fixes apply gradually. In a mixed-version cluster writing to the same Redis, hashes added by older nodes still produce orphaned slots; upgrade all writers to get the full effect."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["Per-hash introspection is available once the storage is upgraded (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f94ecd1de",children:"f94ecd1d"}),"). It reports flag slots and values, creation time, remaining TTL and shingle slot ownership, which answers directly whether a hash can still produce fuzzy matches or whether its shingle anchor has decayed:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm control fuzzyhash <128 hex digest>\n"})}),"\n",(0,i.jsx)(s.h3,{id:"11-lua-api-changes",children:"11. Lua API Changes"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"rspamd_http.request()"})," error delivery in coroutine mode"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/0eddd23dd",children:"0eddd23d"}),"). Synchronous failures (unparseable URL, blocked session, immediate connection or DNS-send failure) returned a bare ",(0,i.jsx)(s.code,{children:"false"}),", which coroutine callers read as ",(0,i.jsx)(s.code,{children:"err = false, response = nil"}),", crashing on ",(0,i.jsx)(s.code,{children:"response.code"})," in every caller written as ",(0,i.jsx)(s.code,{children:"if not err then ... end"}),". They now return ",(0,i.jsx)(s.code,{children:"(err, nil)"})," like the connection error path. DNS-stage failures (resolve error, no records, connection refused) went through the callback-only path and were silently swallowed for c
1oroutine requests, leaving the yielded thread suspended forever; the thread is now resumed with ",(0,i.jsx)(s.code,{children:"(err, nil)"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Third-party plugins using the coroutine form of ",(0,i.jsx)(s.code,{children:"rspamd_http.request()"}),". Code that special-cased the old bare ",(0,i.jsx)(s.code,{children:"false"}),", or that relied on a request never returning, must be updated. Code written the documented way (",(0,i.jsx)(s.code,{children:"local err, response = rspamd_http.request(...)"}),") now behaves correctly for the first time on these paths."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Plain text parts are linked to their HTML alternative"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/212e419a9",children:"212e419a"}),"). The alternative-part search compared the raw ",(0,i.jsx)(s.code,{children:"IS_TEXT_PART_HTML"})," flag value (0 or 4) against a boolean argument (0 or 1), so only the html-to-plain direction of ",(0,i.jsx)(s.code,{children:"alt_text_part"})," was ever populated. Both directions now work. Lua rules that skip a text part when it has an alternative will now also skip plain parts that have an HTML sibling, which previously always fell through to full processing. Re-check any custom rule that tests for an alternative part."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"url_suspect"})," dropped the ",(0,i.jsx)(s.code,{children:"length_thresholds.suspicious"})," setting"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/b1d5cbec6",children:"b1d5cbec"}),"). Both arms of the branch inserted the same ",(0,i.jsx)(s.code,{children:"URL_USER_PASSWORD"})," finding, so the threshold never affected anything; it is removed together with the dead branch. ",(0,i.jsx)(s.code,{children:"mailto"})," URLs are now explicitly excluded from the user-field check, since every email address carries a local part. Remove the setting from your configuration if you set it."]}),"\n",(0,i.jsx)(s.h3,{id:"12-webui-rewritten-without-jquery-or-font-awesome",children:"12. WebUI Rewritten Without jQuery or Font Awesome"}),"\n",(0,i.jsxs)(s.p,{children:["jQuery is no longer loaded by the WebUI and the vendored ",(0,i.jsx)(s.code,{children:"jquery-3.7.1.min.js"})," is deleted (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/9ab0ed6a8",children:"9ab0ed6a"}),", PR ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6124",children:"#6124"}),"). All modules were migrated to native DOM, ",(0,i.jsx)(s.code,{children:"XMLHttpRequest"}),' and the Bootstrap 5 vanilla constructor API. The Font Awesome "SVG with JS" framework is gone as well, replaced by a local subset sprite (',(0,i.jsx)(s.code,{children:"img/icons.svg"}),", 35 glyphs) rendered by ",(0,i.jsx)(s.code,{children:"app/icons.js"}),", removing about 900 KB of JavaScript (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/123b72eaf",children:"123b72ea"}),", PR ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6145",children:"#6145"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone shipping custom WebUI modules, local patches, or a theme that relies on ",(0,i.jsx)(s.code,{children:"$"}),", on ",(0,i.jsx)(s.code,{children:"$.ajax"}),", on the Font Awesome CSS classes, or on the ",(0,i.jsx)(s.code,{children:"[data-fa-i2svg]"})," selectors."]}),"\n",(0,i.jsx)(s.p,{children:"Concretely:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["Custom modules must be ported off jQuery. The shared helpers ",(0,i.jsx)(s.code,{children:"common.el"}),", ",(0,i.jsx)(s.code,{children:"common.delegate"})," and ",(0,i.jsx)(s.code,{children:"common.data"})," cover the common construction, delegation and metadata patterns."]}),"\n",(0,i.jsxs)(s.li,{children:["Icons come from the sprite. Adding a glyph means listing it in ",(0,i.jsx)(s.code,{children:"icons-manifest.txt"})," and regenerating with ",(0,i.jsx)(s.code,{children:"icons-build.mjs"}),"; CI runs ",(0,i.jsx)(s.code,{children:"npm run check:icons"}),", which fails on any stale generated file or Font Awesome version drift."]}),"\n",(0,i.jsxs)(s.li,{children:["The ",(0,i.jsx)(s.code,{children:"modalDialog"})," attribute was renamed from the Bootstrap 4 ",(0,i.jsx)(s.code,{children:"data-backdrop"})," to the Bootstrap 5 ",(0,i.jsx)(s.code,{children:"data-bs-backdrop"}),", so the static-backdrop behaviour is applied for the first time."]}),"\n",(0,i.jsxs)(s.li,{children:["The WebUI now treats only 200-299 as success. The dead ",(0,i.jsx)(s.code,{children:"|| xhr.status === 304"})," branch was removed, since the controller sends ",(0,i.jsx)(s.code,{children:"Cache-Control: no-store"})," on every response and never emits ",(0,i.jsx)(s.code,{children:"Last-Modified"})," or ",(0,i.jsx)(s.code,{children:"ETag"})," (issue ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/3330",children:"#3330"}),") (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/29d76e4a2",children:"29d76e4a"}),"). A 2xx ",(0,i.jsx)(s.code,{children:"/stat"})," response with a non-JSON body (a truncated proxy response, a captive portal page) now routes to the login dialog instead of loading a half-initialized UI (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f116cf0a8",children:"f116cf0a"}),"). If a reverse proxy in front of the controller rewrites or caches WebUI responses, verify it before upgrading."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["Force a hard reload in the browser after upgrading, as stale cached module files will not match the new ",(0,i.jsx)(s.code,{children:"main.js"})," shim."]}),"\n",(0,i.jsx)(s.h3,{id:"13-duplicated-module-sections-now-warn",children:"13. Duplicated Module Sections Now Warn"}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"rspamd_config_get_module_opt()"})," returns the ",(0,i.jsx)(s.code,{children:"rule"})," or option object from the first section only when a C module section is duplicated at the top level, for example by a stray file in ",(0,i.jsx)(s.code,{children:"modules.d"}),". The remaining sections were silently ignored and their rules never loaded. Rspamd now emits an explicit warning per configured C module so that ",(0,i.jsx)(s.code,{children:"configtest"})," and the startup log surface the problem (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/399b7461a",children:"399b7461"}),"). Lua modules were never affected, as ",(0,i.jsx)(s.code,{children:"get_all_opt()"})," flattens the whole duplicate chain."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone whose configuration defines the same C module section more than once. Nothing changes functionally, but you may discover that rules you believed were active have never been loaded."]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm configtest -s\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Fix any warning by merging the duplicate sections into a single block, then re-run ",(0,i.jsx)(s.code,{children:"configtest"})," and expect the affected rules to start loading. Verify with ",(0,i.jsx)(s.code,{children:"rspamc stat"})," or a test message that the newly loaded rules do not change your scores unexpectedly."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-414",children:"Migration to Rspamd 4.1.4"}
1),"\n",(0,i.jsx)(s.p,{children:"Rspamd 4.1.4 is primarily a bug-fix release, but several fixes intentionally change observable behaviour. Review the items below before upgrading."}),"\n",(0,i.jsx)(s.h3,{id:"1-controller-authentication-now-fails-closed-on-malformed-password-hashes",children:"1. Controller Authentication Now Fails Closed on Malformed Password Hashes"}),"\n",(0,i.jsxs)(s.p,{children:["Previously, if the ",(0,i.jsx)(s.code,{children:"password"})," or ",(0,i.jsx)(s.code,{children:"enable_password"})," configured for the controller worker was a ",(0,i.jsx)(s.em,{children:"malformed"})," encrypted hash (for example a truncated ",(0,i.jsx)(s.code,{children:"$1$"})," with the salt or key component missing), the verification code skipped the comparison entirely and accepted ",(0,i.jsx)(s.strong,{children:"any"})," supplied password. This fail-open behaviour has been fixed: a malformed hash now rejects every authentication attempt, and the malformed hash is reported in the logs. Rspamd also validates the complete encoded hash (salt and key lengths against the KDF specified by the hash id) at startup instead of only checking the ",(0,i.jsx)(s.code,{children:"$<id>"})," prefix (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/da433972a",children:"da43397"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Only setups where the configured controller password hash is incomplete or corrupted (e.g. a bad copy-paste). A tell-tale sign is that logging in to the WebUI currently works with ",(0,i.jsx)(s.em,{children:"any"})," password \u2014 that means your hash is malformed and your controller has been effectively unauthenticated. Valid hashes are unaffected."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["Verify your configured hashes have all three components: ",(0,i.jsx)(s.code,{children:"$<id>$<salt>$<key>"}),"."]}),"\n",(0,i.jsx)(s.li,{children:"If in doubt, regenerate the password hash:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm pw\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"3",children:["\n",(0,i.jsxs)(s.li,{children:["Put the new hash into ",(0,i.jsx)(s.code,{children:"local.d/worker-controller.inc"}),":"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'password = "$2$<salt>$<key>";\nenable_password = "$2$<salt>$<key>";\n'})}),"\n",(0,i.jsxs)(s.ol,{start:"4",children:["\n",(0,i.jsx)(s.li,{children:"Restart Rspamd and check the startup log: malformed password hashes are now reported explicitly. If you skip this and your hash is malformed, you will be locked out of the controller/WebUI after the upgrade (which is still strictly better than the previous behaviour of letting everyone in)."}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"2-building-with-jemalloc-requires-the-shared-library",children:"2. Building with jemalloc Requires the Shared Library"}),"\n",(0,i.jsxs)(s.p,{children:["The build system previously preferred the static ",(0,i.jsx)(s.code,{children:"libjemalloc_pic.a"})," and embedded a private copy of the allocator into each of Rspamd's shared objects, which caused a startup segfault in jemalloc-enabled builds (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6153",children:"#6153"}),"). CMake now searches for the shared jemalloc library only and ",(0,i.jsx)(s.strong,{children:"fails at configure time"})," when just a static archive is available, unless ",(0,i.jsx)(s.code,{children:"ENABLE_STATIC"})," is used. jemalloc is linked as a single shared instance per process, and ",(0,i.jsx)(s.code,{children:"libjemalloc-dev"})," is now declared as a Debian build dependency (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a2b649e9e",children:"a2b649e"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/12d999a45",children:"12d999a"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6155",children:"#6155"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Only users building from source with ",(0,i.jsx)(s.code,{children:"-DENABLE_JEMALLOC=ON"})," (including custom package rebuilds) on systems where only the static jemalloc library is installed. Users of the official packages need no action."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"What to do"})," \u2014 install the shared jemalloc development package before building:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"# Debian/Ubuntu\napt install libjemalloc-dev\n\n# Fedora/RHEL\ndnf install jemalloc-devel\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Alternatively, configure with ",(0,i.jsx)(s.code,{children:"-DENABLE_JEMALLOC=OFF"}),", or use ",(0,i.jsx)(s.code,{children:"-DENABLE_STATIC=ON"})," for fully static builds, which remain allowed to use the static archive."]}),"\n",(0,i.jsx)(s.h3,{id:"3-the-regexp-input-size-limit-now-applies-to-named-scopes",children:"3. The Regexp Input Size Limit Now Applies to Named Scopes"}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"regexp.max_size"})," limit (1 MiB by default) was only applied to the default regexp scope, so regexps registered in ",(0,i.jsx)(s.em,{children:"named"})," scopes \u2014 most notably multimap ",(0,i.jsx)(s.code,{children:"regexp_rules"}),", as well as scopes registered by Lua plugins \u2014 were matched against unbounded input. Named scopes now inherit the default scope's limit, and changing the limit updates every registered scope (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5c6644a67",children:"5c6644a"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Deployments with multimap ",(0,i.jsx)(s.code,{children:"regexp_rules"})," (or custom plugins using named regexp scopes) that relied on matching content beyond 1 MiB. Such rules will silently stop hitting on data past the limit after the upgrade."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"What to do:"})," If you legitimately need to match larger inputs, raise the limit in the ",(0,i.jsx)(s.code,{children:"regexp"})," section of your configuration:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/options.inc\nregexp {\n  max_size = 5242880; # 5 MiB; the default is 1 MiB\n}\n"})}),"\n",(0,i.jsx)(s.h3,{id:"4-word-retention-is-now-bounded-for-very-large-messages",children:"4. Word Retention Is Now Bounded for Very Large Messages"}),"\n",(0,i.jsxs)(s.p,{children:["The UTF tokenizer now drops decayed words instead of retaining them, matching the long-standing behaviour of the raw tokenizer, and a message-wide budget (100k words, 1 MiB of text, shared by all text parts and the meta words) has been introduced. Parts that hit the budget are flagged as truncated and the message is logged once (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/03a38dc84",children:"03a38dc"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Deployments processing messages with very large text parts. Word-class regexps and part similarity (fuzzy shingle) hashes no longer see the decayed words of large UTF text parts, so fuzzy hashes learned from such texts before 4.1.4 may stop matching, and Bayes token sets for those messages change. Ordinary-sized mail is unaffected \u2014 the decay only kicks in for parts of roughly a thousand words and more."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"What to do:"})," No configuration change is required or available. If you operate a fuzzy storage and have hashes learned from very large text parts, expect a one-time drift and re-learn the affected patterns after the upgrade."]}),"\n",(0,i.jsx)(s.h3,{id:"5-webui-read-only-password-grants-access-to-errors-history-and-selectors",children:"5. WebUI Read-Only Password Grants Access to Errors History and Selectors"}),"\n",(0,i.jsxs)(s.p,{children:["This is an access-control relaxation rather than a breakage, but it changes what the read-only (",(0,i.jsx)(s.code,{children:"password"}),") credential grants. Read-only users can now view the Errors history tab (",(0,i.jsx)(s.code,{children:"/errors"}),": Rspamd's internal operational error log \u2014 timestamps, pids, modules, messages) and use the Selectors tab catalogs (",(0,i.jsx)(s.code,{children:"list_extractors"}),", ",(0,i.jsx)(s.code,{children:"list_transforms"}),", ",(0,i.jsx)(s.code,{children:"check_selector"}),"). ",(0,i.jsx)(s.code,{children:"check_message"})," remains restricted to the privileged ",(0,i.jsx)(s.code,{children:"enable_password"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6156",children:"#6156"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6157",children:"#6157"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/3edd7d7d5",children:"3edd7d7"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d6b6769f0",children:"d6b6769"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Deployments that share the read-only password with semi-trusted users and assumed these endpoints stayed hidden behind the privileged password."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"What to do:"})," There is no option to restore the previous gating. Audit who holds the read-only password and rotate it if this additional visibility is not acceptable for those users."]}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-415",children:"Migration to Rspamd 4.1.5"}),"\n",(0,i.jsx)(s.h3,{id:"1-upgrade-self-hosted-fuzzy-storages-before-scanners",children:"1. Upgrade Self
1-Hosted Fuzzy Storages Before Scanners"}),"\n",(0,i.jsxs)(s.p,{children:["Rspamd 4.1.5 scanners attach a new sender-facts extension to fuzzy commands on every rule that shares extensions, i.e. a rule with encryption configured and without ",(0,i.jsx)(s.code,{children:"no_share = true"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/856fa4290",children:"856fa42"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6179",children:"#6179"}),"). The extensions wire format was not forward compatible before this release: a storage older than 4.1.5 cannot skip an unknown extension type and rejects the whole command, so the client receives no fuzzy reply at all (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/4ab21b113",children:"4ab21b1"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Deployments running their own fuzzy storage that scanners reach over encrypted rules. Users of the public rspamd.com storage do not need to do anything."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Upgrade all fuzzy storage nodes to 4.1.5 first. The storage-side parser change is backward compatible, so an upgraded storage keeps serving older clients."}),"\n",(0,i.jsx)(s.li,{children:"Upgrade the scanners."}),"\n",(0,i.jsx)(s.li,{children:"If a storage cannot be upgraded first, temporarily disable extension sharing on the scanners for that rule:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/fuzzy_check.conf, inside the affected rule\nno_share = true;\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Note also that scanners no longer send extensions for authenticated senders, for senders in ",(0,i.jsx)(s.code,{children:"local_addrs"}),", or on rules that talk to the storage in plain text (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/2d7db917c",children:"2d7db91"}),"). If your storage consumed the sender IP extension over an unencrypted transport, it stops arriving; enable encryption on the rule to get it back."]}),"\n",(0,i.jsxs)(s.h3,{id:"2-fuzzy-storage-per-key-forbidden_ids-replaces-the-global-list-again",children:["2. Fuzzy Storage: Per-Key ",(0,i.jsx)(s.code,{children:"forbidden_ids"})," Replaces the Global List Again"]}),"\n",(0,i.jsxs)(s.p,{children:["Since ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/6d15756b1",children:"6d15756"}),", the global ",(0,i.jsx)(s.code,{children:"forbidden_ids"})," worker option was applied even to clients whose key carries its own ",(0,i.jsx)(s.code,{children:"forbidden_ids"}),' ACL, so a key with an explicitly empty ACL (meaning "nothing is forbidden for this key") still had the globally listed flags suppressed. 4.1.5 restores the original semantics: a key that defines ',(0,i.jsx)(s.code,{children:"forbidden_ids"})," \u2014 even an empty list \u2014 is governed by that list alone, and the worker-level option is only a default for keys without an ACL of their own (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e0e92d6f5",children:"e0e92d6"}),"). The same rule now also applies to delayed replies to older protocol clients (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a47a85801",children:"a47a858"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Fuzzy storage operators using per-key ",(0,i.jsx)(s.code,{children:"forbidden_ids"})," together with the worker-level ",(0,i.jsx)(s.code,{children:"forbidden_ids"})," who relied on the (unintended) additive behaviour of recent releases."]}),"\n",(0,i.jsxs)(s.p,{children:["If a key must still be denied the globally forbidden flags, add those flag IDs to that key's own ",(0,i.jsx)(s.code,{children:"forbidden_ids"})," list explicitly."]}),"\n",(0,i.jsxs)(s.h3,{id:"3-fuzzy-storage-encrypted_only-no-longer-exempts-local-peers",children:["3. Fuzzy Storage: ",(0,i.jsx)(s.code,{children:"encrypted_only"}),' No Longer Exempts "Local" Peers']}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"encrypted_only"}),' option used to skip the encryption requirement for peers with a "local" source address. That test also matched IPv6 link-local and site-local addresses, so any host on the same segment could send plaintext commands; the exemption is removed, and unencrypted requests are now refused regardless of the source address (',(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5c52b376e",children:"5c52b37"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Fuzzy storages running with ",(0,i.jsx)(s.code,{children:"encrypted_only = true"})," that have clients \u2014 including ones on localhost \u2014 talking to them without encryption."]}),"\n",(0,i.jsxs)(s.p,{children:["Configure the storage's public key (",(0,i.jsx)(s.code,{children:"encryption_key"}),") in the ",(0,i.jsx)(s.code,{children:"fuzzy_check"})," rule of every such client, or remove ",(0,i.jsx)(s.code,{children:"encrypted_only"})," from ",(0,i.jsx)(s.code,{children:"local.d/worker-fuzzy.inc"}),"."]}),"\n",(0,i.jsxs)(s.h3,{id:"4-gpt-plugin-condition-callback-returns-only-a-verdict",children:["4. GPT Plugin: ",(0,i.jsx)(s.code,{children:"condition"})," Callback Returns Only a Verdict"]}),"\n",(0,i.jsxs)(s.p,{children:["The custom ",(0,i.jsx)(s.code,{children:"condition"})," callback of the ",(0,i.jsx)(s.code,{children:"gpt"})," module previously had to return both the decision and the content submitted to the LLM. It now returns only a boolean plus an optional reason string, and the request content is generated inside the module itself (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/c5ee17495",children:"c5ee174"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6159",children:"#6159"}),", see ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/5857",children:"#5857"}),"). A callback still returning content will have that content ignored."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone with a custom ",(0,i.jsx)(s.code,{children:"condition"})," in ",(0,i.jsx)(s.code,{children:"local.d/gpt.conf"}),"."]}),"\n",(0,i.jsx)(s.p,{children:"Rewrite the callback to return just the decision:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"condition = <<EOD\nreturn function(task)\n  local result = task:get_metric_result()\n  if result then\n    local score = result.score\n    if score < 2.0 or score > 8.0 then\n      return false, 'score outside the checked range'\n    end\n  end\n  return true\nend\nEOD\n"})}),"\n",(0,i.jsx)(s.h3,{id:"5-elastic-module-new-document-schema-for-fuzzy-results",children:"5. Elastic Module: New Document Schema for Fuzzy Results"}),"\n",(0,i.jsxs)(s.p,{children:["The flat ",(0,i.jsx)(s.code,{children:"fuzzy_hashes"})," field is replaced by structured per-match objects taken from ",(0,i.jsx)(s.code,{children:"task:get_fuzzy_results()"}),", carrying ",(0,i.jsx)(s.code,{children:"rule"}),", ",(0,i.jsx)(s.code,{children:"symbol"}),", ",(0,i.jsx)(s.code,{children:"found"}),", ",(0,i.jsx)(s.code,{children:"queried"}),", ",(0,i.jsx)(s.code,{children:"type"}),", ",(0,i.jsx)(s.code,{children:"prob"}),", ",(0,i.jsx)(s.code,{children:"flag"})," and ",(0,i.jsx)(s.code,{children:"exact"}),"; the new ",(0,i.jsx)(s.code,{children:"fuzzy_nested"})," module option switches that field to the Elasticsearch ",(0,i.jsx)(s.code,{children:"nested"})," type. Message ",(0,i.jsx)(s.code,{children:"size"})," is now exported with an explicit mapping as well (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/2fbbb4daa",children:"2fbbb4d"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6154",children:"#6154"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6136",children:"#6136"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6147",children:"#6147"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Users of the ",(0,i.jsx)(s.code,{children:"elastic"})," module with saved searches, dashboards or ingest pipelines referencing ",(0,i.jsx)(s.code,{children:"fuzzy_hashes"}),"."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Update queries and visualisations to the new structured field."}),"\n",(0,i.jsx)(s.li,{children:"The updated mapping applies to indices created after the upgrade; documents in pre-upgrade indices keep the old shape, so anything spanning the upgrade date must handle both."}),"\n",(0,i.jsxs)(s.li,{children:["Set ",(0,i.jsx)(s.code,{children:"fuzzy_nested = true"})," in ",(0,i.jsx)(s.code,{children:"local.d/elastic.conf"})," if you need to query several attributes of a single match together."]}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"6-file-and-shared-memory-message-inputs-are-now-gated",children:"6. File and Shared-Memory Message Inputs Are Now Gated"}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"File"}),"/",(0,i.jsx)(s.code,{children:"Path"}),"/",(0,i.jsx)(s.code,{children:"Shm"}),"/",(0,i.jsx)(s.code,{children:"Shm-Offset"}),"/",(0,i.jsx)(s.code,{children:"Shm-Length"})," headers, their v3 metadata equivalents and the proxy's ",(0,i.jsx)(s.code,{children:"File"})," query argument make rspamd open a path or map a shared-memory object named by the client. A new per-worker boolean ",(0,i.jsx)(s.code,{children:"allow_file_and_shm_inputs"})," on the normal, controller and proxy workers now gates them: connections accepted on a unix s
1ocket always keep the capability, while TCP connections have it only while the option is enabled (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5c52b376e",children:"5c52b37"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:["For 4.1.5 the shipped default is ",(0,i.jsx)(s.code,{children:"true"}),", so nothing changes out of the box, but every TCP listener logs a startup warning while the option is enabled, and ",(0,i.jsxs)(s.strong,{children:["the default becomes ",(0,i.jsx)(s.code,{children:"false"})," in the next major release"]})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d1adb29ac",children:"d1adb29"}),"). Independently of the option, the proxy now strips client-supplied ",(0,i.jsx)(s.code,{children:"Shm*"})," headers and the ",(0,i.jsx)(s.code,{children:"File"}),"/",(0,i.jsx)(s.code,{children:"Path"}),"/",(0,i.jsx)(s.code,{children:"Shm"})," query arguments, so scanning local files ",(0,i.jsx)(s.em,{children:"through"})," ",(0,i.jsx)(s.code,{children:"rspamd_proxy"})," no longer works \u2014 such clients must talk to a scanner listener directly."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Setups where clients pass file paths or shared memory instead of the message body over TCP. Typical MTA integrations, which send the message body, are not affected."]}),"\n",(0,i.jsx)(s.p,{children:"If no client of a worker uses these inputs, opt in to the future default now and silence the warning:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/worker-normal.inc (likewise worker-controller.inc, worker-proxy.inc)\nallow_file_and_shm_inputs = false;\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Otherwise move such clients to a unix socket, or keep the option enabled only on listeners not exposed to untrusted networks \u2014 passwords, ",(0,i.jsx)(s.code,{children:"secure_ip"})," and connection encryption deliberately do not unlock this capability."]}),"\n",(0,i.jsxs)(s.p,{children:["The same change set adds ",(0,i.jsx)(s.code,{children:"max_connections"})," and ",(0,i.jsx)(s.code,{children:"max_connections_per_source"})," limits to the controller and proxy, and the scanner's ",(0,i.jsx)(s.code,{children:"max_tasks"})," now counts accepted connections that are still sending their body, so saturated scanners may hit the limit earlier than before."]}),"\n",(0,i.jsx)(s.h3,{id:"7-controller-rate-limits-failed-authentications",children:"7. Controller Rate-Limits Failed Authentications"}),"\n",(0,i.jsxs)(s.p,{children:["The controller now throttles password failures per source: after 10 failed attempts within 60 seconds, further requests from that source get HTTP 429 until the window drains. A successful login clears the counter, and trusted sources (",(0,i.jsx)(s.code,{children:"secure_ip"})," matches and unix-socket clients) are never throttled (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/18fc61992",children:"18fc619"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Deployments with monitoring or scripts that poll the controller with a wrong password, and setups behind a reverse proxy, where all clients share the proxy's source address and therefore one bucket."]}),"\n",(0,i.jsx)(s.p,{children:"Fix misconfigured clients, or tune the limits:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"# local.d/worker-controller.inc\nmax_auth_failures = 10;    # 0 disables throttling\nauth_failure_window = 60s;\n"})}),"\n",(0,i.jsx)(s.h3,{id:"8-previously-broken-checks-now-fire-and-can-change-scores",children:"8. Previously Broken Checks Now Fire and Can Change Scores"}),"\n",(0,i.jsx)(s.p,{children:"Two long-standing bugs are fixed in ways that change symbol hit rates:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"url:get_urls_filtered()"})," called without an include list \u2014 and therefore ",(0,i.jsx)(s.code,{children:"rspamadm mime urls"})," and the ",(0,i.jsx)(s.code,{children:"URI_COUNT_ODD"})," rule \u2014 no longer drops URLs that carry no flags, which is exactly what URLs extracted from HTML parts are. ",(0,i.jsx)(s.code,{children:"URI_COUNT_ODD"})," was running its parity check on an undercount for the multipart/alternative messages it targets (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/81256c96a",children:"81256c9"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/5891",children:"#5891"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6182",children:"#6182"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"HFILTER_HELO_IP_A"})," now fires when the HELO hostname resolves to addresses that do not include the connecting IP, as documented. Previously it fired only when the HELO resolved to no addresses at all, a condition already covered by ",(0,i.jsx)(s.code,{children:"HFILTER_HELO_NORES_A_OR_MX"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5f5c176d6",children:"5f5c176"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6176",children:"#6176"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6165",children:"#6165"}),")."]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"No configuration change is required, but review local score overrides and composites involving these symbols after the upgrade if your scoring depends on them."}),"\n",(0,i.jsx)(s.h2,{id:"migration-to-rspamd-420",children:"Migration to Rspamd 4.2.0"}),"\n",(0,i.jsxs)(s.h3,{id:"1-multimap-top-filter-returns-the-full-public-suffix",children:["1. Multimap ",(0,i.jsx)(s.code,{children:"top"})," Filter Returns the Full Public Suffix"]}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"top"})," filter of multimap used to return the last label of the eSLD, which conflates the ",(0,i.jsx)(s.code,{children:"uk"})," and ",(0,i.jsx)(s.code,{children:"co.uk"})," namespaces. It now returns the actual public suffix from the suffix lookup, falling back to the old derivation only when no suffix list is loaded or nothing matches (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/8f7e6dc2d",children:"8f7e6dc2d"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone with a multimap rule using ",(0,i.jsx)(s.code,{children:'filter = "top"'})," whose map lists bare labels of multi-label suffixes (",(0,i.jsx)(s.code,{children:"au"}),", ",(0,i.jsx)(s.code,{children:"uk"}),", ",(0,i.jsx)(s.code,{children:"br"}),", ",(0,i.jsx)(s.code,{children:"jp"}),", \u2026)."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["Find the maps used by ",(0,i.jsx)(s.code,{children:"top"}),"-filtered rules:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"grep -rn 'filter *= *\"top\"' /etc/rspamd/local.d/ /etc/rspamd/override.d/\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"2",children:["\n",(0,i.jsx)(s.li,{children:"In each of those maps, replace bare labels with the full suffixes you mean:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"grep -nE '^(au|uk|nz|br|jp|za|il)$' /p
1ath/to/your/map\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"au"})," must become ",(0,i.jsx)(s.code,{children:"com.au"}),", ",(0,i.jsx)(s.code,{children:"net.au"}),", \u2026 and ",(0,i.jsx)(s.code,{children:"uk"})," must become ",(0,i.jsx)(s.code,{children:"co.uk"}),", ",(0,i.jsx)(s.code,{children:"org.uk"}),", \u2026 Single-label suffixes (",(0,i.jsx)(s.code,{children:"com"}),", ",(0,i.jsx)(s.code,{children:"org"}),", ",(0,i.jsx)(s.code,{children:"de"}),") are unchanged.\n3. Reload and verify with a message whose sender domain is under the affected suffix."]}),"\n",(0,i.jsx)(s.h3,{id:"2-public-suffix-resolution-reworked",children:"2. Public Suffix Resolution Reworked"}),"\n",(0,i.jsxs)(s.p,{children:["TLD resolution no longer scans hosts with a multipattern; it probes label suffixes against a flat hash map with full public suffix list semantics (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5d181935e",children:"5d181935e"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ac05136e3",children:"ac05136e3"}),"). Two behaviours change:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"!"})," exception rules of the public suffix list are honoured now \u2014 they were silently skipped before."]}),"\n",(0,i.jsx)(s.li,{children:"A host that is itself a public suffix resolves to the whole host instead of being attributed to a shorter suffix."}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Maps, selectors, RBL composition rules and reputation rules keyed on eSLD (",(0,i.jsx)(s.code,{children:"get_tld"}),", ",(0,i.jsx)(s.code,{children:"url:get_tld"}),", ",(0,i.jsx)(s.code,{children:"rbl"})," composition maps). The RBL URL composition maps also moved from per-TLD regex buckets to the same suffix rule set (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ddd3fbe23",children:"ddd3fbe23"}),"): exceptions keep the default eSLD, wildcard rules keep the whole host, and the bare parent is added alongside each wildcard rule."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"After the upgrade, re-check any domain allowlist or blocklist entry that sits on or under an exception rule of the PSL, since the key it is matched by may have changed."}),"\n",(0,i.jsx)(s.li,{children:"Verify a sample of your rules against real URLs:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:'rspamadm lua -e \'print(require("rspamd_util").get_tld("www.city.kobe.jp"))\'\n'})}),"\n",(0,i.jsxs)(s.p,{children:["Also new in this release and available for rules that need exact semantics: ",(0,i.jsx)(s.code,{children:"url:get_public_suffix()"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/8d9e06df3",children:"8d9e06df3"}),"), the ",(0,i.jsx)(s.code,{children:"get_public_suffix"})," selector transform (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/6c354b1ab",children:"6c354b1ab"}),") and the ",(0,i.jsx)(s.code,{children:"rspamd_tld_lookup"})," Lua module (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/7318989ea",children:"7318989ea"}),")."]}),"\n",(0,i.jsx)(s.h3,{id:"3-spf-multiple-records-now-produce-a-permerror",children:"3. SPF: Multiple Records Now Produce a Permerror"}),"\n",(0,i.jsxs)(s.p,{children:["A domain publishing more than one ",(0,i.jsx)(s.code,{children:"v=spf1"})," record used to be evaluated using whichever record the resolver happened to answer first. RFC 7208 4.5 requires a permerror, and that is what Rspamd returns now (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/ffe728811",children:"ffe728811"}),"). The version literal is also matched as the RFC defines it: case-insensitively and terminated by a space or the end of the record, so ",(0,i.jsx)(s.code,{children:"V=SPF1"})," now counts as a duplicate while ",(0,i.jsx)(s.code,{children:"v=spf10"})," no longer counts as an SPF record at all (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/0e1aec738",children:"0e1aec738"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone whose own domains, or whose customers' domains, publish two SPF records. Such mail moves from a pass/fail verdict to ",(0,i.jsx)(s.code,{children:"R_SPF_PERMFAIL"}),", which also changes the DMARC outcome."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Audit the domains you are responsible for before upgrading:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"dig +short TXT example.com | grep -ci 'v=spf1'\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Anything above ",(0,i.jsx)(s.code,{children:"1"})," must be merged into a single record.\n2. Domains publishing one SPF record next to unrelated TXT records (vendor verification tokens) are unaffected."]}),"\n",(0,i.jsxs)(s.h3,{id:"4-dkim-alignment-is-a-symbol-and-r_dkim_allow-is-worth-half",children:["4. DKIM Alignment Is a Symbol, and ",(0,i.jsx)(s.code,{children:"R_DKIM_ALLOW"})," Is Worth Half"]}),"\n",(0,i.jsxs)(s.p,{children:["Whether a signature authorises the author is now decided in the DKIM module and reported as the new ",(0,i.jsx)(s.code,{children:"R_DKIM_ALIGNED"})," symbol, with the option ",(0,i.jsx)(s.code,{children:"strict"})," for an exact domain match and ",(0,i.jsx)(s.code,{children:"relaxed"})," when only the organisational domains agree (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f30ef2071",children:"f30ef2071"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6197",children:"#6197"}),"). DMARC consumes those facts and drops its own comparison; no DMARC verdict moves."]}),"\n",(0,i.jsxs)(s.p,{children:["The credit a passing signature earns is split between the two symbols instead of being added to: ",(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"R_DKIM_ALLOW"})," goes from -0.2 to -0.1"]}),", and ",(0,i.jsx)(s.code,{children:"R_DKIM_ALIGNED"})," takes the other -0.1. A signature aligned with the author is worth what a passing signature used to be worth on its own; one that merely verifies for an unrelated domain now earns half of that."]}),"\n",(0,i.jsxs)(s.p,{children:["Alignment is only reported when the message has a single author: with several mailboxes in ",(0,i.jsx)(s.code,{children:"From"}),", no ",(0,i.jsx)(s.code,{children:"R_DKIM_ALIGNED"}
1)," and no mempool verdict is produced (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e704b8445",children:"e704b8445"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone who overrides the ",(0,i.jsx)(s.code,{children:"R_DKIM_ALLOW"})," score, or whose thresholds were tuned around it."]}),"\n",(0,i.jsxs)(s.p,{children:["To keep the old total for aligned mail while neutralising the new symbol, add to ",(0,i.jsx)(s.code,{children:"local.d/groups.conf"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'group "dkim" {\n  symbols {\n    "R_DKIM_ALLOW" {\n      score = -0.2;\n    }\n    "R_DKIM_ALIGNED" {\n      score = 0.0;\n    }\n  }\n}\n'})}),"\n",(0,i.jsx)(s.h3,{id:"5-symbols-cache-one-scheduler-stricter-dependency-rules",children:"5. Symbols Cache: One Scheduler, Stricter Dependency Rules"}),"\n",(0,i.jsxs)(s.p,{children:["The per-class schedulers (priority-serialised pre/postfilters, topologically ordered filters) are replaced by a single execution plan computed at init: every item gets a stage and a level within that stage, and a dependency inherits the earliest (stage, level) among its dependents (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d144e0df0",children:"d144e0df0"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6225",children:"#6225"}),"). Three consequences are user-visible:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Invalid dependency edges are now rejected at configuration time."})," A connfilter or prefilter depending on a filter used to pull that filter into the wrong stage by accident; such an edge is reported with the stages of both symbols and dropped (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/cc9038fc9",children:"cc9038fc9"}),"). Dependency cycles are likewise broken right after resolution, with an error naming both symbols (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/59c5f9c2e",children:"59c5f9c2e"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Prefilters may now legitimately depend on filters"}),", which hoists the filter and its own dependencies to the prefilter stage. Hoists are logged at info level, so a ratelimit keyed by ",(0,i.jsx)(s.code,{children:"symbol(DKIM_CHECK)"})," or by a selector now costs visibly (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/57128714b",children:"57128714b"}),"). Ratelimit, multimap, rbl and reputation rules register the dependencies of their selectors automatically (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d6419aae2",children:"d6419aae2"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsxs)(s.strong,{children:[(0,i.jsx)(s.code,{children:"ignore_passthrough"})," filters now actually run after a pre-result."]})," ",(0,i.jsx)(s.code,{children:"task:set_pre_result()"})," no longer marks the classifier stages as processed, so the filters stage is no longer jumped over (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/fec387488",children:"fec387488"}),")."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone with custom Lua rules using ",(0,i.jsx)(s.code,{children:"rspamd_config:register_dependency()"}),", and anyone whose prefilters set a pre-result while other symbols are flagged ",(0,i.jsx)(s.code,{children:"ignore_passthrough"}),"."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Before restarting the daemon, validate the configuration \u2014 rejected edges and broken cycles are reported here:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm configtest\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"2",children:["\n",(0,i.jsx)(s.li,{children:"Inspect the computed execution order and any hoists:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm configdump -e\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"3",children:["\n",(0,i.jsx)(s.li,{children:"Fix any rejected edge by moving the symbol to a compatible stage rather than relying on the old accidental execution."}),"\n"]}),"\n",(0,i.jsx)(s.h3,{id:"6-fuzzy-storage-hash-count-comes-from-a-periodic-scan",children:"6. Fuzzy Storage: Hash Count 
1Comes From a Periodic SCAN"}),"\n",(0,i.jsxs)(s.p,{children:["The Redis backend used to maintain ",(0,i.jsx)(s.code,{children:"<prefix>_count"})," inside the update script: incremented on every add including a re-learn, decremented on every delete including a missing digest, and never decremented on expiry. The reported number only grew, and a negative value turned into a huge one. Updates no longer touch the counter; instead the first fuzzy worker walks the keyspace with a paced SCAN and publishes the result (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/252d3e378",children:"252d3e378"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6259",children:"#6259"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:["Defaults: a pass at most every 4 hours, ",(0,i.jsx)(s.code,{children:"SCAN COUNT"})," 1000, a 10% duty cycle, all configurable in the ",(0,i.jsx)(s.code,{children:"count_scan"})," section of the fuzzy worker. The scan is pinned to one read server (a replica when configured), progress is checkpointed so a restarted worker resumes, and the lock lives in Redis so storages sharing a Redis run one pass per interval."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Operators of fuzzy storage, and anyone alerting on the fuzzy hash count."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["Expect the count to read ",(0,i.jsx)(s.code,{children:"0"})," right after the upgrade: negative counters left by older versions are reported as 0 until the first pass completes, which may take up to the configured interval."]}),"\n",(0,i.jsxs)(s.li,{children:["If your Redis uses ACLs, make sure the fuzzy storage user may run ",(0,i.jsx)(s.code,{children:"SCAN"})," on the read server it is pinned to."]}),"\n",(0,i.jsxs)(s.li,{children:["Re-baseline any monitoring threshold on the fuzzy hash count \u2014 the new number is the real one and is usually ",(0,i.jsx)(s.em,{children:"lower"})," than what the old counter reported."]}),"\n",(0,i.jsxs)(s.li,{children:["Optional: sampled storage statistics (weights per flag, shingles, creation age buckets) are collected during the same scan and published under ",(0,i.jsx)(s.code,{children:"<prefix>_stats"}),", exposed as the ",(0,i.jsx)(s.code,{children:"storage"})," object of ",(0,i.jsx)(s.code,{children:"/fuzzystat"})," and printed by ",(0,i.jsx)(s.code,{children:"rspamadm fuzzystat"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/274ed527a",children:"274ed527a"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6260",children:"#6260"}),"). To restore the plain SCAN, in ",(0,i.jsx)(s.code,{children:"local.d/worker-fuzzy.inc"}),":"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"count_scan {\n  stats_sample = 0;\n}\n"})}),"\n",(0,i.jsx)(s.p,{children:"The wire STAT reply still carries only the count, so fuzzy clients see nothing new."}),"\n",(0,i.jsx)(s.h3,{id:"7-fuzzy-storage-key-policy-precedence-and-per-source-statistics",children:"7. Fuzzy Storage: Key Policy Precedence and Per-Source Statistics"}),"\n",(0,i.jsx)(s.p,{children:"Two changes to fuzzy storage key handling need attention."}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Source policy is applied after decryption"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5d054b248",children:"5d054b248"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6255",children:"#6255"}),"). A request carrying a valid customer key is no longer dropped by a static or dynamic IP ban; IP restrictions are kept for the shared default key, and per-key limits, expiry and permissions are retained. Telemetry for blocked requests is preserved before they are dropped."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Storage operators who used IP bans to cut off a client that still holds a valid key. Such a client is now served. Revoke or expire the key itself instead of banning the address."]}),"\n",(0,i.jsxs)(s.p,{children:["Key expiry parsing was also repaired: ",(0,i.jsx)(s.code,{children:"strptime"})," left the time-of-day and DST fields untouched when parsing ",(0,i.jsx)(s.code,{children:"DD-MM-YYYY"}),", so an expired key could inherit a far-future expiry from stack contents, and failed date parses are now rejected (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/f24fc15f5",children:"f24fc15f5"}),"). Re-check any key you believed to be expired."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Per-source statistics are now lazy, and off by default for the default key"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/5c92924a9",children:"5c92924a9"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6248",children:"#6248"}),"). Tables are created on the first request that uses a key, starting at four buckets. The default key tracks nothing unless given an explicit cap, since it is the one key whose source set a bounded table cannot represent \u2014 ",(0,i.jsx)(s.code,{children:"rspamadm fuzzystat"})," says so rather than printing nothing."]}),"\n",(0,i.jsxs)(s.p,{children:["To restore per-IP tracking, set the worker default or a per-key override in ",(0,i.jsx)(s.code,{children:"local.d/worker-fuzzy.inc"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"max_ips_per_key = 1024;\n"})}),"\n",(0,i.jsxs)(s.p,{children:["and give the default key an explicit ",(0,i.jsx)(s.code,{children:"max_ips"})," extension in the keys file (",(0,i.jsx)(s.code,{children:"0"})," disables tracking for that key). The statistics output gains ",(0,i.jsx)(s.code,{children:"ips_inserted"})," and ",(0,i.jsx)(s.code,{children:"ips_overflow"}),"; a table whose evictions exceed eight times its cap within an hour is dropped and the key is flagged for the rest of its life."]}),"\n",(0,i.jsx)(s.h3,{id:"8-ucl-parsing-of-untrusted-input-is-bounded",children:"8. UCL Parsing of Untrusted Input Is Bounded"}),"\n",(0,i.jsxs)(s.p,{children:["Request bodies were parsed without a structural budget, so a body well within ",(0,i.jsx)(s.code,{children:"max_message"})," could describe a tree orders of magnitude larger than its wire size. Untrusted parsing now applies a depth of 64, a million elements, 1KB ke
1ys, 16MB strings and ",(0,i.jsx)(s.code,{children:"min(96 * inlen + 256KB, 64MB)"})," of tree (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/8c269facc",children:"8c269facc"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6207",children:"#6207"}),"). This covers checkv3 metadata in both JSON and msgpack form, both controller body parsers and both proxy parsers. Container nesting is bounded for every parse type at the libucl default of 1024 (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d2c0d372a",children:"d2c0d372a"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone submitting very large or deeply nested scan metadata through checkv3, the controller or the proxy. Such a request is now rejected instead of being parsed."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"If you pass generated metadata (large recipient lists, nested JSON blobs) with scan requests, check that no single string exceeds 16MB and that nesting stays under 64."}),"\n",(0,i.jsxs)(s.li,{children:["Lua modules parsing replies from outside the local configuration now use ",(0,i.jsx)(s.code,{children:"ucl.untrusted_parser()"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a7c1d5d54",children:"a7c1d5d54"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/0b730fb15",children:"0b730fb15"}),") \u2014 HTTP request headers, the LLM providers, the ",(0,i.jsx)(s.code,{children:"lua_scanners"})," backends, bimi, contextal, the updates and openphish feeds, the external neural service. A custom module parsing network replies should do the same, overriding individual caps where the data legitimately needs it:"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-lua",children:"local parser = ucl.untrusted_parser({max_string_length = 64 * 1024 * 1024})\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Clickhouse, elastic, ",(0,i.jsx)(s.code,{children:"lua_cache"}),", ",(0,i.jsx)(s.code,{children:"history_redis"}),", the neural model paths and everything in config or ",(0,i.jsx)(s.code,{children:"rspamadm"})," mode keep the ordinary parser on purpose."]}),"\n",(0,i.jsx)(s.h3,{id:"9-clickhouse-schema-12",children:"9. ClickHouse Schema 12"}),"\n",(0,i.jsxs)(s.p,{children:["Migration 11 \u2192 12 adds bloom-filter data-skipping indexes (granularity 4) on ",(0,i.jsx)(s.code,{children:"MessageId"}),", ",(0,i.jsx)(s.code,{children:"From"}),", ",(0,i.jsx)(s.code,{children:"MimeFrom"}),", ",(0,i.jsx)(s.code,{children:"IP"}),", ",(0,i.jsx)(s.code,{children:"Urls.Tld"})," and ",(0,i.jsx)(s.code,{children:"Attachments.Digest"}),", plus a bloom filter on ",(0,i.jsx)(s.code,{children:"Subject"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/b9275cfa6",children:"b9275cfa6"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/b1ddb79cb",children:"b1ddb79cb"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6241",children:"#6241"}),"). On a 20M messages/day install, a single Message-ID lookup over 30 days read 303M rows and 15.5 GB before this change."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," All ClickHouse module users. The migration runs automatically and is metadata-only, so it is fast and safe \u2014 but new parts carry the index while older parts only pick it up as they merge or expire."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure (optional, operator's choice):"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Let Rspamd apply the migration by restarting normally, then confirm:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"clickhouse-client -q \"SELECT name, type_full FROM system.data_skipping_indices WHERE table = 'rspamd'\"\n"})}),"\n",(0,i.jsxs)(s.ol,{start:"2",children:["\n",(0,i.jsx)(s.li,{children:"To make the indexes effective on existing parts immediately, materialise them by name. This is deliberately left to you because it rereads the column as a background mutation:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:'clickhouse-client -q "ALTER TABLE rspamd MATERIALIZE INDEX <index_name>"\n'})}),"\n",(0,i.jsxs)(s.ol,{start:"3",children:["\n",(0,i.jsx)(s.li,{children:"Watch the mutation until it drains before starting the next one:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"clickhouse-client -q \"SELECT * FROM system.mutations WHERE table = 'rspamd' AND NOT is_done\"\n"})}),"\n",(0,i.jsx)(s.h3,{id:"10-new-attachment-and-hidden-text-scoring",children:"10. New Attachment and Hidden-Text Scoring"}),"\n",(0,i.jsx)(s.p,{children:"Two groups of changes move scores on messages that previously scored differently. Re-check your thresholds and any per-symbol overrides against a ham corpus before rolling this to production."}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"SVG attachments are parsed"})," and produce ",(0,i.jsx)(s.code,{children:"SVG_CONTENT"}),", ",(0,i.jsx)(s.code,{children:"SVG_SUSPICIOUS"}),", ",(0,i.jsx)(s.code,{children:"SVG_SCRIPT"}),", ",(0,i.jsx)(s.code,{children:"SVG_FOREIGN_OBJECT"}),", ",(0,i.jsx)(s.code,{children:"SVG_DATA_URI"}),", ",(0,i.jsx)(s.code,{children:"SVG_EXTERNAL_LINKS"}),", ",(0,i.jsx)(s.code,{children:"SVG_EXTERNAL_RESOURCES"}),", ",(0,i.jsx)(s.code,{children:"SVG_FORM"})," and ",(0,i.jsx)(s.code,{children:"SVG_REDIRECT"}),", with ",(0,i.jsx)(s.strong,{children:"small non-zero default weights on the scripting and embedding indicators"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/c7deefa9a",children:"c7deefa9a"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6240",children:"#6240"}),"). HTML smuggled inside an SVG is injected back into the scan as an HTML part, so its URLs reach the RBL and reputation pipeline (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e05302399",children:"e05302399"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/960b17959",children:"960b17959"}),"). Configure or disable under ",(0,i.jsx)(s.code,{children:"lua_content"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:"lua_content {\n  svg {\n    enabled = false;\n  }\n}\n"})}),"\n",(0,i.jsxs)(s.p,{children:["XLSX and PPTX join DOCX in the OOXML pipeline, adding ",(0,i.jsx)(s.code,{children:"XLSX_CONTENT"}),", ",(0,i.jsx)(s.code,{children:"XLSX_EXTERNAL_LINKS"}),", ",(0,i.jsx)(s.code,{children:"XLSX_SUSPICIOUS"}),", ",(0,i.jsx)(s.code,{children:"PPTX_CONTENT"}),", ",(0,i.jsx)(s.code,{children:"PPTX_EXTERNAL_LINKS"}),", ",(0,i.jsx)(s.code,{children:"PPTX_SUSPICIOUS"}),", ",(0,i.jsx)(s.code,{children:"OOXML_MACROS"}),", ",(0,i.jsx)(s.code,{children:"OOXML_OLE_OBJECT"}),", ",(0,i.jsx)(s.code,{children:"OOXML_REMOTE_TEMPLATE"})," and ",(0,i.jsx)(s.code,{children:"OOXML_EXTERNAL_DATA"})," \u2014 all with ",(0,i.jsx)(s.strong,{children:"zero weight by default"})," (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/fe26f2923",children:"fe26f2923"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6238",children:"#6238"}),"). They are safe to leave alone until you decide to score them."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"CSS handling changed substantially, in both directions."})," Compound selectors and combinators (",(0,i.jsx)(s.code,{children:"div.mainbox"}),", ",(0,i.jsx)(s.code,{children:"div p"}),", ",(0,i.jsx)(s.code,{children:"div > p"}),", ",(0,i.jsx)(s.code,{children:"h1 + p"}),", ",(0,i.jsx)(s.code,{children:"h1 ~ p"}),") are evaluated now, so hidden text behind the rules mail templates actually use is detected (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e6162390a",children:"e6162390a"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6252",children:"#6252"}),"), and ",(0,i.jsx)(s.code,{children:"R_WHITE_ON_WHITE"})," fires on white-on-white text whether or not the message also contains hidden text (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/307fa376f",children:"307fa376f"}),", fixes ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6262",children:"#6262"}),"). At the same time several severe false positives are gone: a grouped selector that could not be evaluated used to register a rule for a bare tag and move whole message bodies to the invisible buffer (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/d89aa4a8c",children:"d89aa4a8c"}),"), and the ",(0,i.jsx)(s.code,{children:"font"})," shorthand, the ",(0,i.jsx)(s.code,{children:"vw"}),"/",(0,i.jsx)(s.code,{children:"vh"})," units and percent sizes were all mishandled so that ordinary webmail replies compiled as invisible (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/e7d85786c",children:"e7d85786c"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6245",children:"#6245"}),"). Expect ",(0,i.jsx)(s.code,{children:"HIDDEN_TEXT"})," to stop firing on legitimate mail and to start firing on the templates it was meant for."]}),"\n",(0,i.jsxs)(s.h3,{id:"11-url_redirector-redirectors_only-is-now-enforced",children:["11. ",(0,i.jsx)(s.code,{children:"url_redirector"}),": ",(0,i.jsx)(s.code,{children:"redirectors_only"})," Is Now Enforced"]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"map:get_key()"})," returns ",(0,i.jsx)(s.code,{children:"false"}),", not ",(0,i.jsx)(s.code,{children:"nil"}),", on a miss for glob, regexp and set maps, so the ",(0,i.jsx)(s.code,{children:"redirectors_only"})," gate written as ",(0,i.jsx)(s.code,{children:"get_key(...) ~= nil"})," was always true and ",(0,i.jsx)(s.strong,{children:"every"})," redirect target was followed regardless of ",(0,i.jsx)(s.code,{children:"redirector_hosts_map"}),". The gate now uses truthiness and is applied at all three sites that can issue an HTTP request for a hop (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/a16e47594",children:"a16e47594"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6198",children:"#6198"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:["A related map cache bug is fixed alongside: a map given as a ",(0,i.jsx)(s.em,{children:"list"})," of URLs was keyed by the digest of the list alone, ignoring the requested type, so whichever module registered first decided the type for everyone. ",(0,i.jsx)(s.code,{children:"url_redirector"})," asked for a glob map of ",(0,i.jsx)(s.code,{children:"redirector_hosts_map"})," while a multimap ",(0,i.jsx)(s.code,{children:"redirector"})," rule asked for a hash map of the same URLs, and no glob pattern in that feed could ever match (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/19bad32bb",children:"19bad32bb"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/53acca685",children:"53acca685"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/pull/6226",children:"#6226"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Anyone running ",(0,i.jsx)(s.code,{children:"url_redirector"})," with ",(0,i.jsx)(s.code,{children:"redirectors_only = true"}),", and anyone sharing one URL list between modules that ask for different map types."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsx)(s.li,{children:"Expect noticeably fewer outbound HTTP requests and fewer resolved redirect targets after the upgrade \u2014 that is the documented behaviour finally taking effect."}),"\n",(0,i.jsxs)(s.li,{children:["If you relied on full redirect following, set ",(0,i.jsx)(s.code,{children:"redirectors_only = false"})," explicitly in ",(0,i.jsx)(s.code,{children:"local.d/url_redirector.conf"})," rather than depending on the old bug."]}),"\n",(0,i.jsx)(s.li,{children:"Confirm which hops are actually fetched using the debug line added before every request:"}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"rspamadm configtest -L url_redirector\n"})}),"\n",(0,i.jsxs)(s.h3,{id:"12-forged_recipients-compares-by-mailbox-identity-and-fetches-a-new-map",children:["12. ",(0,i.jsx)(s.code,{children:"forged_recipients"})," Compares by Mailbox Identity and Fetches a New Map"]}),"\n",(0,i.jsxs)(s.p,{children:["Envelope and header addresses were compared as raw strings, so ",(0,i.jsx)(s.code,{children:"[email protected]"})," with ",(0,i.jsx)(s.code,{children:"RCPT TO [email protected]"}),", or ",(0,i.jsx)(s.code,{children:"From: [email protected]"})," with ",(0,i.jsx)(s.code,{children:"MAIL FROM [email protected]"}),", were reported as ",(0,i.jsx)(s.code,{children:"FORGED_RECIPIENTS"})," / ",(0,i.jsx)(s.code,{children:"FORGED_SENDER"})," although both sides name one mailbox. Both sides now match by ",(0,i.jsx)(s.code,{children:"lua_aliases.mailbox_identity"}),": equivalent domains are folded, gmail dots and plus tags are stripped (",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/7bb2c77a0",children:"7bb2c77a0"}),", ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/commit/8b36f110e",children:"8b36f110e"}),", issue ",(0,i.jsx)(s.a,{href:"https://github.com/rspamd/rspamd/issues/6227",children:"#6227"}),")."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"Who is affected:"})," Everyone \u2014 the equivalence classes come from a ",(0,i.jsxs)(s.strong,{children:["new map fetched from ",(0,i.jsx)(s.code,{children:"maps.rspamd.com"})]}),"."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.strong,{children:"Migration procedure:"})}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:["If your workers reach ",(0,i.jsx)(s.code,{children:"maps.rspamd.com"})," through a firewall or proxy, allow ",(0,i.jsx)(s.code,{children:"rspamd/equivalent_domains.inc"})," alongside the other Rspamd maps. A fallback snapshot ships in ",(0,i.jsx)(s.code,{children:"conf/maps.d"}),", so a blocked fetch degrades to the builtin Google class rather than failing."]}),"\n",(0,i.jsxs)(s.li,{children:["To point the option at your own list, override it in ",(0,i.jsx)(s.code,{children:"local.d/forged_recipients.c
1onf"}),":"]}),"\n"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-hcl",children:'equivalent_domains = "/etc/rspamd/maps.d/equivalent_domains.inc";\n'})}),"\n",(0,i.jsxs)(s.ol,{start:"3",children:["\n",(0,i.jsxs)(s.li,{children:["Expect fewer ",(0,i.jsx)(s.code,{children:"FORGED_RECIPIENTS"})," and ",(0,i.jsx)(s.code,{children:"FORGED_SENDER"})," hits. The symbol options still show the wire addresses, and the addresses on the task are not modified \u2014 the identity is used for comparison only, never written back, since the domains stay distinct for SPF, DKIM and DMARC."]}),"\n"]})]})}function h(e={}){const{wrapper:s}={...(0,t.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.