]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Project] Use plugins registry
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 18 Nov 2025 14:13:27 +0000 (14:13 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 18 Nov 2025 14:13:27 +0000 (14:13 +0000)
src/plugins/lua/aws_s3.lua
src/plugins/lua/bimi.lua
src/plugins/lua/clustering.lua
src/plugins/lua/contextal.lua
src/plugins/lua/external_relay.lua
src/plugins/lua/history_redis.lua
src/plugins/lua/known_senders.lua
src/plugins/lua/milter_headers.lua
src/plugins/lua/neural.lua
src/plugins/lua/reputation.lua

index 99c8cabe4c59a58dd429b98fb2904888267e00d0..5a4290c06adbe39e8e5e3536aa4841d962505a56 100644 (file)
@@ -22,6 +22,7 @@ local T = require "lua_shape.core"
 local rspamd_text = require "rspamd_text"
 local rspamd_http = require "rspamd_http"
 local rspamd_util = require "rspamd_util"
+local PluginSchema = require "lua_shape.plugin_schema"
 
 local settings = {
   s3_bucket = nil,
@@ -53,6 +54,8 @@ local settings_schema = T.table({
   inline_content_limit = T.number():optional():doc({ summary = "Max inline content size before external ref" }),
 }):doc({ summary = "AWS S3 plugin configuration" })
 
+PluginSchema.register("plugins.aws_s3", settings_schema)
+
 local function raw_data(task, nonce, queue_id)
   local ext, content, content_type
 
index 68d09349d29405a14f82c71cdd3b405de903ce8b..a0303273979d301fab7c1e6abdd96cd0ac3d3933 100644 (file)
@@ -19,6 +19,7 @@ local lua_util = require "lua_util"
 local rspamd_logger = require "rspamd_logger"
 local T = require "lua_shape.core"
 local lua_redis = require "lua_redis"
+local PluginSchema = require "lua_shape.plugin_schema"
 local ucl = require "ucl"
 local lua_mime = require "lua_mime"
 local rspamd_http = require "rspamd_http"
@@ -67,6 +68,8 @@ local settings_schema = lua_redis.enrich_schema({
   }):optional():doc({ summary = "Helper read timeout (seconds)" }),
 })
 
+PluginSchema.register("plugins.bimi", settings_schema)
+
 local function check_dmarc_policy(task)
   local dmarc_sym = task:get_symbol('DMARC_POLICY_ALLOW')
 
index 4d7df96d8bb08cd9508a18c3201e5d16979fe0d5..f427d11117547c8e63e75d931fe35f726f7e636a 100644 (file)
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 ]]--
 
-if confighelp then
-  return
-end
-
 -- Plugin for finding patterns in email flows
 
 local N = 'clustering'
@@ -28,6 +24,7 @@ local lua_verdict = require "lua_verdict"
 local lua_redis = require "lua_redis"
 local lua_selectors = require "lua_selectors"
 local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 
 local redis_params
 
@@ -68,6 +65,20 @@ local rule_schema = T.table({
   prefix = T.string():optional():doc({ summary = "Redis key prefix" }),
 }):doc({ summary = "Clustering rule configuration" })
 
+local config_schema = lua_redis.enrich_schema({
+  enabled = T.boolean():optional():doc({ summary = "Enable the plugin" }),
+  rules = T.table({}, {
+    open = true,
+    extra = rule_schema
+  }):doc({ summary = "Clustering rules keyed by name" })
+}):doc({ summary = "Clustering plugin configuration" })
+
+PluginSchema.register("plugins.clustering", config_schema)
+
+if confighelp then
+  return
+end
+
 -- Redis scripts
 
 -- Queries for a cluster's data
@@ -256,7 +267,6 @@ local function clusterting_idempotent_cb(task, rule)
   )
 end
 -- Init part
-redis_params = lua_redis.parse_redis_server('clustering')
 local opts = rspamd_config:get_all_opt("clustering")
 
 -- Initialization part
@@ -265,6 +275,17 @@ if not (opts and type(opts) == 'table') then
   return
 end
 
+local cfg, cfg_err = config_schema:transform(opts)
+if not cfg then
+  rspamd_logger.errx(rspamd_config, 'invalid clustering config: %s', cfg_err)
+  lua_util.disable_module(N, "config")
+  return
+end
+
+opts = cfg
+
+redis_params = lua_redis.parse_redis_server('clustering', opts)
+
 if not redis_params then
   lua_util.disable_module(N, "redis")
   return
index 22ef6476c6e6dada17e9d1761626181f5fb979f8..c43991bb145074e1efb9d2dd3990ccc7153419d3 100644 (file)
@@ -17,15 +17,6 @@ limitations under the License.
 local E = {}
 local N = 'contextal'
 
-if confighelp then
-  return
-end
-
-local opts = rspamd_config:get_all_opt(N)
-if not opts then
-  return
-end
-
 local lua_redis = require "lua_redis"
 local lua_util = require "lua_util"
 local redis_cache = require "lua_cache"
@@ -34,6 +25,7 @@ local rspamd_logger = require "rspamd_logger"
 local rspamd_util = require "rspamd_util"
 local T = require "lua_shape.core"
 local ucl = require "ucl"
+local PluginSchema = require "lua_shape.plugin_schema"
 
 local cache_context, redis_params
 
@@ -65,6 +57,17 @@ local config_schema = lua_redis.enrich_schema {
   read_timeout = T.number():optional():doc({ summary = "Read timeout (seconds)" }),
 }
 
+PluginSchema.register("plugins.contextal", config_schema)
+
+if confighelp then
+  return
+end
+
+local opts = rspamd_config:get_all_opt(N)
+if not opts then
+  return
+end
+
 local settings = {
   action_symbol_prefix = 'CONTEXTAL_ACTION',
   base_url = 'http://localhost:8080',
index 95d3ce2e38b5c83d4702cbdfecaebe7f3786ac8b..6dd1e3e462cccc9844e6ef3653caa7af1f7200a8 100644 (file)
@@ -18,14 +18,11 @@ limitations under the License.
 external_relay plugin - sets IP/hostname from Received headers
 ]]--
 
-if confighelp then
-  return
-end
-
 local lua_maps = require "lua_maps"
 local lua_util = require "lua_util"
 local rspamd_logger = require "rspamd_logger"
 local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 
 local E = {}
 local N = "external_relay"
@@ -92,6 +89,12 @@ local config_schema = T.table({
   }):doc({ summary = "External relay rules keyed by name" }),
 }):doc({ summary = "External relay plugin configuration" })
 
+PluginSchema.register("plugins.external_relay", config_schema)
+
+if confighelp then
+  return
+end
+
 local function set_from_rcvd(task, rcvd)
   local rcvd_ip = rcvd.real_ip
   if not (rcvd_ip and rcvd_ip:is_valid()) then
index a0e83bd3ba5bb71e7ef8061198804d12bdec4c16..212735f7b0252947821a6bf39e0d401c3e3fdc86 100644 (file)
@@ -14,32 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 ]]--
 
-if confighelp then
-  rspamd_config:add_example(nil, 'history_redis',
-      "Store history of checks for WebUI using Redis",
-      [[
-redis_history {
-  # History key name
-  key_prefix = 'rs_history{{HOSTNAME}}{{COMPRESS}}';
-  # Expire in seconds for inactive keys, default to 5 days
-  expire = 432000;
-  # History rows limit
-  nrows = 200;
-  # Use zstd compression when storing data in redis
-  compress = true;
-  # Obfuscate subjects for privacy
-  subject_privacy = false;
-  # Default hash-algorithm to obfuscate subject
-  subject_privacy_alg = 'blake2';
-  # Prefix to show it's obfuscated
-  subject_privacy_prefix = 'obf';
-  # Cut the length of the hash if desired
-  subject_privacy_length = 16;
-}
-  ]])
-  return
-end
-
 local rspamd_logger = require "rspamd_logger"
 local rspamd_util = require "rspamd_util"
 local lua_util = require "lua_util"
@@ -47,6 +21,7 @@ local lua_redis = require "lua_redis"
 local fun = require "fun"
 local ucl = require "ucl"
 local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 local E = {}
 local N = "history_redis"
 
@@ -81,6 +56,34 @@ local settings_schema = lua_redis.enrich_schema({
   subject_privacy_length = T.number():optional():doc({ summary = "Hash length for obfuscated subjects" }),
 })
 
+PluginSchema.register("plugins.history_redis", settings_schema)
+
+if confighelp then
+  rspamd_config:add_example(nil, 'history_redis',
+      "Store history of checks for WebUI using Redis",
+      [[
+redis_history {
+  # History key name
+  key_prefix = 'rs_history{{HOSTNAME}}{{COMPRESS}}';
+  # Expire in seconds for inactive keys, default to 5 days
+  expire = 432000;
+  # History rows limit
+  nrows = 200;
+  # Use zstd compression when storing data in redis
+  compress = true;
+  # Obfuscate subjects for privacy
+  subject_privacy = false;
+  # Default hash-algorithm to obfuscate subject
+  subject_privacy_alg = 'blake2';
+  # Prefix to show it's obfuscated
+  subject_privacy_prefix = 'obf';
+  # Cut the length of the hash if desired
+  subject_privacy_length = 16;
+}
+  ]])
+  return
+end
+
 local function process_addr(addr)
   if addr then
     return addr.addr
index 0cbf3cdcf17bdab432c36376d83790327c4a2e92..bc3581c5b3add711aced9eb308e86f5730e050b4 100644 (file)
@@ -23,30 +23,12 @@ local lua_util = require "lua_util"
 local lua_redis = require "lua_redis"
 local lua_maps = require "lua_maps"
 local rspamd_cryptobox_hash = require "rspamd_cryptobox_hash"
-
-if confighelp then
-  rspamd_config:add_example(nil, 'known_senders',
-      "Maintain a list of known senders using Redis",
-      [[
-known_senders {
-  # Domains to track senders
-  domains = "https://maps.rspamd.com/freemail/free.txt.zst";
-  # Maximum number of elements
-  max_senders = 100000;
-  # Maximum time to live (when not using bloom filters)
-  max_ttl = 30d;
-  # Use bloom filters (must be enabled in Redis as a plugin)
-  use_bloom = false;
-  # Insert symbol for new senders from the specific domains
-  symbol_unknown = 'UNKNOWN_SENDER';
-}
-  ]])
-  return
-end
+local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 
 local redis_params
 local settings = {
-  domains = {},
+  domains = nil,
   max_senders = 100000,
   max_ttl = 30 * 86400,
   use_bloom = false,
@@ -65,27 +47,54 @@ local settings = {
   reply_sender_privacy_length = 16,
 }
 
---[[
-XXX: please fix tableshape one day
 local settings_schema = lua_redis.enrich_schema({
-  domains = lua_maps.map_schema:is_optional(),
-  enabled = ts.boolean:is_optional(),
-  max_senders = (ts.integer + ts.string / tonumber):is_optional(),
-  max_ttl = (ts.integer + ts.string / tonumber):is_optional(),
-  use_bloom = ts.boolean:is_optional(),
-  redis_key = ts.string:is_optional(),
-  symbol = ts.string:is_optional(),
-  symbol_unknown = ts.string:is_optional(),
-  max_recipients = ts.integer:is_optional(),
-  sender_prefix = ts.string:is_optional(),
-  sender_key_global = ts.string:is_optional(),
-  sender_key_size = ts.integer:is_optional(),
-  reply_sender_privacy = ts.boolean:is_optional(),
-  reply_sender_privacy_alg = ts.string:is_optional(),
-  reply_sender_privacy_prefix = ts.string:is_optional(),
-  reply_sender_privacy_length = ts.integer:is_optional(),
-})
-]]--
+  domains = lua_maps.map_schema:optional():doc({ summary = "Map of domains to track senders" }),
+  enabled = T.boolean():optional():doc({ summary = "Enable the plugin" }),
+  max_senders = T.one_of({
+    T.number(),
+    T.transform(T.string(), tonumber)
+  }):optional():doc({ summary = "Maximum number of senders to keep" }),
+  max_ttl = T.one_of({
+    T.number(),
+    T.transform(T.string(), lua_util.parse_time_interval)
+  }):optional():doc({ summary = "Maximum sender lifetime (seconds)" }),
+  use_bloom = T.boolean():optional():doc({ summary = "Use Redis bloom filters" }),
+  redis_key = T.string():optional():doc({ summary = "Redis key for known senders" }),
+  symbol = T.string():optional():doc({ summary = "Symbol for known senders" }),
+  symbol_unknown = T.string():optional():doc({ summary = "Symbol for unknown senders" }),
+  symbol_check_mail_global = T.string():optional():doc({ summary = "Symbol to check global replies" }),
+  symbol_check_mail_local = T.string():optional():doc({ summary = "Symbol to check local replies" }),
+  max_recipients = T.number():optional():doc({ summary = "Maximum recipients to inspect" }),
+  sender_prefix = T.string():optional():doc({ summary = "Redis prefix for sender reply sets" }),
+  sender_key_global = T.string():optional():doc({ summary = "Redis key for global replies" }),
+  sender_key_size = T.number():optional():doc({ summary = "Length of hashed sender keys" }),
+  reply_sender_privacy = T.boolean():optional():doc({ summary = "Enable reply sender privacy mode" }),
+  reply_sender_privacy_alg = T.string():optional():doc({ summary = "Hash algorithm for sender privacy" }),
+  reply_sender_privacy_prefix = T.string():optional():doc({ summary = "Prefix for hashed sender keys" }),
+  reply_sender_privacy_length = T.number():optional():doc({ summary = "Length of hashed sender output" }),
+}):doc({ summary = "Known senders plugin configuration" })
+
+PluginSchema.register("plugins.known_senders", settings_schema)
+
+if confighelp then
+  rspamd_config:add_example(nil, 'known_senders',
+      "Maintain a list of known senders using Redis",
+      [[
+known_senders {
+  # Domains to track senders
+  domains = "https://maps.rspamd.com/freemail/free.txt.zst";
+  # Maximum number of elements
+  max_senders = 100000;
+  # Maximum time to live (when not using bloom filters)
+  max_ttl = 30d;
+  # Use bloom filters (must be enabled in Redis as a plugin)
+  use_bloom = false;
+  # Insert symbol for new senders from the specific domains
+  symbol_unknown = 'UNKNOWN_SENDER';
+}
+  ]])
+  return
+end
 
 local function make_key(input)
   local hash = rspamd_cryptobox_hash.create_specific('md5')
@@ -367,6 +376,20 @@ end
 local opts = rspamd_config:get_all_opt('known_senders')
 if opts then
   settings = lua_util.override_defaults(settings, opts)
+  local res, err = settings_schema:transform(settings)
+  if not res then
+    rspamd_logger.errx(rspamd_config, 'invalid %s config: %s', N, err)
+    lua_util.disable_module(N, "config")
+    return
+  end
+  settings = res
+
+  if not settings.domains then
+    rspamd_logger.errx(rspamd_config, '%s requires "domains" map to be defined', N)
+    lua_util.disable_module(N, "config")
+    return
+  end
+
   redis_params = lua_redis.parse_redis_server(N, opts)
 
   if redis_params then
index bc1c4f5d8d324a124cb487a5f87e94949200f748..e13b5a9ec8365514c4373af18b86631c13b0854c 100644 (file)
@@ -15,10 +15,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 ]] --
 
-if confighelp then
-  return
-end
-
 -- A plugin that provides common header manipulations
 
 local logger = require "rspamd_logger"
@@ -28,6 +24,7 @@ local lua_util = require "lua_util"
 local lua_maps = require "lua_maps"
 local lua_mime = require "lua_mime"
 local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 local E = {}
 
 local HOSTNAME = rspamd_util.get_hostname()
@@ -657,6 +654,12 @@ local config_schema = T.table({
   open = true
 }):doc({ summary = "Milter headers plugin configuration" })
 
+PluginSchema.register("plugins.milter_headers", config_schema)
+
+if confighelp then
+  return
+end
+
 local opts = rspamd_config:get_all_opt(N) or
     rspamd_config:get_all_opt('rmilter_headers')
 
index 494ef43ef84c02d249f0d1588ca55e60e881a3ff..282f49ef5d6e9e635ed8b353d852bd0aada62601 100644 (file)
@@ -15,10 +15,6 @@ limitations under the License.
 ]] --
 
 
-if confighelp then
-  return
-end
-
 local fun = require "fun"
 local lua_redis = require "lua_redis"
 local lua_util = require "lua_util"
@@ -30,6 +26,7 @@ local rspamd_tensor = require "rspamd_tensor"
 local rspamd_text = require "rspamd_text"
 local rspamd_util = require "rspamd_util"
 local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 -- Load providers
 pcall(require, "plugins/neural/providers/llm")
 pcall(require, "plugins/neural/providers/symbols")
@@ -47,6 +44,12 @@ local redis_profile_schema = T.table({
   providers_digest = T.string():optional():doc({ summary = "Providers digest" }),
 }):doc({ summary = "Neural network profile schema" })
 
+PluginSchema.register("plugins.neural.profile", redis_profile_schema)
+
+if confighelp then
+  return
+end
+
 local has_blas = rspamd_tensor.has_blas()
 local text_cookie = rspamd_text.cookie
 
index 4511c5c1e1bc5dbcd901dc911c18cd1d691e5029..e43edc3478424ec6af86117cbb307e98a60d63f7 100644 (file)
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 ]]--
 
-if confighelp then
-  return
-end
-
 -- A generic plugin for reputation handling
 
 local E = {}
@@ -33,6 +29,7 @@ local lua_redis = require "lua_redis"
 local fun = require "fun"
 local lua_selectors = require "lua_selectors"
 local T = require "lua_shape.core"
+local PluginSchema = require "lua_shape.plugin_schema"
 
 local redis_params = nil
 local default_expiry = 864000 -- 10 day by default
@@ -848,6 +845,8 @@ local generic_selector = {
   idempotent = generic_reputation_idempotent -- used to set scores
 }
 
+PluginSchema.register("plugins.reputation.selector.generic", generic_selector.schema)
+
 local selectors = {
   ip = ip_selector,
   sender = ip_selector, -- Better name
@@ -1203,6 +1202,13 @@ local backends = {
   }
 }
 
+PluginSchema.register("plugins.reputation.backend.redis", backends.redis.schema)
+PluginSchema.register("plugins.reputation.backend.dns", backends.dns.schema)
+
+if confighelp then
+  return
+end
+
 local function is_rule_applicable(task, rule)
   local ip = task:get_from_ip()
   if not (rule.selector.config.outbound and rule.selector.config.inbound) then