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.

forwarding.lua 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  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. -- Rules to detect forwarding
  14. local rspamd_util = require "rspamd_util"
  15. rspamd_config.FWD_GOOGLE = {
  16. callback = function (task)
  17. if not (task:has_from(1) and task:has_recipients(1)) then
  18. return false
  19. end
  20. local envfrom = task:get_from{'smtp', 'orig'}
  21. local envrcpts = task:get_recipients(1)
  22. -- Forwarding will only be to a single recipient
  23. if #envrcpts > 1 then return false end
  24. -- Get recipient and compute VERP address
  25. local rcpt = envrcpts[1].addr:lower()
  26. local verp = rcpt:gsub('@','=')
  27. -- Get the user portion of the envfrom
  28. local ef_user = envfrom[1].user:lower()
  29. -- Check for a match
  30. if ef_user:find('+caf_=' .. verp, 1, true) then
  31. local _,_,user = ef_user:find('^(.+)+caf_=')
  32. if user then
  33. user = user .. '@' .. envfrom[1].domain
  34. return true, user
  35. end
  36. end
  37. return false
  38. end,
  39. score = 0.0,
  40. description = "Message was forwarded by Google",
  41. group = "forwarding"
  42. }
  43. rspamd_config.FWD_YANDEX = {
  44. callback = function (task)
  45. if not (task:has_from(1) and task:has_recipients(1)) then
  46. return false
  47. end
  48. local hostname = task:get_hostname()
  49. if hostname and hostname:lower():find('%.yandex%.[a-z]+$') then
  50. return task:has_header('X-Yandex-Forward')
  51. end
  52. return false
  53. end,
  54. score = 0.0,
  55. description = "Message was forwarded by Yandex",
  56. group = "forwarding"
  57. }
  58. rspamd_config.FWD_MAILRU = {
  59. callback = function (task)
  60. if not (task:has_from(1) and task:has_recipients(1)) then
  61. return false
  62. end
  63. local hostname = task:get_hostname()
  64. if hostname and hostname:lower():find('%.mail%.ru$') then
  65. return task:has_header('X-MailRu-Forward')
  66. end
  67. return false
  68. end,
  69. score = 0.0,
  70. description = "Message was forwarded by Mail.ru",
  71. group = "forwarding"
  72. }
  73. rspamd_config.FWD_SRS = {
  74. callback = function (task)
  75. if not (task:has_from(1) and task:has_recipients(1)) then
  76. return false
  77. end
  78. local envfrom = task:get_from(1)
  79. local envrcpts = task:get_recipients(1)
  80. -- Forwarding is only to a single recipient
  81. if #envrcpts > 1 then return false end
  82. -- Get recipient and compute rewritten SRS address
  83. local srs = '=' .. envrcpts[1].domain:lower() ..
  84. '=' .. envrcpts[1].user:lower()
  85. if envfrom[1].user:lower():find('^srs[01]=') and
  86. envfrom[1].user:lower():find(srs, 1, false)
  87. then
  88. return true
  89. end
  90. return false
  91. end,
  92. score = 0.0,
  93. description = "Message was forwarded using Sender Rewriting Scheme (SRS)",
  94. group = "forwarding"
  95. }
  96. rspamd_config.FORWARDED = {
  97. callback = function (task)
  98. local function normalize_addr(addr)
  99. addr = string.match(addr, '^<?([^>]*)>?$') or addr
  100. local cap, _,domain = string.match(addr, '^([^%+][^%+]*)(%+[^@]*)@(.*)$')
  101. if cap then
  102. addr = string.format('%s@%s', cap, domain)
  103. end
  104. return addr
  105. end
  106. if not task:has_recipients(1) or not task:has_recipients(2) then return false end
  107. local envrcpts = task:get_recipients(1)
  108. -- Forwarding will only be for single recipient messages
  109. if #envrcpts > 1 then return false end
  110. -- Get any other headers we might need
  111. local has_list_unsub = task:has_header('List-Unsubscribe')
  112. local to = task:get_recipients(2)
  113. local matches = 0
  114. -- Retrieve and loop through all Received headers
  115. local rcvds = task:get_received_headers()
  116. if rcvds then
  117. for _, rcvd in ipairs(rcvds) do
  118. local addr = rcvd['for']
  119. if addr then
  120. addr = normalize_addr(addr)
  121. matches = matches + 1
  122. -- Check that it doesn't match the envrcpt
  123. if not rspamd_util.strequal_caseless(addr, envrcpts[1].addr) then
  124. -- Check for mailing-lists as they will have the same signature
  125. if matches < 2 and has_list_unsub and to and rspamd_util.strequal_caseless(to[1].addr, addr) then
  126. return false
  127. else
  128. return true, 1.0, addr
  129. end
  130. end
  131. -- Prevent any other iterations as we only want
  132. -- process the first matching Received header
  133. return false
  134. end
  135. end
  136. end
  137. return false
  138. end,
  139. score = 0.0,
  140. description = "Message was forwarded",
  141. group = "forwarding"
  142. }