You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

bounce.lua 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. --[[
  2. Copyright (c) 2020, Anton Yuzhaninov <citrin@citrin.ru>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. -- Rule to detect bounces:
  14. -- RFC 3464 Delivery status notifications and most common non-standard ones
  15. local function make_subj_bounce_keywords_re()
  16. -- Words and phrases commonly used in Subjects for bounces
  17. -- We cannot practically test all localized Subjects, but luckily English is by far the most common here
  18. local keywords = {
  19. 'could not send message',
  20. "couldn't be delivered",
  21. 'delivery failed',
  22. 'delivery failure',
  23. 'delivery report',
  24. 'delivery warning',
  25. 'failure delivery',
  26. 'failure notice',
  27. "hasn't been delivered",
  28. 'mail failure',
  29. 'returned mail',
  30. 'undeliverable',
  31. 'undelivered',
  32. }
  33. return string.format([[Subject=/\b(%s)\b/i{header}]], table.concat(keywords, '|'))
  34. end
  35. config.regexp.SUBJ_BOUNCE_WORDS = {
  36. re = make_subj_bounce_keywords_re(),
  37. group = 'headers',
  38. score = 0.0,
  39. description = 'Words/phrases typical for DNS'
  40. }
  41. rspamd_config.BOUNCE = {
  42. callback = function(task)
  43. local from = task:get_from('smtp')
  44. if from and from[1].addr ~= '' then
  45. -- RFC 3464:
  46. -- Whenever an SMTP transaction is used to send a DSN, the MAIL FROM
  47. -- command MUST use a NULL return address, i.e., "MAIL FROM:<>"
  48. -- In practise it is almost always the case for DNS
  49. return false
  50. end
  51. local parts = task:get_parts()
  52. local top_type, top_subtype, params = parts[1]:get_type_full()
  53. -- RFC 3464, RFC 8098
  54. if top_type == 'multipart' and top_subtype == 'report' and params and
  55. (params['report-type'] == 'delivery-status' or params['report-type'] == 'disposition-notification') then
  56. -- Assume that inner parts are OK, don't check them to save time
  57. return true, 1.0, 'DSN'
  58. end
  59. -- Apply heuristics for non-standard bounecs
  60. local bounce_sender
  61. local mime_from = task:get_from('mime')
  62. if mime_from then
  63. local from_user = mime_from[1].user:lower()
  64. -- Check common bounce senders
  65. if (from_user == 'postmaster' or from_user == 'mailer-daemon') then
  66. bounce_sender = from_user
  67. -- MDaemon >= 14.5 sends multipart/report (RFC 3464) DNS covered above,
  68. -- but older versions send non-standard bounces with localized subjects and they
  69. -- are still around
  70. elseif from_user == 'mdaemon' and task:has_header('X-MDDSN-Message') then
  71. return true, 1.0, 'MDaemon'
  72. end
  73. end
  74. local subj_keywords = task:has_symbol('SUBJ_BOUNCE_WORDS')
  75. if not (bounce_sender or subj_keywords) then
  76. return false
  77. end
  78. if bounce_sender and subj_keywords then
  79. return true, 0.5, bounce_sender .. '+subj'
  80. end
  81. -- Look for a message/rfc822(-headers) part inside
  82. local rfc822_part
  83. parts[10] = nil -- limit numbe of parts to check
  84. for _, p in ipairs(parts) do
  85. local mime_type, mime_subtype = p:get_type()
  86. if (mime_subtype == 'rfc822' or mime_subtype == 'rfc822-headers') and
  87. (mime_type == 'message' or mime_type == 'text') then
  88. rfc822_part = mime_type .. '/' .. mime_subtype
  89. break
  90. end
  91. end
  92. if rfc822_part and bounce_sender then
  93. return true, 0.5, bounce_sender .. '+' .. rfc822_part
  94. elseif rfc822_part and subj_keywords then
  95. return true, 0.2, rfc822_part .. '+subj'
  96. end
  97. end,
  98. description = '(Non) Delivery Status Notification',
  99. group = 'headers',
  100. }
  101. rspamd_config:register_dependency('BOUNCE', 'SUBJ_BOUNCE_WORDS')