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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
|
--[[
Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
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.
]]--
-- Plugin for comparing smtp dialog recipients and sender with recipients and sender
-- in mime headers
if confighelp then
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'
local symbol_sender = 'FORGED_SENDER'
local rspamd_util = require "rspamd_util"
local E = {}
local function check_forged_headers(task)
local auser = task:get_user()
local delivered_to = task:get_header('Delivered-To')
local smtp_rcpts = task:get_recipients(1)
local smtp_from = task:get_from(1)
if not smtp_rcpts then
return
end
if #smtp_rcpts == 0 then
return
end
local mime_rcpts = task:get_recipients({ 'mime', 'orig' })
if not mime_rcpts then
return
elseif #mime_rcpts == 0 then
return
end
-- Find pair for each smtp recipient in To or Cc headers
if #smtp_rcpts > 100 or #mime_rcpts > 100 then
-- Trim array, suggested by Anton Yuzhaninov
smtp_rcpts[100] = nil
mime_rcpts[100] = nil
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
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
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:lower()]._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)
if not mime_from or not mime_from[1] or
not rspamd_util.strequal_caseless_utf8(mime_from[1]['addr'], smtp_from[1]['addr']) then
task:insert_result(symbol_sender, 1, ((mime_from or E)[1] or E).addr or '', smtp_from[1].addr)
end
end
end
-- Configuration
local opts = rspamd_config:get_all_opt('forged_recipients')
if opts then
if opts['symbol_rcpt'] or opts['symbol_sender'] then
local id = rspamd_config:register_symbol({
name = 'FORGED_CALLBACK',
callback = check_forged_headers,
type = 'callback',
group = 'headers',
score = 0.0,
})
if opts['symbol_rcpt'] then
symbol_rcpt = opts['symbol_rcpt']
rspamd_config:register_symbol({
name = symbol_rcpt,
type = 'virtual',
parent = id,
})
end
if opts['symbol_sender'] then
symbol_sender = opts['symbol_sender']
rspamd_config:register_symbol({
name = symbol_sender,
type = 'virtual',
parent = id,
})
end
end
end
|