PageSourceSearch

https://docs.rspamd.com/assets/js/09f5bbae.3302ac54.js

js rspamd.com collected 2026-10-02 02:29:13 UTC 45,205 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkrspamd_docs=self.webpackChunkrspamd_docs||[]).push([[2595],{28453:(e,n,s)=>{s.d(n,{R:()=>a,x:()=>d});var t=s(96540);const i={},r=t.createContext(i);function a(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),t.createElement(r.Provider,{value:n},e.children)}},93360:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>h,frontMatter:()=>a,metadata:()=>t,toc:()=>o});const t=JSON.parse('{"id":"configuration/settings","title":"User settings","description":"Introduction","source":"@site/docs/configuration/settings.md","sourceDirName":"configuration","slug":"/configuration/settings","permalink":"/configuration/settings","draft":false,"unlisted":false,"editUrl":"https://github.com/rspamd/docs.rspamd.com/edit/master/docs/configuration/settings.md","tags":[],"version":"current","frontMatter":{"title":"User settings"},"sidebar":"docs","previous":{"title":"Configuration Guide","permalink":"/configuration/"},"next":{"title":"Actions and scores","permalink":"/configuration/metrics"}}');var i=s(74848),r=s(28453);const a={title:"User settings"},d="Rspamd user settings",l={},o=[{value:"Introduction",id:"introduction",level:2},{value:"Settings structure",id:"settings-structure",level:2},{value:"Settings match",id:"settings-match",level:3},{value:"Redis settings",id:"redis-settings",level:3},{value:"Layered settings merge (4.0.1+)",id:"layered-settings-merge-401",level:2},{value:"Layer hierarchy",id:"layer-hierarchy",level:3},{value:"How layers are applied",id:"how-layers-are-applied",level:3},{value:"Conflict resolution rules",id:"conflict-resolution-rules",level:3},{value:"Merge semantics per field",id:"merge-semantics-per-field",level:3},{value:"Example: per-user threshold over a shared profile",id:"example-per-user-threshold-over-
1a-shared-profile",level:3},{value:"Interaction with symcache dependencies",id:"interaction-with-symcache-dependencies",level:3},{value:"External Map for Dynamic Settings",id:"external-map-for-dynamic-settings",level:2},{value:"How It Works",id:"how-it-works",level:3},{value:"Configuration Structure",id:"configuration-structure",level:3},{value:"Map Types",id:"map-types",level:3},{value:"HTTP Backend",id:"http-backend",level:4},{value:"CDB Backend",id:"cdb-backend",level:4},{value:"Selector Format",id:"selector-format",level:3},{value:"Response Format",id:"response-format",level:3},{value:"Complete Example",id:"complete-example",level:3},{value:"Error Handling",id:"error-handling",level:3},{value:"Performance Considerations",id:"performance-considerations",level:3},{value:"Example HTTP Server",id:"example-http-server",level:2},{value:"Server Implementation",id:"server-implementation",level:3},{value:"Database Schema",id:"database-schema",level:3},{value:"Rspamd Configuration",id:"rspamd-configuration",level:3},{value:"Testing the Server",id:"testing-the-server",level:3},{value:"Production Considerations",id:"production-considerations",level:3}];function c(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",img:"img",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,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"rspamd-user-settings",children:"Rspamd user settings"})}),"\n",(0,i.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,i.jsx)(n.p,{children:"Rspamd offers the flexibility to apply various settings for scanned messages. Each setting can define a specific set of custom metric weights, symbol scores, actions scores, and the ability to enable or disable certain checks. These settings can be loaded as dynamic maps, allowing them to be updated automatically whenever the corresponding file or URL has changed since the last update."}),"\n",(0,i.jsx)(n.p,{children:"To load settings as a dynamic map, you can set the 'settings' to a map string as follows:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'settings = "http://host/url"\n'})}),"\n",(0,i.jsx)(n.p,{children:"If you prefer not to use dynamic updates, you can define settings as an object using the following format:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:"settings {\n\tsetting1 = {\n\t...\n\t}\n\tsetting2 = {\n\t...\n\t}\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["To define static settings, you can edit the ",(0,i.jsx)(n.code,{children:"local.d/settings.conf"})," file (from Rspamd 1.8 onwards). On the other hand, if you want to use a dynamic map for settings, it's recommended to define it in the override file ",(0,i.jsx)(n.code,{children:"rspamd.conf.override"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'settings = "http://host/url"\n'})}),"\n",(0,i.jsx)(n.p,{children:"Alternatively, the settings apply part (see later) could be passed to Rspamd by a client through a query parameter:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'POST /scanv2?settings="{symbol1 = 10.0}" HTTP/1.0\n'})}),"\n",(0,i.jsx)(n.p,{children:"or HTTP header"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"POST /scanv2 HTTP/1.0\nSettings: {symbol1 = 10.0}\n"})}),"\n",(0,i.jsx)(n.p,{children:"Settings can also be indexed by ID, enabling the selection of a specific setting without the need to check its conditions. This feature can be used to split inbound and outbound mail flows by specifying different rulesets from the MTA side. Another use case for the settings ID option is to create dedicated lightweight checks for certain conditions, such as DKIM checks."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Important note"}),": Using settings ID is optimal in terms of performance."]}),"\n",(0,i.jsxs)(n.p,{children:["Let's assume we have the following settings in the configuration with an ID of ",(0,i.jsx)(n.code,{children:"dkim"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'# local.d/settings.conf\ndkim {\n\tid = "dkim";\n\tapply {\n\t\tgroups_enabled = ["dkim"];\n\t}\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Afterwards, if we send a request with this settings ID using the HTTP protocol:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"POST /scanv2 HTTP/1.0\nSettings-ID: dkim\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Then Rspamd will only check the DKIM rules and skip the other rules. Alternatively, you could test this setup using the ",(0,i.jsx)(n.code,{children:"rspamc"})," command:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'rspamc --header="settings-id=dkim" message.eml\n'})}),"\n",(0,i.jsx)(n.admonition,{title:"Available from Rspamd 4.0.1",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"Settings-ID"})," and ",(0,i.jsx)(n.code,{children:"Settings:"})," can now be used ",(0,i.jsx)(n.strong,{children:"together"})," in the same request. Rspamd merges them according to the ",(0,i.jsx)(n.a,{href:"#layered-settings-merge-401",children:"layered settings merge"})," rules, with the inline ",(0,i.jsx)(n.code,{children:"Settings:"})," header taking precedence over the named rule. This allows an MTA to select a named profile via ",(0,i.jsx)(n.code,{children:"Settings-ID"})," and then fine-tune individual values via ",(0,i.jsx)(n.code,{children:"Settings:"})," without duplicating the entire profile inline."]})}),"\n",(0,i.jsx)(n.h2,{id:"settings-structure",children:"Settings structure"}),"\n",(0,i.jsx)(n.p,{children:'The settings file should contain a single section called "settings":'}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'# local.d/settings.conf\nsome_users {\n\tid = "some_users";\n\tpriority = high;\n\tfrom = "@example.com";\n\trcpt = "admin";\n\trcpt = "/user.*/";\n\tip = "172.16.0.0/16";\n\tuser = "@example.net";
1\n\trequest_header = {\n\t\t"MTA-Tag" = "\\.example\\.net$";\n\t}\n\tapply {\n\t\tsymbol1 = 10.0;\n\t\tsymbol2 = 0.0;\n\t\tactions {\n\t\t\treject = 100.0;\n\t\t\tgreylist = null; # Disable greylisting (from 1.8.1)\n\t\t\t"add header" = 5.0; # Please note the space, NOT an underscore\n\t\t}\n\t}\n\t# Always add these symbols when settings rule has matched\n\tsymbols [\n\t\t"symbol2", "symbol4"\n\t]\n}\nwhitelist {\n\tpriority = low;\n\trcpt = "[email protected]";\n\twant_spam = yes;\n}\n# Disable some checks for authenticated users\nauthenticated {\n\tpriority = high;\n\tauthenticated = yes;\n\tapply {\n\t\tgroups_disabled = ["rbl", "spf"];\n\t}\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"So each setting has the following attributes:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"name"})," - section name that identifies this specific setting (e.g. ",(0,i.jsx)(n.code,{children:"some_users"}),")"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"priority"})," - ",(0,i.jsx)(n.code,{children:"high"})," (3), ",(0,i.jsx)(n.code,{children:"medium"})," (2), ",(0,i.jsx)(n.code,{children:"low"})," (1) or any positive integer value (default priority is ",(0,i.jsx)(n.code,{children:"low"}),"). Rules with greater priorities are matched first. Starting from version 1.4, Rspamd checks rules with equal priorities in ",(0,i.jsx)(n.strong,{children:"alphabetical"})," order. Within a single settings source, once a rule matches, only that rule is applied from that source and the rest are ignored. Settings arriving from multiple independent sources (config rules, Redis, HTTP header) are merged \u2014 see ",(0,i.jsx)(n.a,{href:"#layered-settings-merge-401",children:"Layered settings merge"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"match list"})," - list of rules which this rule matches:\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"from"})," - match SMTP sender"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"from_mime"})," - match MIME sender"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"rcpt"})," - match SMTP recipient"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"rcpt_mime"})," - match MIME recipient"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"ip"})," - match source IP address (CIDR notation supported; also accepts ",(0,i.jsx)(n.code,{children:"map:"})," URIs)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"ip_map"})," - match source IP address using a radix map (accepts a map definition instead of inline values)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"client_ip"})," - match the SMTP client IP as reported by the MTA (via ",(0,i.jsx)(n.code,{children:"task:get_client_ip()"}),"; distinct from the sending IP matched by ",(0,i.jsx)(n.code,{children:"ip"}),")"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"client_ip_map"})," - match the SMTP client IP using a radix map (accepts a map definition instead of inline values)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"hostname"})," - match the source hostname (regexp supported)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"user"})," - matches authenticated user ID of message sender if any"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"authenticated"})," - matches any authenticated user"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"local"})," - matches any local IP"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"request_header"})," - collection of request header names and regexes to match them against (condition is satisfied if any match)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"header"})," - collection of MIME message header names and regexes to match them against (condition is satisfied if any match), available since Rspamd 1.7"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"selector"})," - apply the specific selector to check if we need to apply these settings. If selector returns a non-nil/truthy value, then the settings are applied (the value itself is not used beyond truthiness). Available since Rspamd 1.8."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"apply"})," - list of applied rules\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"symbol"})," - modify weight of a symbol"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"actions"})," - defines actions"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"symbols_enabled"})," - array of symbols that should be checked (all other rules are disabled)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"groups_enabled"})," - array of rules groups that should be checked (all other rules are disabled)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"symbols_disabled"})," - array of disabled checks by symbol name (all other rules are enabled)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"groups_disabled"})," - array of disabled checks by group name (all other rules are enabled)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"subject"})," - set subject based on the new pattern: ",(0,i.jsx)(n.code,{children:"%s"})," is replaced with the existing subject, ",(0,i.jsx)(n.code,{children:"%d"})," is replaced with the message's spam score (e.g. ",(0,i.jsx)(n.code,{children:'subject = "SPAM: %s (%d)"'}),")"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"add_headers"})," - map of headers to add to the message (passed to the MTA via the milter interface)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"remove_headers"})," - map of headers to remove from the message (passed to the MTA via the milter interface)"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"symbols"})," - add symbols from the list if a rule has matched"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"inverse"})," - inverse match (e.g. it will NOT match when all elements are matched and vice-versa)"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["If ",(0,i.jsx)(n.code,{children:"symbols_enabled"})," or ",(0,i.jsx)(n.code,{children:"groups_enabled"})," are found in ",(0,i.jsx)(n.code,{children:"apply"})," element, then Rspamd disables all checks with the exception of the enabled ones. When ",(0,i.jsx)(n.code,{children:"enabled"})," and ",(0,i.jsx)(n.code,{children:"disabled"})," options are both presented, then the precedence of operations is the following:"]}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsx)(n.li,{children:"Disable all symbols"}),"\n",(0,i.jsxs)(n.li,{children:["Enable symbols from ",(0,i.jsx)(n.code,{children:"symbols_enabled"})," and ",(0,i.jsx)(n.code,{children:"groups_enabled"})]}),"\n",(0,i.jsxs)(n.li,{children:["Disable symbols from ",(0,i.jsx)(n.code,{children:"symbols_disabled"})," and ",(0,i.jsx)(n.code,{children:"groups_disabled"})]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Certain rules, like ",(0,i.jsx)(n.code,{children:"metadata exporter"}),", ",(0,i.jsx)(n.code,{children:"history redis"}),", or ",(0,i.jsx)(n.code,{children:"clickhouse"}),", are labeled as ",(0,i.jsx)(n.code,{children:"explicit_disable"}),". This means that even if you enable specific symbols in ",(0,i.jsx)(n.code,{children:"symbols_enabled"}),", these rules will still be executed. This behavior is intentional as enabling specific checks should not interfere with data exporting or history logging."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Important notice"}),": This is ",(0,i.jsx)(n.strong,{children:"NOT"})," applicable to ",(0,i.jsx)(n.code,{children:"want_spam"})," option. This option disable ",(0,i.jsx)(n.strong,{children:"ALL"})," Rspamd rules, even history or data exporting. Actually, it is a full bypass of all Rspamd processing."]}),"\n",(0,i.jsx)(n.h3,{id:"settings-match",children:"Settings match"}),"\n",(0,i.jsxs)(n.p,{children:["The match section performs ",(0,i.jsx)(n.code,{children:"AND"})," operation on different matches: for example, if you have ",(0,i.jsx)(n.code,{children:"from"})," and ",(0,i.jsx)(n.code,{children:"rcpt"})," in the same rule, then the rule matches only when ",(0,i.jsx)(n.code,{children:"from"})," ",(0,i.jsx)(n.code,{children:"AND"})," ",(0,i.jsx)(n.code,{children:"rcpt"})," match. For similar matches, the ",(0,i.jsx)(n.code,{children:"OR"})," rule applies: if you have multiple ",(0,i.jsx)(n.code,{children:"rcpt"})," matches, then ",(0,i.jsx)(n.em,{children:"any"})," of these will trigger the rule. If a rule is triggered then no more rules are matched."]}),"\n",(0,i.jsxs)(n.p,{children:["By default, regular expressions are case-sensitive. This can be changed with the ",(0,i.jsx)(n.code,{children:"i"})," flag.\nRegexp rules can be slow and should not be used extensively."]}),"\n",(0,i.jsx)(n.p,{children:"In order to make matching case-insensitive, string comparisons convert input strings to lowercase. Thus, strings in the match lists should always be in lowercase."}),"\n",(0,i.jsx)(n.p,{children:"The picture below describes the architecture of settings matching."}),"\n",(0,i.jsx)(n.img,{className:"img-fluid",width:"50%",src:"/img/settings.png"}),"\n",(0,i.jsx)(n.h3,{id:"redis-settings",children:"Redis settings"}),"\n",(0,i.jsx)(n.p,{children:"Storing settings in Redis offers a highly flexible way to apply settings and eliminates the need to reload a map."}),"\n",(0,i.jsxs)(n.p,{children:["To utilize settings in Redis, we create one or more handlers in Lua, each of which may return a key. If a key is returned and exists in Redis, its value is used as the settings. The value of the key should be formatted similarly to the contents of the ",(0,i.jsx)(n.code,{children:"apply"})," block or settings posted in headers."]}),"\n",(0,i.jsx)(n.p,{children:"Let's consider a scenario where we want to base our settings on the domain of the first SMTP recipient."}),"\n",(0,i.jsx)(n.p,{children:"We can set our keys as follows:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'127.0.0.1:6379> SET "setting:example.com" "{symbol1 = 5000;}"\nOK\n'})}),"\n",(0,i.jsx)(n.p,{children:'Where "setting:" is a prefix we have chosen for our settings and "example.com" is the recipient domain we want to apply settings to and the value of the key contains our desired settings.'}),"\n",(0,i.jsxs)(n.p,{children:["We would then define configuration as follows in ",(0,i.jsx)(n.code,{children:"/etc/rspamd/rspamd.conf.override"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:"# Redis settings are configured in a \"settings_redis\" block\nsettings_redis {\n  # Here we will define our Lua functions\n  handlers = {\n    # Everything in here is a Lua function with an arbitrary name\nmy_check_rcpt_domain = <<EOD\nreturn function(task)\n  local rcpt = task:get_recipients('smtp')\n  -- Return nothing if we can't find domain of first SMTP recipient\n  if not (rcpt and rcpt[1] and rcpt[1]['domain']) then return end\n  -- Return \"setting:\" concatenated with the domain\n  local key = 'setting:' .. rcpt[1]['domain']\n  return key\n  -- From Rspamd 1.6.3 this function can return a list of keys to check.\n  -- Use this if you need to check for settings according to priority:\n  return {key, 'setting:global'}\nend\nEOD;\n  }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Redis servers are configured as per usual - see ",(0,i.jsx)(n.a,{href:"/configuration/redis",children:"here"})," for details."]}),"\n",(0,i.jsx)(n.h2,{id:"layered-settings-merge-401",children:"Layered settings merge (4.0.1+)"}
1),"\n",(0,i.jsxs)(n.p,{children:["Prior to Rspamd 4.0.1, settings from different sources were mutually exclusive: the first source that produced a result won and all others were discarded. Starting from 4.0.1, settings collected from ",(0,i.jsx)(n.strong,{children:"different sources"})," are merged in a fixed priority hierarchy before being applied to the task."]}),"\n",(0,i.jsx)(n.h3,{id:"layer-hierarchy",children:"Layer hierarchy"}),"\n",(0,i.jsx)(n.p,{children:"Each settings source is assigned a numbered layer. Layers are merged in ascending order so that higher-numbered layers take precedence:"}),"\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\n\n\n\n\n\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Layer"}),(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Source"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"0"}),(0,i.jsx)(n.td,{children:"CONFIG"}),(0,i.jsx)(n.td,{children:"Baseline applied unconditionally from configuration"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"1"}),(0,i.jsx)(n.td,{children:"PROFILE"}),(0,i.jsx)(n.td,{children:"Settings profiles (reserved for future use)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"2"}),(0,i.jsx)(n.td,{children:"RULE"}),(0,i.jsxs)(n.td,{children:["The matched rule from ",(0,i.jsx)(n.code,{children:"local.d/settings.conf"})," (or a dynamic map)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"3"}),(0,i.jsx)(n.td,{children:"PER_USER"}),(0,i.jsxs)(n.td,{children:["Redis settings returned by ",(0,i.jsx)(n.code,{children:"settings_redis"})," handlers"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"4"}),(0,i.jsx)(n.td,{children:"HTTP"}),(0,i.jsxs)(n.td,{children:["Inline settings passed via ",(0,i.jsx)(n.code,{children:"Settings:"})," HTTP header"]})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"Within a single source (e.g. the config file), the existing first-match-wins rule still applies \u2014 only one rule is collected per source. Once collected, layers are merged in the order above."}),"\n",(0,i.jsx)(n.h3,{id:"how-layers-are-applied",children:"How layers are applied"}),"\n",(0,i.jsxs)(n.p,{children:["Rspamd runs a dedicated ",(0,i.jsx)(n.code,{children:"SETTINGS_APPLY"})," prefilter that fires after all settings collectors have completed (including async Redis lookups). If only one layer matched, it is applied immediately as before. If multiple layers are present, they are merged and the result is applied atomically."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Typical multi-layer scenarios:"})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Settings-ID + Settings header"}),": ",(0,i.jsx)(n.code,{children:"Settings-ID: dkim"})," selects a named config rule (RULE layer); ",(0,i.jsx)(n.code,{children:"Settings: {actions {reject = 999.0;}}"})," adds an override (HTTP layer). Both are applied, with the HTTP layer overriding the reject threshold only."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Config rule + Redis"}),": A config rule disables certain symbol groups (RULE layer); a Redis handler returns per-user score overrides (PER_USER layer). Both take effect, with Redis values winning on any overlapping keys."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Config rule + HTTP"}),": An MTA selects a scanning profile via ",(0,i.jsx)(n.code,{children:"Settings-ID"})," (RULE layer) and then injects a request-specific setting via ",(0,i.jsx)(n.code,{children:"Settings:"})," (HTTP layer) without needing to reproduce the full profile inline."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"conflict-resolution-rules",children:"Conflict resolution rules"}),"\n",(0,i.jsx)(n.p,{children:"When the same key appears in multiple layers, these rules are applied in order:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Higher layer wins"}),": HTTP (4) beats PER_USER (3) beats RULE (2) beats CONFIG (0)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Per-symbol specificity beats per-group"}),": if one layer disables a group and another layer explicitly enables a specific symbol within that group, the symbol remains enabled regardless of layer order."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"At equal specificity and equal layer, disable wins"})," (safety-first)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Explicit enable beats implicit omission"}),": a symbol not mentioned by a layer is not treated as disabled by it."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"merge-semantics-per-field",children:"Merge semantics per field"}),"\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\n\n\n\n\n\n\n\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Merge behavior"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"actions"}),", ",(0,i.jsx)(n.code,{children:"scores"}),", ",(0,i.jsx)(n.code,{children:"symbols"})," (score overrides), ",(0,i.jsx)(n.code,{children:"variables"})]}),(0,i.jsx)(n.td,{children:"Per-key: higher-layer value replaces lower-layer value for that key only"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"symbols_enabled"})," / ",(0,i.jsx)(n.code,{children:"symbols_disabled"})]}),(0,i.jsx)(n.td,{children:"Union across layers; conflict resolved by specificity then layer priority"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"groups_enabled"})," / ",(0,i.jsx)(n.code,{children:"groups_disabled"})]}),(0,i.jsx)(n.td,{children:"Union across layers;
1 per-symbol entries override per-group entries"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"subject"})}),(0,i.jsx)(n.td,{children:"Scalar: highest-layer value wins"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"whitelist"})," / ",(0,i.jsx)(n.code,{children:"want_spam"})]}),(0,i.jsxs)(n.td,{children:["Boolean OR: any layer setting this to ",(0,i.jsx)(n.code,{children:"true"})," activates it for the task"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"add_headers"})," / ",(0,i.jsx)(n.code,{children:"remove_headers"})]}),(0,i.jsx)(n.td,{children:"Union: entries from all layers are collected"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"messages"})}),(0,i.jsx)(n.td,{children:"Per-key: higher-layer value wins"})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"example-per-user-threshold-over-a-shared-profile",children:"Example: per-user threshold over a shared profile"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'# local.d/settings.conf\n\n# Shared outbound profile: disable reputation checks, lower thresholds\noutbound {\n  id = "outbound";\n  priority = high;\n  request_header { "X-Flow" = "^outbound$"; }\n  apply {\n    groups_disabled = ["rbl", "surbl", "bayes"];\n    actions {\n      reject    = 20.0;\n      "add header" = 8.0;\n    }\n  }\n}\n'})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:"# Redis: per-user override for a known VIP sender\n# Key \"rspamd:settings:[email protected]\" \u2192  {actions {reject = 999.0;}}\nsettings_redis {\n  handlers {\n    per_user_settings = <<EOD\nreturn function(task)\n  local from = task:get_from('smtp')\n  if from and from[1] then\n    return 'rspamd:settings:' .. from[1]['addr']\n  end\nend\nEOD;\n  }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["When Rspamd processes a message from ",(0,i.jsx)(n.code,{children:"[email protected]"})," with ",(0,i.jsx)(n.code,{children:"X-Flow: outbound"}),":"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"RULE layer"})," (from ",(0,i.jsx)(n.code,{children:"outbound"})," rule): disables RBL/SURBL/Bayes, sets ",(0,i.jsx)(n.code,{children:"reject = 20.0"})," and ",(0,i.jsx)(n.code,{children:"add header = 8.0"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"PER_USER layer"})," (from Redis): sets ",(0,i.jsx)(n.code,{children:"reject = 999.0"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Merged result: RBL/SURBL/Bayes disabled, ",(0,i.jsx)(n.code,{children:"reject = 999.0"})," (Redis wins), ",(0,i.jsx)(n.code,{children:"add header = 8.0"})," (only in RULE layer, kept as-is)."]}),"\n",(0,i.jsx)(n.h3,{id:"interaction-with-symcache-dependencies",children:"Interaction with symcache dependencies"}),"\n",(0,i.jsxs)(n.p,{children:["The layered merge also introduces a distinction between ",(0,i.jsx)(n.strong,{children:"hard"})," and ",(0,i.jsx)(n.strong,{children:"weak"})," symbol dependencies:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Hard dependency"}),": if the dependency symbol is disabled by settings, the dependent symbol is also disabled (cascade). Used when a symbol's result is meaningless without its dependency (e.g. ",(0,i.jsx)(n.code,{children:"ARC_SIGNED"})," \u2192 ",(0,i.jsx)(n.code,{children:"ARC_CHECK"}),")."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Weak dependency"})," (default): if the dependency is disabled, the dependent symbol may still run. Used when the dependency is optional context (e.g. ",(0,i.jsx)(n.code,{children:"DMARC_CHECK"})," \u2192 ",(0,i.jsx)(n.code,{children:"SPF_CHECK"}),")."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["This distinction matters when settings ",(0,i.jsx)(n.code,{children:"symbols_disabled"})," or ",(0,i.jsx)(n.code,{children:"groups_disabled"})," disable part of a dependency chain \u2014 hard deps cascade the disable while weak deps let the dependent proceed independently."]}),"\n",(0,i.jsx)(n.h2,{id:"external-map-for-dynamic-settings",children:"Exter
1nal Map for Dynamic Settings"}),"\n",(0,i.jsxs)(n.p,{children:["Rspamd's settings system can retrieve dynamic configuration from external sources using the external map feature. When a settings rule includes an ",(0,i.jsx)(n.code,{children:"external_map"})," block, Rspamd will query an external data source for settings that will be applied to the task."]}),"\n",(0,i.jsx)(n.h3,{id:"how-it-works",children:"How It Works"}),"\n",(0,i.jsx)(n.p,{children:"The external_map feature works as follows:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Selector Evaluation"}),": When a settings rule matches, the selector is evaluated with the current task to produce key-value pairs."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Map Query"}),": The external map is queried using these key-value pairs as the request data."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Response Processing"}),": If the map returns data, it is parsed as UCL and applied as settings to the task."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"configuration-structure",children:"Configuration Structure"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"external_map"})," block requires two components:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"map"}),": Map definition specifying the external data source"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"selector"}),": A ",(0,i.jsx)(n.a,{href:"/configuration/selectors",children:"selector expression"})," that generates key-value pairs for the request"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Important"}),": The selector must return an even number of elements that form key-value pairs. Odd elements become keys, even elements become values. Use the ",(0,i.jsx)(n.code,{children:"id('key')"})," selector to specify constant keys."]}),"\n",(0,i.jsx)(n.h3,{id:"map-types",children:"Map Types"}),"\n",(0,i.jsx)(n.p,{children:"External maps support several backends:"}),"\n",(0,i.jsx)(n.h4,{id:"http-backend",children:"HTTP Backend"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'external_map = {\n  map = {\n    external = true;\n    backend = "http://settings.example.com/api";\n    method = "body";        # "body", "header", or "query" \n    encode = "json";        # "json" or "messagepack"\n    timeout = 5.0;          # optional timeout in seconds\n  }\n  selector = "id(\'user\');user;id(\'from\');from(\'smtp\'):addr";\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"For HTTP maps:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:'method = "body"'}),": Key-value pairs are sent in request body as POST request (encoded as specified)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:'method = "header"'}),": Key-value pairs are sent as HTTP headers"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:'method = "query"'}),": Key-value pairs are sent as query parameters"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"cdb-backend",children:"CDB Backend"}),"\n",(0,i.jsx)(n.p,{children:"CDB is a constant key-value database that can be used to store settings locally"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:"external_map = {\n  map = {\n    external = true;\n    cdb = \"/path/to/settings.cdb\";\n  }\n  selector = \"id('user');user;id('domain');from('smtp'):domain\";\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"selector-format",children:"Selector Format"}),"\n",(0,i.jsx)(n.p,{children:"The selector must return key-value pairs using the following format:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.code,{children:"id('key_name')"})," to specify literal keys"]}),"\n",(0,i.jsxs)(n.li,{children:["Separate key-value pairs with semicolons (",(0,i.jsx)(n.code,{children:";"}),")"]}),"\n",(0,i.jsxs)(n.li,{children:["Example: ",(0,i.jsx)(n.code,{children:"id('user');user;id('from');from('smtp'):addr"})]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"This selector would create the following data structure:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "user": "authenticated_username",\n  "from": "[email protected]"\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"response-format",children:"Response Format"}),"\n",(0,i.jsx)(n.p,{children:"The external map must return valid UCL data representing settings to apply:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "symbols": {\n    "CUSTOM_SYMBOL": 10.0\n  },\n  "actions": {\n    "reject": 15.0\n  },\n  "subject": "SPAM: %s"\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"complete-example",children:"Complete Example"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'# local.d/settings.conf\ndynamic_user_settings {\n  id = "dynamic_user_settings";\n  priority = medium;\n  authenticated = yes;\n  \n  external_map = {\n    map = {\n      external = true;
1\n      backend = "http://settings-api.example.com/user-settings";\n      method = "body";\n      encode = "json";\n      timeout = 2.0;\n    }\n    # Creates key-value pairs: {user: "username", ip: "client_ip"}\n    selector = "id(\'user\');user;id(\'ip\');ip";\n  }\n}\n\n# Settings based on sender domain and recipient count\ndomain_specific_settings {\n  priority = low;\n  \n  external_map = {\n    map = {\n      external = true;\n      cdb = "/etc/rspamd/domain_settings.cdb";\n    }\n    # Creates key-value pairs: {domain: "example.com", rcpt_count: "2"}\n    selector = "id(\'domain\');from(\'smtp\'):domain;id(\'rcpt_count\');rcpts:addr.len";\n  }\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"error-handling",children:"Error Handling"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"If the selector returns nil or an odd number of elements, the external map query is skipped"}),"\n",(0,i.jsx)(n.li,{children:"If the external map returns an error or no data, the request is logged and processing continues"}),"\n",(0,i.jsx)(n.li,{children:"If the response cannot be parsed as UCL, an error is logged and no settings are applied"}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"performance-considerations",children:"Performance Considerations"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"External map queries are asynchronous and won't block message processing"}),"\n",(0,i.jsx)(n.li,{children:"Use appropriate timeouts to prevent slow external services from affecting performance"}),"\n",(0,i.jsx)(n.li,{children:"Consider caching on the external service side for frequently queried keys"}),"\n",(0,i.jsx)(n.li,{children:"CDB maps offer better performance than HTTP for static datasets"}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"example-http-server",children:"Example HTTP Server"}),"\n",(0,i.jsx)(n.p,{children:"Here's a complete example of a Python HTTP server that can handle external map requests from Rspamd:"}),"\n",(0,i.jsx)(n.h3,{id:"server-implementation",children:"Server Implementation"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:"#!/usr/bin/env python3\n\"\"\"\nExample HTTP server for Rspamd external settings maps\nSupports different HTTP methods and database backend\n\"\"\"\n\nfrom flask import Flask, request, jsonify\nimport sqlite3\nimport json\nimport msgpack\nimport logging\n\napp = Flask(__name__)\nlogging.basicConfig(level=logging.INFO)\n\n# Database setup\ndef init_db():\n    conn = sqlite3.connect('settings.db')\n    cursor = conn.cursor()\n    cursor.execute('''\n        CREATE TABLE IF NOT EXISTS user_settings (\n            user TEXT,\n            domain TEXT,\n            settings TEXT NOT NULL,\n            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n            PRIMARY KEY (user, domain)\n        )\n    ''')\n    \n    # Insert some example data\n    examples = [\n        ('[email protected]', 'example.com', {\n            'symbols': {'CUSTOM_USER_SYMBOL': 5.0},\n            'actions': {'reject': 20.0}\n        }),\n        ('[email protected]', 'example.com', {\n            'symbols': {'ADMIN_SYMBOL': -10.0},\n            'actions': {'reject': 50.0}\n        }),\n        (None, 'spam.domain.com', {\n            'symbols': {'SPAM_DOMAIN': 15.0},\n            'actions': {'reject': 5.0}\n        })\n    ]\n    \n    for user, domain, settings in examples:\n        cursor.execute(\n            'INSERT OR REPLACE INTO user_settings (user, domain, settings) VALUES (?, ?, ?)',\n            (user, domain, json.dumps(settings))\n        )\n    \n    conn.commit()\n    conn.close()\n\ndef get_settings(data):\n    \"\"\"Retrieve settings from database based on key-value data\"\"\"\n    conn = sqlite3.connect('settings.db')\n    cursor = conn.cursor()\n    \n    # Build query based on available data\n    conditions = []\n    params = []\n    \n    if 'user' in data:\n        conditions.append('user = ?')\n        params.append(data['user'])\n    \n    if 'domain' in data:\n        conditions.append('domain = ?')\n        params.append(data['domain'])\n    \n    if not conditions:\n        return None\n    \n    query = 'SELECT settings FROM user_settings WHERE ' + ' AND '.join(conditions)\n    cursor.execute(query, params)\n    result = cursor.fetchone()\n    conn.close()\n    \n    if result:\n        return json.loads(result[0])\n    return None\n\[email protected]('/settings', methods=['POST'])\ndef handle_settings():\n    \"\"\"Handle settings requests from Rspamd\"\"\"\n    data = None\n    \n    # Parse request data based on content type\n    content_type = request.headers.get('Content-Type', '')\n    \n    if 'application/json' in content_type:\n        data = request.get_json()\n    elif 'application/x-msgpack' in content_type:\n        try:\n            data = msgpack.unpackb(request.data)\n        except:\n            app.logger.error(\"Failed to decode msgpack data\")\n            return jsonify({'error': 'Invalid msgpack data'}), 400\n    else:\n        # Try to parse as form data for query method\n        data = dict(request.form) or dict(request.args)\n    \n    if not data:\n        app.logger.warning(\"No data provided in request\")\n        return jsonify({'error': 'No data provided'}), 400\n    \n    app.logger.info(f\"Looking up settings for data: {data}\")\n    \n    # Get settings from database\n    settings = get_settings(data)\n    \n    if settings:\n        app.logger.info(f\"Found settings for data: {data}\")\n        return jsonify(settings)\n    else:\n        app.logger.info(f\"No settings found for data: {data}\")\n        return jsonify({'error': 'Settings not found'}), 404\n\[email protected]('/health', methods=['GET'])\ndef health_check():\n    \"\"\"Health check endpoint\"\"\"\n    return jsonify({'status': 'healthy'})\n\nif __name__ == '__main__':\n    init_db()\n    app.run(host='0.0.0.0', port=8080, debug=True)\n"})}),"\n",(0,i.jsx)(n.h3,{id:"database-schema",children:"Database Schema"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sql",children:'-- settings.db schema\nCREATE TABLE user_settings (\n    user TEXT,\n    domain TEXT,\n    settings TEXT NOT NULL,  -- JSON string of settings\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    PRIMARY KEY (user, domain)\n);\n\n-- Example data\nINSERT INTO user_settings (user, domain, settings) VALUES \n(\'[email protected]\', \'example.com\', \'{"symbols": {"CUSTOM_USER": 5.0}, "actions": {"reject": 20.0}}\'),\n(\'[email protected]\', \'example.com\', \'{"symbols": {"ADMIN_SYMBOL": -10.0}, "actions": {"reject": 50.0}}\'),\n(NULL, \'spam.domain.com\', \'{"symbols": {"SPAM_DOMAIN": 15.0}, "actions": {"reject": 5.0}}\');\n'})}),"\n",(0,i.jsx)(n.h3,{id:"rspamd-configuration",children:"Rspamd Configuration"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-hcl",children:'# local.d/settings.conf\nuser_based_settings {\n  priority = high;\n  authenticated = yes;\n  \n  external_map = {\n    map = {\n      external = true;\n      backend = "http://localhost:8080/settings";\n      method = "body";\n      encode = "json";\n      timeout = 2.0;\n    }\n    selector = "id(\'user\');user;id(\'domain\');from(\'smtp\'):domain";\n  }\n}\n\ndomain_based_settings {\n  priority = medium;\n  \n  external_map = {\n    map = {\n      external = true;\n      backend = "http://localhost:8080/settings";\n      method = "body";\n      encode = "json";\n      timeout = 2.0;\n    }\n    selector = "id(\'domain\');from(\'smtp\'):domain";\n  }\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"testing-the-server",children:"Testing the Server"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'# Install dependencies\npip install flask msgpack\n\n# Ru
1n the server\npython3 settings_server.py\n\n# Test with curl\ncurl -X POST http://localhost:8080/settings \\\n  -H "Content-Type: application/json" \\\n  -d \'{"user": "[email protected]", "domain": "example.com"}\'\n\n# Test domain-only lookup\ncurl -X POST http://localhost:8080/settings \\\n  -H "Content-Type: application/json" \\\n  -d \'{"domain": "spam.domain.com"}\'\n'})}),"\n",(0,i.jsx)(n.h3,{id:"production-considerations",children:"Production Considerations"}),"\n",(0,i.jsx)(n.p,{children:"For production use, consider:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Authentication"}),": Add API key authentication"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Rate limiting"}),": Implement request rate limiting"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Caching"}),": Add Redis or memory caching for frequently accessed settings"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Database"}),": Use PostgreSQL or MySQL for better performance"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Monitoring"}),": Add metrics and logging"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"SSL/TLS"}),": Use HTTPS for secure communication"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:"# Example with Redis caching\nimport redis\nimport json\n\nredis_client = redis.Redis(host='localhost', port=6379, db=0)\n\ndef get_settings_cached(data):\n    cache_key = f\"settings:{json.dumps(data, sort_keys=True)}\"\n    \n    # Try cache first\n    cached = redis_client.get(cache_key)\n    if cached:\n        return json.loads(cached)\n    \n    # Get from database\n    settings = get_settings(data)\n    if settings:\n        # Cache for 5 minutes\n        redis_client.setex(cache_key, 300, json.dumps(settings))\n    \n    return settings\n"})}),"\n",(0,i.jsx)(n.p,{children:"This example provides a complete working HTTP server that can handle the key-value pairs sent by Rspamd's external map feature and can be easily extended for production use."})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...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.