aboutsummaryrefslogtreecommitdiffstats
path: root/rules/bounce.lua
blob: fb74b97978728b3a88d19ff9a501a68d673bf29a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
--[[
Copyright (c) 2020, Anton Yuzhaninov <citrin@citrin.ru>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--

-- Rule to detect bounces:
-- RFC 3464 Delivery status notifications and most common non-standard ones

local function make_subj_bounce_keywords_re()
  -- Words and phrases commonly used in Subjects for bounces
  -- We cannot practically test all localized Subjects, but luckily English is by far the most common here
  local keywords = {
    'could not send message',
    "couldn't be delivered",
    'delivery failed',
    'delivery failure',
    'delivery report',
    'delivery status',
    'delivery warning',
    'failure delivery',
    'failure notice',
    "hasn't been delivered",
    'mail failure',
    'returned mail',
    'undeliverable',
    'undelivered',
  }
  return string.format([[Subject=/\b(%s)\b/i{header}]], table.concat(keywords, '|'))
end

config.regexp.SUBJ_BOUNCE_WORDS = {
  re = make_subj_bounce_keywords_re(),
  group = 'headers',
  score = 0.0,
  description = 'Words/phrases typical for DSN'
}

rspamd_config.BOUNCE = {
  callback = function(task)
    local from = task:get_from('smtp')
    if from and from[1].addr ~= '' then
      -- RFC 3464:
      -- Whenever an SMTP transaction is used to send a DSN, the MAIL FROM
      -- command MUST use a NULL return address, i.e., "MAIL FROM:<>"
      -- In practise it is almost always the case for DSN
      return false
    end

    local parts = task:get_parts()
    local top_type, top_subtype, params = parts[1]:get_type_full()
    -- RFC 3464, RFC 8098
    if top_type == 'multipart' and top_subtype == 'report' and params and
        (params['report-type'] == 'delivery-status' or params['report-type'] == 'disposition-notification') then
      -- Assume that inner parts are OK, don't check them to save time
      return true, 1.0, 'DSN'
    end

    -- Apply heuristics for non-standard bounces
    local bounce_sender
    local mime_from = task:get_from('mime')
    if mime_from then
      local from_user = mime_from[1].user:lower()
      -- Check common bounce senders
      if (from_user == 'postmaster' or from_user == 'mailer-daemon') then
        bounce_sender = from_user
        -- MDaemon >= 14.5 sends multipart/report (RFC 3464) DSN covered above,
        -- but older versions send non-standard bounces with localized subjects and they
        -- are still around
      elseif from_user == 'mdaemon' and task:has_header('X-MDDSN-Message') then
        return true, 1.0, 'MDaemon'
      end
    end

    local subj_keywords = task:has_symbol('SUBJ_BOUNCE_WORDS')

    if not (bounce_sender or subj_keywords) then
      return false
    end

    if bounce_sender and subj_keywords then
      return true, 0.5, bounce_sender .. '+subj'
    end

    -- Look for a message/rfc822(-headers) part inside
    local rfc822_part
    parts[10] = nil -- limit number of parts to check
    for _, p in ipairs(parts) do
      local mime_type, mime_subtype = p:get_type()
      if (mime_subtype == 'rfc822' or mime_subtype == 'rfc822-headers') and
          (mime_type == 'message' or mime_type == 'text') then
        rfc822_part = mime_type .. '/' .. mime_subtype
        break
      end
    end

    if rfc822_part and bounce_sender then
      return true, 0.5, bounce_sender .. '+' .. rfc822_part
    elseif rfc822_part and subj_keywords then
      return true, 0.2, rfc822_part .. '+subj'
    end
  end,
  description = '(Non) Delivery Status Notification',
  group = 'headers',
}

rspamd_config:register_dependency('BOUNCE', 'SUBJ_BOUNCE_WORDS')