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')
|