]> source.dussan.org Git - rspamd.git/commitdiff
[Rework] Rework and refactor forged recipients plugin
authorVsevolod Stakhov <vsevolod@highsecure.ru>
Mon, 20 Jul 2020 14:45:24 +0000 (15:45 +0100)
committerVsevolod Stakhov <vsevolod@highsecure.ru>
Mon, 20 Jul 2020 18:49:04 +0000 (19:49 +0100)
src/plugins/lua/forged_recipients.lua

index 328644c8c912635a43dd57fbb1e5eca71a21e6b9..4f7942f7985ebf2994714ae87786f25f70155cdc 100644 (file)
@@ -18,7 +18,14 @@ limitations under the License.
 -- in mime headers
 
 if confighelp then
-  return
+  rspamd_config:add_example(nil, 'forged_recipients',
+      "Check forged recipients and senders (e.g. mime and smtp recipients mismatch)",
+      [[
+  forged_recipients {
+    symbol_sender = "FORGED_SENDER"; # Symbol for a forged sender
+    symbol_rcpt = "FORGED_RECIPIENTS"; # Symbol for a forged recipients
+  }
+  ]])
 end
 
 local symbol_rcpt = 'FORGED_RECIPIENTS'
@@ -29,68 +36,107 @@ local E = {}
 local function check_forged_headers(task)
   local auser = task:get_user()
   local delivered_to = task:get_header('Delivered-To')
-  local smtp_rcpt = task:get_recipients(1)
+  local smtp_rcpts = task:get_recipients(1)
   local smtp_from = task:get_from(1)
-  local res
-  local score = 1.0
 
-  if not smtp_rcpt then return end
-  if #smtp_rcpt == 0 then return end
+  if not smtp_rcpts then return end
+  if #smtp_rcpts == 0 then return end
 
-  local mime_rcpt = task:get_recipients({'mime','orig'})
+  local mime_rcpts = task:get_recipients({ 'mime', 'orig'})
 
-  if not mime_rcpt then
+  if not mime_rcpts then
     return
-  elseif #mime_rcpt == 0 then
+  elseif #mime_rcpts == 0 then
     return
   end
 
   -- Find pair for each smtp recipient in To or Cc headers
-  -- This cycle has O(N^2) complexity so it is better to limit number of iterations
-  if #smtp_rcpt > 100 or #mime_rcpt > 100 then
+  if #smtp_rcpts > 100 or #mime_rcpts > 100 then
     -- Trim array, suggested by Anton Yuzhaninov
-    smtp_rcpt[100] = nil
-    mime_rcpt[100] = nil
+    smtp_rcpts[100] = nil
+    mime_rcpts[100] = nil
   end
 
-  for _,sr in ipairs(smtp_rcpt) do
-    res = false
-    for _,mr in ipairs(mime_rcpt) do
-      if mr.addr and mr.addr ~= '' then
-        if sr['addr'] and
-            string.lower(mr['addr']) == string.lower(sr['addr']) then
-          res = true
-          break
-        elseif delivered_to and delivered_to == mr['addr'] then
-          -- allow alias expansion and forwarding (Postfix)
-          res = true
-          break
-        elseif auser and auser == sr['addr'] then
-          -- allow user to BCC themselves
-          res = true
-          break
-        elseif ((smtp_from or E)[1] or E).addr and
-            smtp_from[1]['addr'] == sr['addr'] then
-          -- allow sender to BCC themselves
-          res = true
-          break
-        elseif mr['user'] and sr['user'] and
-            string.lower(mr['user']) == string.lower(sr['user']) then
-          -- If we have the same username but for another domain, then
-          -- lower the overall score
-          score = score / 2
-        end
+  -- map smtp recipient domains to a list of addresses for this domain
+  local smtp_rcpt_domain_map = {}
+  local smtp_rcpt_map = {}
+  for _, smtp_rcpt in ipairs(smtp_rcpts) do
+    local addr = smtp_rcpt.addr
+
+    if addr and addr ~= '' then
+      local dom = string.lower(smtp_rcpt.domain)
+      addr = addr:lower()
+
+      local dom_map = smtp_rcpt_domain_map[dom]
+      if not dom_map then
+        dom_map = {}
+        smtp_rcpt_domain_map[dom] = dom_map
+      end
+
+      dom_map[addr] = smtp_rcpt
+      smtp_rcpt_map[addr] = smtp_rcpt
+
+      if auser and auser == addr then
+        smtp_rcpt.matched = true
+      end
+      if ((smtp_from or E)[1] or E).addr and
+          smtp_from[1]['addr'] == addr then
+        -- allow sender to BCC themselves
+        smtp_rcpt.matched = true
+      end
+    end
+  end
+
+  for _,mime_rcpt in ipairs(mime_rcpts) do
+    if mime_rcpt.addr and mime_rcpt.addr ~= '' then
+      local addr = string.lower(mime_rcpt.addr)
+      local dom =  string.lower(mime_rcpt.domain)
+      local matched_smtp_addr = smtp_rcpt_map[addr]
+      if matched_smtp_addr then
+        -- Direct match, go forward
+        matched_smtp_addr.matched = true
+        mime_rcpt.matched = true
+      elseif delivered_to and delivered_to == addr then
+        mime_rcpt.matched = true
+      elseif auser and auser == addr then
+        -- allow user to BCC themselves
+        mime_rcpt.matched = true
       else
-        res = true
+        local matched_smtp_domain = smtp_rcpt_domain_map[dom]
+
+        if matched_smtp_domain then
+          -- Same domain but another user, it is likely okay due to aliases substitution
+          mime_rcpt.matched = true
+          -- Special field
+          matched_smtp_domain._seen_mime_domain = true
+        end
       end
     end
-    if not res then
-      local mra = mime_rcpt[1].addr .. (#mime_rcpt > 1 and ' ..' or '')
-      local sra = smtp_rcpt[1].addr .. (#smtp_rcpt > 1 and ' ...' or '')
-      task:insert_result(symbol_rcpt, score, mra, sra)
-      break
+  end
+
+  -- Now go through all lists one more time and find unmatched stuff
+  local opts = {}
+  local seen_mime_unmatched = false
+  local seen_smtp_unmatched = false
+  for _,mime_rcpt in ipairs(mime_rcpts) do
+    if not mime_rcpt.matched then
+      seen_mime_unmatched = true
+      table.insert(opts, 'm:' .. mime_rcpt.addr)
+    end
+  end
+  for _,smtp_rcpt in ipairs(smtp_rcpts) do
+    if not smtp_rcpt.matched then
+      if not smtp_rcpt_domain_map[smtp_rcpt.domain]._seen_mime_domain then
+        seen_smtp_unmatched = true
+        table.insert(opts, 's:' .. smtp_rcpt.addr)
+      end
     end
   end
+
+  if seen_smtp_unmatched and seen_mime_unmatched then
+    task:insert_result(symbol_rcpt, 1.0, opts)
+  end
+
   -- Check sender
   if smtp_from and smtp_from[1] and smtp_from[1]['addr'] ~= '' then
     local mime_from = task:get_from(2)