Skip to main content

Rspamd GPT Plugin

The Rspamd GPT Plugin, introduced in Rspamd 3.9, integrates OpenAI's GPT API to enhance spam filtering capabilities using advanced natural language processing techniques. Here are the basic ideas behind this plugin:

  • The selected displayed text part is extracted and submitted to the GPT API for spam probability assessment
  • Additional message details such as Subject, displayed From, and URLs are also included in the assessment
  • Then, we ask GPT to provide results in JSON format since human-readable GPT output cannot be parsed (in general)
  • Some specific symbols (BAYES_SPAM, FUZZY_DENIED, REPLY, etc.) are excluded from the GPT scan
  • Obvious spam and ham are also excluded from the GPT evaluation

The last two points reduce the GPT workload for something that is already known, where GPT cannot add any value in the evaluation. We also use GPT as one of the classifiers, meaning that we do not rely solely on GPT evaluation.

Configuration Options

By default, the GPT Plugin is disabled. To enable the plugin, add the following command in your Rspamd configuration:

gpt {
enabled = true; # Ensure this line is present to enable the GPT Plugin
}

The full list of the plugin configuration options:

gpt {
# Enable the plugin
enabled = true;

# LLM provider type: openai (remote) or ollama (local)
type = "openai";

# Your OpenAI API key (not required for ollama)
api_key = "xxx";

# Model name (string or a list for ensemble requests)
model = "gpt-5-mini";

# Per-model parameters — works for both openai and ollama.
# Each key is the model name; the value is merged into the request body.
model_parameters = {
"gpt-5-mini" = {
max_completion_tokens = 1000,
},
"gpt-5-nano" = {
max_completion_tokens = 1000,
},
"gpt-4o-mini" = {
max_tokens = 1000,
temperature = 0.0,
}
};

# Overall HTTP request timeout (seconds)
timeout = 10s;

# Fine-grained HTTP stage timeouts (all optional; unset = use timeout)
# connect_timeout = 3;
# ssl_timeout = 3;
# write_timeout = 5;
# read_timeout = 10;

# Optional server-side time budget passed as max_completion_time in the
# request body. Not supported by the standard OpenAI API; only enable when
# your endpoint/proxy explicitly supports it. The value sent is multiplied
# by 0.95 to account for connection overhead.
# request_timeout = 8;

# Prompt for the model (use default if not set)
prompt = "xxx";

# Custom condition (Lua function)
condition = "xxx";

# Autolearn if GPT classified
autolearn = true;

# Custom Lua function to convert the model's reply. Leave unset to use the
# built-in parsers for OpenAI / Ollama replies.
reply_conversion = "xxx";

# Controls how reply text is trimmed before sending to the LLM.
# Accepted values: "replies" (default), "all", "none".
reply_trim_mode = "replies";

# Minimum number of words a text part must contain to be considered.
# Parts shorter than this are skipped. 0 accepts any length.
min_words = 10;

# Header to add with the reason produced by GPT. Disabled (nil) by default.
# Set to a header name string to enable, e.g.:
# reason_header = "X-GPT-Reason";

# Skip GPT scan if **any** of these symbols are present (and their absolute
# weight is equal or above the specified value)
symbols_to_except = {
BAYES_SPAM = 0.9;
WHITELIST_SPF = -1;
};

# Trigger GPT scan only when **all** of these symbols are present (and their
# absolute weight is equal or above the specified value)
# symbols_to_trigger = {
# URIBL_BLOCKED = 1.0;
# };

# Check messages that resulted in a `passthrough` action
allow_passthrough = false;

# Check messages that already look like ham (negative score)
allow_ham = false;

# Request / expect JSON reply from GPT
json = false;

# Minimum spam probability (from ensemble voting) to set GPT_SPAM
consensus_spam_threshold = 0.75;

# Maximum ham probability (from ensemble voting) to set GPT_HAM
consensus_ham_threshold = 0.25;

# Map of extra virtual symbols that could be set from GPT response categories
# extra_symbols = {
# GPT_MARKETING = {
# score = 0.0;
# description = "GPT model detected marketing content";
# category = "marketing";
# group = "GPT";
# };
# };

# Prefix for Redis cache keys
cache_prefix = "rsllm";

# Add `response_format = {type = "json_object"}` to requests (OpenAI only)
include_response_format = false;

# API endpoint
url = "https://api.openai.com/v1/chat/completions";

# Custom context augmentation — a Lua expression that returns a function
# with signature: function(task, content, callback). The callback must be
# called with a string that is injected as additional context into the LLM
# prompt. Async operations (Redis, HTTP) are supported.
# context_augment = "return function(task, content, cb) cb('extra context') end";

# Optional user/domain conversation context stored in Redis
context = {
enabled = false;
# ... see Context block options below
};

# Optional web-search context (domains extracted from URLs in the email)
search_context = {
enabled = false;
# ... see Search Context block options below
};
}

Core options

OptionTypeDefaultDescription
typestring"openai"LLM provider. Accepted values: openai, ollama
api_keystringAPI key. Required for openai; not needed for ollama
modelstring or list"gpt-5-mini"Model name, or a list of names to query in parallel (ensemble mode)
model_parameterstablesee defaultsPer-model request body overrides (e.g. max_completion_tokens, temperature). Works for both openai and ollama
urlstring"https://api.openai.com/v1/chat/completions"API endpoint
timeoutnumber10Overall HTTP request timeout in seconds
connect_timeoutnumbernilTCP connect timeout in seconds (falls back to timeout)
ssl_timeoutnumbernilTLS handshake timeout in seconds (falls back to timeout)
write_timeoutnumbernilRequest write timeout in seconds (falls back to timeout)
read_timeoutnumbernilResponse read timeout in seconds (falls back to timeout)
request_timeoutnumbernilOptional server-side time budget sent as max_completion_time in the request body. Multiplied by 0.95 before sending. Not supported by the standard OpenAI API
promptstringnilSystem prompt. A sensible default is used when not set
conditionstringnilLua code returning a function that decides whether to run GPT on a message
autolearnbooleanfalseFeed GPT verdicts into Bayes learner
reply_conversionstringnilLua code returning a custom reply parser. Leave unset to use built-in parsers
reply_trim_modestring"replies"Controls body trimming before sending to the LLM: replies, all, or none
min_wordsnumber10Minimum word count for a text part to be selected. 0 accepts any length
reason_headerstringnilHeader name to inject with the GPT explanation (e.g. "X-GPT-Reason"). Disabled by default
symbols_to_excepttablesee defaultsSymbols that skip GPT processing; value is the minimum absolute weight (-1 = any weight)
symbols_to_triggertablenilSymbols that must all be present to trigger GPT processing
allow_passthroughbooleanfalseStill run GPT on messages with a passthrough action
allow_hambooleanfalseStill run GPT on messages that already have a negative score
jsonbooleanfalseAsk the model for a JSON response and parse it as such
consensus_spam_thresholdnumber0.75Minimum spam probability required when resolving multi-model ensemble votes
consensus_ham_thresholdnumber0.25Maximum ham probability required when resolving multi-model ensemble votes
extra_symbolstablesee defaultsExtra virtual symbols the model can set via returned categories
cache_prefixstring"rsllm"Redis key prefix for response caching
include_response_formatbooleanfalseAdd response_format = json_object to the OpenAI request (only useful with json = true)
context_augmentstringnilLua expression returning a function(task, content, callback) for custom async context injection

context block — user/domain conversation context

This optional block enables per-user or per-domain conversation history stored in Redis. When enabled, a compact digest of recent messages is injected into the LLM prompt so the model can take prior classification history into account. Context is updated after every classification.

OptionTypeDefaultDescription
enabledbooleanfalseEnable conversation context
levelstring"user"Identity scope: user, domain, or esld
key_prefixstring"user"Redis key prefix component
key_suffixstring"mail_context"Redis key suffix component
max_messagesnumber40Maximum compact message summaries to keep in the sliding window
min_messagesnumber5Warm-up threshold — context is injected into the prompt only after this many messages have been collected
message_ttlnumber1209600Messages older than this (seconds, default 14 days) are discarded when the window is recomputed
ttlnumber2592000Redis key TTL in seconds (default 30 days)
top_sendersnumber5Number of top senders to track
summary_max_charsnumber512Maximum characters of message body to store per summary
flagged_phraseslist["reset your password", "click here to verify"]Phrases to flag in stored summaries
last_labels_countnumber10Number of recent classification labels to retain
as_systembooleantrueInject the context snippet as a system message (false = user message)
enable_maptablenilSimple map-based gating: {selector = "...", map = "/path/to/map", type = "set"}
enable_expressiontablenilMaps-expression gating to enable context for specific conditions
disable_expressiontablenilMaps-expression gating to disable context for specific conditions

search_context block — web search context

This optional block enables on-the-fly domain lookups for URLs found in the email. Search results are cached in Redis and injected into the LLM prompt as additional context.

OptionTypeDefaultDescription
enabledbooleanfalseEnable web search context
search_urlstring"https://leta.mullvad.net/search/__data.json"Search API endpoint
search_enginestring"brave"Search engine hint passed to the API (e.g. brave, google)
max_domainsnumber3Maximum number of unique domains to search
max_results_per_querynumber3Maximum search results to use per domain
timeoutnumber5HTTP timeout in seconds for search requests
cache_ttlnumber3600Search result cache TTL in seconds (1 hour)
cache_key_prefixstring"gpt_search"Redis cache key prefix for search results
as_systembooleantrueInject search context as a system message (false = user message)
enable_expressiontablenilMaps-expression gating to enable search context for specific conditions
disable_expressiontablenilMaps-expression gating to disable search context for specific conditions

Example Configuration

Here is a minimal example configuration:

gpt {
enabled = true;
type = "openai";
api_key = "your_api_key_here";
model = "gpt-5-mini";
model_parameters = {
"gpt-5-mini" = {
max_completion_tokens = 1000,
}
}
timeout = 10s;
}

Additional Notes

JSON vs. Plain-Text Responses

The plugin works best when the model can answer in plain text because it gives the language model more freedom and generally yields better reasoning. Enabling json = true (and optionally include_response_format) constrains the model to produce a strict JSON object. This often degrades the probability estimate or makes the reply longer than required, so turn it on only if your prompts really need JSON.

reason_header

The reason_header option is disabled by default (nil). Set it to a header name string (e.g. reason_header = "X-GPT-Reason") to inject a mail header with the explanation produced by GPT. This is convenient for debugging or for downstream systems, but remember that the text is untrusted input and might reveal information to message recipients.

Multiple-Model Ensemble

The model option can be a list:

model = ["gpt-4o-mini", "gpt-5-nano"];

In this case Rspamd queries all listed models in parallel and applies a consensus algorithm:

  • Each model returns a spam probability.
  • If the majority classifies the message as spam with probability > consensus_spam_threshold (default 0.75), the highest spam probability is used.
  • If the majority classifies it as ham with probability < consensus_ham_threshold (default 0.25), the lowest ham probability is used.
  • Otherwise no GPT symbol is added (no consensus), and GPT_UNCERTAIN is set instead.

This improves robustness and lets you mix cheap fast models with a slower high-quality one.

Caching Policies

To avoid repeated LLM calls, responses are cached in Redis (if configured). Key facts:

  • Key: <cache_prefix>_<env>_<digest> where env depends on prompt, model, and url, and digest is the SHA256 of the examined text part (truncated).
  • Default TTL is one hour (cache_ttl option accepts seconds).
  • Workers coordinate using pending markers so that only one request per message is sent even in large clusters.
  • Changing the prompt, model list, or endpoint automatically invalidates the cache because they are part of the key.
  • You can tune cache_prefix, cache_ttl, and other cache options directly inside the gpt {} block.

Conclusion

The Rspamd GPT Plugin integrates OpenAI's GPT models into Rspamd, enhancing its spam filtering capabilities with advanced text processing techniques. By configuring the options above, users can customize the plugin to meet specific requirements, thereby enhancing the efficiency and accuracy of spam filtering within Rspamd.