]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] headers_checks: add Reply-To address validity checks
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 1 Feb 2026 12:31:17 +0000 (12:31 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 1 Feb 2026 15:44:47 +0000 (15:44 +0000)
Add RFC 5321 compliance checks for Reply-To header:
- REPLYTO_INVALID: address doesn't pass RFC 5321 validation
- REPLYTO_LOCALPART_LONG: local-part exceeds 64 characters
- REPLYTO_DOMAIN_LONG: domain exceeds 255 characters

This helps detect spam with intentionally invalid Reply-To addresses
that users cannot actually reply to.

Closes: #5854
rules/headers_checks.lua

index 05906cc7a5f6b7752feae8fb0cad45352111fb25..5b8f1623d6a0b177ebc4e6c3daefe47d64e34712 100644 (file)
@@ -198,12 +198,26 @@ local check_replyto_id = rspamd_config:register_symbol({
       return false
     end
     local rt = util.parse_mail_address(replyto, task:get_mempool())
-    if not (rt and rt[1] and (string.len(rt[1].addr) > 0)) then
+    if not (rt and rt[1] and #rt[1].addr > 0) then
       task:insert_result('REPLYTO_UNPARSEABLE', 1.0)
       return false
     else
       local rta = rt[1].addr
       task:insert_result('HAS_REPLYTO', 1.0, rta)
+
+      -- RFC 5321 validity checks
+      local dominated_by_unparseable = not rt[1].flags or not rt[1].flags.valid
+      if dominated_by_unparseable then
+        task:insert_result('REPLYTO_INVALID', 1.0)
+      end
+      local user_len = rt[1].user and #rt[1].user or 0
+      local domain_len = rt[1].domain and #rt[1].domain or 0
+      if user_len > 64 then
+        task:insert_result('REPLYTO_LOCALPART_LONG', 1.0, tostring(user_len))
+      end
+      if domain_len > 255 then
+        task:insert_result('REPLYTO_DOMAIN_LONG', 1.0, tostring(domain_len))
+      end
       -- Check if Reply-To address starts with title seen in display name
       local sym = task:get_symbol('FROM_NAME_HAS_TITLE')
       local title = (((sym or E)[1] or E).options or E)[1]
@@ -353,6 +367,30 @@ rspamd_config:register_symbol {
   description = 'Reply-To domain does not match the To domain',
   group = 'headers',
 }
+rspamd_config:register_symbol {
+  name = 'REPLYTO_INVALID',
+  score = 1.0,
+  parent = check_replyto_id,
+  type = 'virtual',
+  description = 'Reply-To address is not RFC 5321 compliant',
+  group = 'headers',
+}
+rspamd_config:register_symbol {
+  name = 'REPLYTO_LOCALPART_LONG',
+  score = 2.0,
+  parent = check_replyto_id,
+  type = 'virtual',
+  description = 'Reply-To local-part exceeds 64 characters (RFC 5321)',
+  group = 'headers',
+}
+rspamd_config:register_symbol {
+  name = 'REPLYTO_DOMAIN_LONG',
+  score = 2.0,
+  parent = check_replyto_id,
+  type = 'virtual',
+  description = 'Reply-To domain exceeds 255 characters (RFC 5321)',
+  group = 'headers',
+}
 
 rspamd_config:register_dependency('CHECK_REPLYTO', 'CHECK_FROM')