blob: acc40559e264f9eadfe99d2fe21bc1a5fc74b58f [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001rspamd_config.MAILCOW_AUTH = {
2 callback = function(task)
3 local uname = task:get_user()
4 if uname then
5 return 1
6 end
7 end
8}
9
10local monitoring_hosts = rspamd_config:add_map{
11 url = "/etc/rspamd/custom/monitoring_nolog.map",
12 description = "Monitoring hosts",
13 type = "regexp"
14}
15
16rspamd_config:register_symbol({
17 name = 'SMTP_ACCESS',
18 type = 'postfilter',
19 callback = function(task)
20 local util = require("rspamd_util")
21 local rspamd_logger = require "rspamd_logger"
22 local rspamd_ip = require 'rspamd_ip'
23 local uname = task:get_user()
24 local limited_access = task:get_symbol("SMTP_LIMITED_ACCESS")
25
26 if not uname then
27 return false
28 end
29
30 if not limited_access then
31 return false
32 end
33
34 local hash_key = 'SMTP_ALLOW_NETS_' .. uname
35
36 local redis_params = rspamd_parse_redis_server('smtp_access')
37 local ip = task:get_from_ip()
38
39 if ip == nil or not ip:is_valid() then
40 return false
41 end
42
43 local from_ip_string = tostring(ip)
44 smtp_access_table = {from_ip_string}
45
46 local maxbits = 128
47 local minbits = 32
48 if ip:get_version() == 4 then
49 maxbits = 32
50 minbits = 8
51 end
52 for i=maxbits,minbits,-1 do
53 local nip = ip:apply_mask(i):to_string() .. "/" .. i
54 table.insert(smtp_access_table, nip)
55 end
56 local function smtp_access_cb(err, data)
57 if err then
58 rspamd_logger.infox(rspamd_config, "smtp_access query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
59 return false
60 else
61 rspamd_logger.infox(rspamd_config, "checking ip %s for smtp_access in %s", from_ip_string, hash_key)
62 for k,v in pairs(data) do
63 if (v and v ~= userdata and v == '1') then
64 rspamd_logger.infox(rspamd_config, "found ip in smtp_access map")
65 task:insert_result(true, 'SMTP_ACCESS', 0.0, from_ip_string)
66 return true
67 end
68 end
69 rspamd_logger.infox(rspamd_config, "couldnt find ip in smtp_access map")
70 task:insert_result(true, 'SMTP_ACCESS', 999.0, from_ip_string)
71 return true
72 end
73 end
74 table.insert(smtp_access_table, 1, hash_key)
75 local redis_ret_user = rspamd_redis_make_request(task,
76 redis_params, -- connect params
77 hash_key, -- hash key
78 false, -- is write
79 smtp_access_cb, --callback
80 'HMGET', -- command
81 smtp_access_table -- arguments
82 )
83 if not redis_ret_user then
84 rspamd_logger.infox(rspamd_config, "cannot check smtp_access redis map")
85 end
86 end,
87 priority = 10
88})
89
90rspamd_config:register_symbol({
91 name = 'POSTMASTER_HANDLER',
92 type = 'prefilter',
93 callback = function(task)
94 local rcpts = task:get_recipients('smtp')
95 local rspamd_logger = require "rspamd_logger"
96 local lua_util = require "lua_util"
97 local from = task:get_from(1)
98
99 -- not applying to mails with more than one rcpt to avoid bypassing filters by addressing postmaster
100 if rcpts and #rcpts == 1 then
101 for _,rcpt in ipairs(rcpts) do
102 local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
103 if #rcpt_split == 2 then
104 if rcpt_split[1] == 'postmaster' then
105 task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
106 return
107 end
108 end
109 end
110 end
111
112 if from then
113 for _,fr in ipairs(from) do
114 local fr_split = rspamd_str_split(fr['addr'], '@')
115 if #fr_split == 2 then
116 if fr_split[1] == 'postmaster' and task:get_user() then
117 -- no whitelist, keep signatures
118 task:insert_result(true, 'POSTMASTER_FROM', -2500.0)
119 return
120 end
121 end
122 end
123 end
124
125 end,
126 priority = 10
127})
128
129rspamd_config:register_symbol({
130 name = 'KEEP_SPAM',
131 type = 'prefilter',
132 callback = function(task)
133 local util = require("rspamd_util")
134 local rspamd_logger = require "rspamd_logger"
135 local rspamd_ip = require 'rspamd_ip'
136 local uname = task:get_user()
137
138 if uname then
139 return false
140 end
141
142 local redis_params = rspamd_parse_redis_server('keep_spam')
143 local ip = task:get_from_ip()
144
145 if ip == nil or not ip:is_valid() then
146 return false
147 end
148
149 local from_ip_string = tostring(ip)
150 ip_check_table = {from_ip_string}
151
152 local maxbits = 128
153 local minbits = 32
154 if ip:get_version() == 4 then
155 maxbits = 32
156 minbits = 8
157 end
158 for i=maxbits,minbits,-1 do
159 local nip = ip:apply_mask(i):to_string() .. "/" .. i
160 table.insert(ip_check_table, nip)
161 end
162 local function keep_spam_cb(err, data)
163 if err then
164 rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
165 return false
166 else
167 for k,v in pairs(data) do
168 if (v and v ~= userdata and v == '1') then
169 rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
170 task:set_pre_result('accept', 'ip matched with forward hosts')
171 end
172 end
173 end
174 end
175 table.insert(ip_check_table, 1, 'KEEP_SPAM')
176 local redis_ret_user = rspamd_redis_make_request(task,
177 redis_params, -- connect params
178 'KEEP_SPAM', -- hash key
179 false, -- is write
180 keep_spam_cb, --callback
181 'HMGET', -- command
182 ip_check_table -- arguments
183 )
184 if not redis_ret_user then
185 rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map")
186 end
187 end,
188 priority = 19
189})
190
191rspamd_config:register_symbol({
192 name = 'TLS_HEADER',
193 type = 'postfilter',
194 callback = function(task)
195 local rspamd_logger = require "rspamd_logger"
196 local tls_tag = task:get_request_header('TLS-Version')
197 if type(tls_tag) == 'nil' then
198 task:set_milter_reply({
199 add_headers = {['X-Last-TLS-Session-Version'] = 'None'}
200 })
201 else
202 task:set_milter_reply({
203 add_headers = {['X-Last-TLS-Session-Version'] = tostring(tls_tag)}
204 })
205 end
206 end,
207 priority = 12
208})
209
210rspamd_config:register_symbol({
211 name = 'TAG_MOO',
212 type = 'postfilter',
213 callback = function(task)
214 local util = require("rspamd_util")
215 local rspamd_logger = require "rspamd_logger"
216 local redis_params = rspamd_parse_redis_server('taghandler')
217 local rspamd_http = require "rspamd_http"
218 local rcpts = task:get_recipients('smtp')
219 local lua_util = require "lua_util"
220
221 local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
222 local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
223
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100224 local function remove_moo_tag()
225 local moo_tag_header = task:get_header('X-Moo-Tag', false)
226 if moo_tag_header then
227 task:set_milter_reply({
228 remove_headers = {['X-Moo-Tag'] = 0},
229 })
230 end
231 return true
232 end
233
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100234 if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
235 local tag = tagged_rcpt[1].options[1]
236 rspamd_logger.infox("found tag: %s", tag)
237 local action = task:get_metric_action('default')
238 rspamd_logger.infox("metric action now: %s", action)
239
240 if action ~= 'no action' and action ~= 'greylist' then
241 rspamd_logger.infox("skipping tag handler for action: %s", action)
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100242 remove_moo_tag()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100243 return true
244 end
245
246 local function http_callback(err_message, code, body, headers)
247 if body ~= nil and body ~= "" then
248 rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
249
250 local function tag_callback_subject(err, data)
251 if err or type(data) ~= 'string' then
252 rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
253
254 local function tag_callback_subfolder(err, data)
255 if err or type(data) ~= 'string' then
256 rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100257 remove_moo_tag()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100258 else
259 rspamd_logger.infox("Add X-Moo-Tag header")
260 task:set_milter_reply({
261 add_headers = {['X-Moo-Tag'] = 'YES'}
262 })
263 end
264 end
265
266 local redis_ret_subfolder = rspamd_redis_make_request(task,
267 redis_params, -- connect params
268 body, -- hash key
269 false, -- is write
270 tag_callback_subfolder, --callback
271 'HGET', -- command
272 {'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
273 )
274 if not redis_ret_subfolder then
275 rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100276 remove_moo_tag()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100277 end
278
279 else
280 rspamd_logger.infox("user wants subject modified for tagged mail")
281 local sbj = task:get_header('Subject')
282 new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
283 task:set_milter_reply({
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100284 remove_headers = {
285 ['Subject'] = 1,
286 ['X-Moo-Tag'] = 0
287 },
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100288 add_headers = {['Subject'] = new_sbj}
289 })
290 end
291 end
292
293 local redis_ret_subject = rspamd_redis_make_request(task,
294 redis_params, -- connect params
295 body, -- hash key
296 false, -- is write
297 tag_callback_subject, --callback
298 'HGET', -- command
299 {'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
300 )
301 if not redis_ret_subject then
302 rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100303 remove_moo_tag()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100304 end
305
306 end
307 end
308
309 if rcpts and #rcpts == 1 then
310 for _,rcpt in ipairs(rcpts) do
311 local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
312 if #rcpt_split == 2 then
313 if rcpt_split[1] == 'postmaster' then
314 rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100315 remove_moo_tag()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100316 else
317 rspamd_http.request({
318 task=task,
319 url='http://nginx:8081/aliasexp.php',
320 body='',
321 callback=http_callback,
322 headers={Rcpt=rcpt['addr']},
323 })
324 end
325 end
326 end
327 end
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100328 else
329 remove_moo_tag()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100330 end
331 end,
332 priority = 19
333})
334
335rspamd_config:register_symbol({
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200336 name = 'BCC',
337 type = 'postfilter',
338 callback = function(task)
339 local util = require("rspamd_util")
340 local rspamd_http = require "rspamd_http"
341 local rspamd_logger = require "rspamd_logger"
342
343 local from_table = {}
344 local rcpt_table = {}
345
346 if task:has_symbol('ENCRYPTED_CHAT') then
347 return -- stop
348 end
349
350 local send_mail = function(task, bcc_dest)
351 local lua_smtp = require "lua_smtp"
352 local function sendmail_cb(ret, err)
353 if not ret then
354 rspamd_logger.errx(task, 'BCC SMTP ERROR: %s', err)
355 else
356 rspamd_logger.infox(rspamd_config, "BCC SMTP SUCCESS TO %s", bcc_dest)
357 end
358 end
359 if not bcc_dest then
360 return -- stop
361 end
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100362 -- dot stuff content before sending
363 local email_content = tostring(task:get_content())
364 email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
365 -- send mail
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200366 lua_smtp.sendmail({
367 task = task,
368 host = os.getenv("IPV4_NETWORK") .. '.253',
369 port = 591,
370 from = task:get_from(stp)[1].addr,
371 recipients = bcc_dest,
372 helo = 'bcc',
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100373 timeout = 20,
374 }, email_content, sendmail_cb)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200375 end
376
377 -- determine from
378 local from = task:get_from('smtp')
379 if from then
380 for _, a in ipairs(from) do
381 table.insert(from_table, a['addr']) -- add this rcpt to table
382 table.insert(from_table, '@' .. a['domain']) -- add this rcpts domain to table
383 end
384 else
385 return -- stop
386 end
387
388 -- determine rcpts
389 local rcpts = task:get_recipients('smtp')
390 if rcpts then
391 for _, a in ipairs(rcpts) do
392 table.insert(rcpt_table, a['addr']) -- add this rcpt to table
393 table.insert(rcpt_table, '@' .. a['domain']) -- add this rcpts domain to table
394 end
395 else
396 return -- stop
397 end
398
399 local action = task:get_metric_action('default')
400 rspamd_logger.infox("metric action now: %s", action)
401
402 local function rcpt_callback(err_message, code, body, headers)
403 if err_message == nil and code == 201 and body ~= nil then
404 if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
405 send_mail(task, body)
406 end
407 end
408 end
409
410 local function from_callback(err_message, code, body, headers)
411 if err_message == nil and code == 201 and body ~= nil then
412 if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
413 send_mail(task, body)
414 end
415 end
416 end
417
418 if rcpt_table then
419 for _,e in ipairs(rcpt_table) do
420 rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
421 rspamd_http.request({
422 task=task,
423 url='http://nginx:8081/bcc.php',
424 body='',
425 callback=rcpt_callback,
426 headers={Rcpt=e}
427 })
428 end
429 end
430
431 if from_table then
432 for _,e in ipairs(from_table) do
433 rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
434 rspamd_http.request({
435 task=task,
436 url='http://nginx:8081/bcc.php',
437 body='',
438 callback=from_callback,
439 headers={From=e}
440 })
441 end
442 end
443
444 return true
445 end,
446 priority = 20
447})
448
449rspamd_config:register_symbol({
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100450 name = 'DYN_RL_CHECK',
451 type = 'prefilter',
452 callback = function(task)
453 local util = require("rspamd_util")
454 local redis_params = rspamd_parse_redis_server('dyn_rl')
455 local rspamd_logger = require "rspamd_logger"
456 local envfrom = task:get_from(1)
457 local uname = task:get_user()
458 if not envfrom or not uname then
459 return false
460 end
461 local uname = uname:lower()
462
463 local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
464
465 local function redis_cb_user(err, data)
466
467 if err or type(data) ~= 'string' then
468 rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for user %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying dynamic ratelimit for domain...", uname, data, err)
469
470 local function redis_key_cb_domain(err, data)
471 if err or type(data) ~= 'string' then
472 rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for domain %s returned invalid or empty data (\"%s\") or error (\"%s\")", env_from_domain, data, err)
473 else
474 rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for domain %s with value %s", env_from_domain, data)
475 task:insert_result('DYN_RL', 0.0, data, env_from_domain)
476 end
477 end
478
479 local redis_ret_domain = rspamd_redis_make_request(task,
480 redis_params, -- connect params
481 env_from_domain, -- hash key
482 false, -- is write
483 redis_key_cb_domain, --callback
484 'HGET', -- command
485 {'RL_VALUE', env_from_domain} -- arguments
486 )
487 if not redis_ret_domain then
488 rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for domain")
489 end
490 else
491 rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for user %s with value %s", uname, data)
492 task:insert_result('DYN_RL', 0.0, data, uname)
493 end
494
495 end
496
497 local redis_ret_user = rspamd_redis_make_request(task,
498 redis_params, -- connect params
499 uname, -- hash key
500 false, -- is write
501 redis_cb_user, --callback
502 'HGET', -- command
503 {'RL_VALUE', uname} -- arguments
504 )
505 if not redis_ret_user then
506 rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for user")
507 end
508 return true
509 end,
510 flags = 'empty',
511 priority = 20
512})
513
514rspamd_config:register_symbol({
515 name = 'NO_LOG_STAT',
516 type = 'postfilter',
517 callback = function(task)
518 local from = task:get_header('From')
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100519 if from and (monitoring_hosts:get_key(from) or from == "watchdog@localhost") then
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100520 task:set_flag('no_log')
521 task:set_flag('no_stat')
522 end
523 end
524})
Matthias Andreas Benkardd1f5b682023-11-18 13:18:30 +0100525
526rspamd_config:register_symbol({
527 name = 'MOO_FOOTER',
528 type = 'prefilter',
529 callback = function(task)
530 local lua_mime = require "lua_mime"
531 local lua_util = require "lua_util"
532 local rspamd_logger = require "rspamd_logger"
533 local rspamd_redis = require "rspamd_redis"
534 local ucl = require "ucl"
535 local redis_params = rspamd_parse_redis_server('footer')
536 local envfrom = task:get_from(1)
537 local uname = task:get_user()
538 if not envfrom or not uname then
539 return false
540 end
541 local uname = uname:lower()
542 local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
543
544 local function newline(task)
545 local t = task:get_newlines_type()
546
547 if t == 'cr' then
548 return '\r'
549 elseif t == 'lf' then
550 return '\n'
551 end
552
553 return '\r\n'
554 end
555 local function redis_cb_footer(err, data)
556 if err or type(data) ~= 'string' then
557 rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
558 else
559 -- parse json string
560 local parser = ucl.parser()
561 local res,err = parser:parse_string(data)
562 if not res then
563 rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
564 else
565 local footer = parser:get_object()
566
567 if footer and type(footer) == "table" and (footer.html or footer.plain) then
568 rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain)
569
570 local envfrom_mime = task:get_from(2)
571 local from_name = ""
572 if envfrom_mime and envfrom_mime[1].name then
573 from_name = envfrom_mime[1].name
574 elseif envfrom and envfrom[1].name then
575 from_name = envfrom[1].name
576 end
577
578 local replacements = {
579 auth_user = uname,
580 from_user = envfrom[1].user,
581 from_name = from_name,
582 from_addr = envfrom[1].addr,
583 from_domain = envfrom[1].domain:lower()
584 }
585 if footer.html then
586 footer.html = lua_util.jinja_template(footer.html, replacements, true)
587 end
588 if footer.plain then
589 footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
590 end
591
592 -- add footer
593 local out = {}
594 local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
595
596 local seen_cte
597 local newline_s = newline(task)
598
599 local function rewrite_ct_cb(name, hdr)
600 if rewrite.need_rewrite_ct then
601 if name:lower() == 'content-type' then
602 local nct = string.format('%s: %s/%s; charset=utf-8',
603 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
604 out[#out + 1] = nct
605 return
606 elseif name:lower() == 'content-transfer-encoding' then
607 out[#out + 1] = string.format('%s: %s',
608 'Content-Transfer-Encoding', 'quoted-printable')
609 seen_cte = true
610 return
611 end
612 end
613 out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
614 end
615
616 task:headers_foreach(rewrite_ct_cb, {full = true})
617
618 if not seen_cte and rewrite.need_rewrite_ct then
619 out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
620 end
621
622 -- End of headers
623 out[#out + 1] = newline_s
624
625 if rewrite.out then
626 for _,o in ipairs(rewrite.out) do
627 out[#out + 1] = o
628 end
629 else
630 out[#out + 1] = task:get_rawbody()
631 end
632 local out_parts = {}
633 for _,o in ipairs(out) do
634 if type(o) ~= 'table' then
635 out_parts[#out_parts + 1] = o
636 out_parts[#out_parts + 1] = newline_s
637 else
638 out_parts[#out_parts + 1] = o[1]
639 if o[2] then
640 out_parts[#out_parts + 1] = newline_s
641 end
642 end
643 end
644 task:set_message(out_parts)
645 else
646 rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
647 end
648 end
649 end
650 end
651
652 local redis_ret_footer = rspamd_redis_make_request(task,
653 redis_params, -- connect params
654 env_from_domain, -- hash key
655 false, -- is write
656 redis_cb_footer, --callback
657 'HGET', -- command
658 {"DOMAIN_WIDE_FOOTER", env_from_domain} -- arguments
659 )
660 if not redis_ret_footer then
661 rspamd_logger.infox(rspamd_config, "cannot make request to load footer for domain")
662 end
663
664 return true
665 end,
666 priority = 1
667})