Skip to main content

Rule Writing Workshop

This workshop teaches you to create effective custom rules for Rspamd through progressive learning. We start with simple concepts and build toward complex, production-ready rules that solve real-world spam filtering challenges.

Learning Path Overview

See the Tool Selection Guide for when to use each tool: /guides/configuration/tool-selection

Prerequisites

Level 1: Understanding Symbols

Symbol anatomy

rspamd_config:register_symbol({
name = 'MY_CUSTOM_RULE',
score = 2.5,
description = 'Detects something bad',
callback = my_function,
type = 'normal'
})

Your first symbol

-- /etc/rspamd/local.d/custom_rules.lua
local function always_fires(task)
return true
end

rspamd_config:register_symbol({
name = 'WORKSHOP_FIRST_RULE',
score = 0.1,
description = 'My first custom rule - always fires',
callback = always_fires
})

Level 2: Simple Pattern Matching

local function check_sender_domain(task)
local from = task:get_from('mime')
if from and from[1] and from[1]['domain'] == 'suspicious-domain.com' then
return true
end
return false
end

rspamd_config:register_symbol({
name = 'SUSPICIOUS_SENDER_DOMAIN',
score = 5.0,
description = 'Message from suspicious domain',
callback = check_sender_domain
})

Level 3: Multimap Rules

BAD_DOMAINS {
type = "from";
map = "/etc/rspamd/bad_domains.map";
score = 8.0;
}

Level 4: Regular Expression Rules

local function check_credit_cards(task)
local text_parts = task:get_text_parts()
if text_parts then
for _, part in ipairs(text_parts) do
local content = tostring(part:get_content())
local cc_pattern = '%d%d%d%d[%s%-]?%d%d%d%d[%s%-]?%d%d%d%d[%s%-]?%d%d%d%d'
if string.match(content, cc_pattern) then
return true, 1.0
end
end
end
return false
end

Level 4.1: Conditional Regular Expressions

Use re_conditions to validate complex patterns efficiently and avoid false positives. This lets you keep regexps fast while confirming matches with Lua.

-- Example inspired by rules/bitcoin.lua
config.regexp['BITCOIN_ADDR'] = {
re = string.format('(%s) + (%s) > 0', normal_wallet_re, btc_bleach_re),
expression_flags = { 'noopt' },
re_conditions = {
[normal_wallet_re] = function(task, txt, s, e)
local word = lua_util.str_trim(txt:sub(s + 1, e))
local valid = is_traditional_btc_address(word)
if valid then
task:insert_result('BITCOIN_ADDR', 1.0, word)
return true
end
return false
end,
[btc_bleach_re] = function(task, txt, s, e)
local word = tostring(lua_util.str_trim(txt:sub(s + 1, e)))
local valid = is_segwit_bech32_address(task, word)
if valid then
task:insert_result('BITCOIN_ADDR', 1.0, word)
return true
end
return false
end,
},
}

Tips:

  • Keep regexps bounded and strict
  • Use re_conditions to confirm structure/checksums

Level 5: Selectors

Selectors extract and transform data from a message and can be reused across modules (regexp, multimap, ratelimit, reputation, etc.). See the full reference: /configuration/selectors

Minimal examples:

# /etc/rspamd/local.d/multimap.conf
RISKY_SUBJECT_SELECTOR {
type = "selector";
selector = "header('Subject').lower";
map = "/etc/rspamd/maps/risky_subjects.list";
score = 3.0;
}

You can also feed selectors into regexp rules by registering them first:

rspamd_config:register_re_selector('user_subj', "user.lower;header('Subject').lower", ' ')

config['regexp']['USER_SUBJECT_MATCH'] = {
re = 'user_subj=/^admin .* urgent/{selector}',
score = 2.0,
one_shot = true,
}

Level 6: Basic Lua Scripting

local function explore_task_object(task)
local logger = require "rspamd_logger"
logger.infox(task, "Subject: %s", task:get_header('Subject') or 'N/A')
return false
end

rspamd_config:register_symbol({
name = 'TASK_EXPLORER',
score = 0.0,
description = 'Explores task object properties',
callback = explore_task_object
})

Level 7: Plugin Creation

Use a plugin when you need configurable, reusable multi-symbol logic.

-- /etc/rspamd/local.d/my_plugin.lua (deployed in the right place for your setup)
local lua_util = require "lua_util"

local M = 'my_plugin'

local function check_one(task)
if task:get_header('X-Flag') == 'on' then
task:insert_result('MY_PLUGIN_SYMBOL', 1.0)
end
end

rspamd_config:register_symbol({
name = 'MY_PLUGIN_SYMBOL',
callback = check_one,
score = 0.0,
group = 'policies',
})

return { name = M }

Configuration:

# /etc/rspamd/local.d/my_plugin.conf
enabled = true;
threshold = 5;

Level 8: Advanced Lua Functions

local function check_sender_reputation(task)
local from = task:get_from('mime')
if not from or not from[1] then return false end
local domain = from[1]['domain']
local function dns_callback(resolver, to_resolve, results, err)
if results and results[1] == '127.0.0.2' then
task:insert_result('SENDER_REPUTATION_BAD', 1.0)
end
end
task:get_resolver():resolve_txt({ task = task, name = domain .. '.reputation.example.com', callback = dns_callback })
return false
end

rspamd_config:register_symbol({ name = 'SENDER_REPUTATION_CHECK', type = 'prefilter', callback = check_sender_reputation, priority = 5 })
rspamd_config:register_symbol({ name = 'SENDER_REPUTATION_BAD', score = 5.0, parent = 'SENDER_REPUTATION_CHECK' })

Level 9: Performance Optimization

  • Early returns
  • Prefer simple string ops over regex when possible
  • Cache expensive operations
  • Process selectively

Level 10: Production Deployment

  • Test with rspamc
  • Monitor rule performance, log slow rules
  • Add error handling with pcall

Reference: Additional Technical Details

Configuration files

.include(try=true,priority=1) "$CONFDIR/local.d/config.conf"
.include(try=true,priority=1) "$CONFDIR/override.d/config.conf"

Rule weights

symbol "MY_SYMBOL" { description = "my cool rule"; score = 1.5; }

Symbols registration patterns

rspamd_config:register_symbol{ type = 'normal', name = 'MY_SYMBOL', callback = function(task) end, score = 1.0 }

Redis requests

local lua_redis = require "lua_redis"
local redis_params = lua_redis.parse_redis_server('module')

config vs rspamd_config

rspamd_config:get_all_opts('section')
rspamd_config.SYMBOL = {...}
config['regexp']['SYMBOL'] = { callback = function(task) ... end }

Rules check order

StageDescription
Connection filtersInitial stage right after connection established
Message processingText extraction, HTML parsing, language detection
Pre-filtersExecuted before normal filters
Normal filtersMain rules, dependency graph defines order
StatisticsChecked after normal symbols
Composites (pass 1)Combine results before post-filters
Post-filtersAfter normal filters and composites
Composites (pass 2)Combine results including postfilters
Idempotent filtersMust not change result