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 status',
  25. 'delivery warning',
  26. 'failure delivery',
  27. 'failure notice',
  28. "hasn't been delivered",
  29. 'mail failure',
  30. 'returned mail',
  31. 'undeliverable',
  32. 'undelivered',
  33. }
  34. return string.format([[Subject=/\b(%s)\b/i{header}]], table.concat(keywords, '|'))
  35. end
  36. config.regexp.SUBJ_BOUNCE_WORDS = {
  37. re = make_subj_bounce_keywords_re(),
  38. group = 'headers',
  39. score = 0.0,
  40. description = 'Words/phrases typical for DSN'
  41. }
  42. rspamd_config.BOUNCE = {
  43. callback = function(task)
  44. local from = task:get_from('smtp')
  45. if from and from[1].addr ~= '' then
  46. -- RFC 3464:
  47. -- Whenever an SMTP transaction is used to send a DSN, the MAIL FROM
  48. -- command MUST use a NULL return address, i.e., "MAIL FROM:<>"
  49. -- In practise it is almost always the case for DSN
  50. return false
  51. end
  52. local parts = task:get_parts()
  53. local top_type, top_subtype, params = parts[1]:get_type_full()
  54. -- RFC 3464, RFC 8098
  55. if top_type == 'multipart' and top_subtype == 'report' and params and
  56. (params['report-type'] == 'delivery-status' or params['report-type'] == 'disposition-notification') then
  57. -- Assume that inner parts are OK, don't check them to save time
  58. return true, 1.0, 'DSN'
  59. end
  60. -- Apply heuristics for non-standard bounces
  61. local bounce_sender
  62. local mime_from = task:get_from('mime')
  63. if mime_from then
  64. local from_user = mime_from[1].user:lower()
  65. -- Check common bounce senders
  66. if (from_user == 'postmaster' or from_user == 'mailer-daemon') then
  67. bounce_sender = from_user
  68. -- MDaemon >= 14.5 sends multipart/report (RFC 3464) DSN covered above,
  69. -- but older versions send non-standard bounces with localized subjects and they
  70. -- are still around
  71. elseif from_user == 'mdaemon' and task:has_header('X-MDDSN-Message') then
  72. return true, 1.0, 'MDaemon'
  73. end
  74. end
  75. local subj_keywords = task:has_symbol('SUBJ_BOUNCE_WORDS')
  76. if not (bounce_sender or subj_keywords) then
  77. return false
  78. end
  79. if bounce_sender and subj_keywords then
  80. return true, 0.5, bounce_sender .. '+subj'
  81. end
  82. -- Look for a message/rfc822(-headers) part inside
  83. local rfc822_part
  84. parts[10] = nil -- limit number of parts to check
  85. for _, p in ipairs(parts) do
  86. local mime_type, mime_subtype = p:get_type()
  87. if (mime_subtype == 'rfc822' or mime_subtype == 'rfc822-headers') and
  88. (mime_type == 'message' or mime_type == 'text') then
  89. rfc822_part = mime_type .. '/' .. mime_subtype
  90. break
  91. end
  92. end
  93. if rfc822_part and bounce_sender then
  94. return true, 0.5, bounce_sender .. '+' .. rfc822_part
  95. elseif rfc822_part and subj_keywords then
  96. return true, 0.2, rfc822_part .. '+subj'
  97. end
  98. end,
  99. description = '(Non) Delivery Status Notification',
  100. group = 'headers',
  101. }
  102. rspamd_config:register_dependency('BOUNCE', 'SUBJ_BOUNCE_WORDS')