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

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