aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/lua/emails.lua
blob: 850cc51c011c73b4a9e3fcf10e5d36d74b23a590 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
--[[
Copyright (c) 2011-2017, Vsevolod Stakhov <vsevolod@highsecure.ru>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--

-- Emails is module for different checks for emails inside messages

if confighelp then
  return
end

-- Rules format:
-- symbol = sym, map = file:///path/to/file, domain_only = yes
-- symbol = sym2, dnsbl = bl.somehost.com, domain_only = no
local rules = {}
local logger = require "rspamd_logger"
local hash = require "rspamd_cryptobox_hash"
local rspamd_lua_utils = require "lua_util"
local util = require "rspamd_util"
local lua_maps = require "lua_maps"
local lua_maps_expressions = require "lua_maps_expressions"
local N = "emails"

-- TODO: move this into common part

-- Check rule for a single email
local function check_email_rule(task, rule, addr)
  if rule['whitelist'] then
    if rule['whitelist']:get_key(addr.addr)
        or rule['whitelist']:get_key(addr.domain) then
      logger.debugm(N, task, "whitelisted address: %s", addr.addr)
      return
    end
  elseif rule.whitelist_expr then
    if rule['whitelist']:process(task) then
      logger.debugm(N, task, "whitelisted emails processing: %s", addr.addr)
      return
    end
  end

  if rule['dnsbl'] then
      local email
      local to_resolve

      if rule['domain_only'] then
        email = addr.domain
      else
        email = string.format('%s%s%s', addr.user, rule.delimiter, addr.domain)
      end

      email = email:lower()

      local function emails_dns_cb(_, _, results, err)
        if err and (err ~= 'requested record is not found'
            and err ~= 'no records with this name') then
          logger.errx(task, 'Error querying DNS(%s.%s): %s', to_resolve,
              rule['dnsbl'], err)
        elseif results then
          local expected_found = false
          local symbol = rule['symbol']

          local function check_ip(ip)
            for _,result in ipairs(results) do
              local ipstr = result:to_string()
              if ipstr == ip then
                return true
              end
            end

            return false
          end

          if rule['expect_ip'] then
            if check_ip(rule['expect_ip']) then
              expected_found = true
            end
          else
            expected_found = true -- Accept any result
          end

          if rule['returncodes'] then
            for k,codes in pairs(rule['returncodes']) do
              if type(codes) == 'table' then
                for _,code in ipairs(codes) do
                  if check_ip(code) then
                    expected_found = true
                    symbol = k
                    break
                  end
                end
              else
                if check_ip(codes) then
                  expected_found = true
                  symbol = k
                  break
                end
              end
            end
          end

          if expected_found then
            if rule['hash'] then
              task:insert_result(symbol, 1.0, {email, to_resolve})
            else
              task:insert_result(symbol, 1.0, email)
            end
          end

        end
      end

      logger.debugm(N, task, "check %s on %s", email, rule['dnsbl'])

      if rule['hash'] then
        local hkey = hash.create_specific(rule['hash'], email)

        if rule['encoding'] == 'base32' then
          to_resolve = hkey:base32()
        else
          to_resolve = hkey:hex()
        end

        if rule['hashlen'] and type(rule['hashlen']) == 'number' then
          if #to_resolve > rule['hashlen'] then
            to_resolve = string.sub(to_resolve, 1, rule['hashlen'])
          end
        end
      else
        to_resolve = email
      end

      local dns_arg = string.format('%s.%s', to_resolve, rule['dnsbl'])

      logger.debugm(N, task, "query %s", dns_arg)

      task:get_resolver():resolve_a({
        task=task,
        name = dns_arg,
        callback = emails_dns_cb})
    elseif rule['map'] then
      if rule['domain_only'] then
        local key = addr.domain
        if rule['map']:get_key(key) then
          task:insert_result(rule['symbol'], 1.0, key)
          logger.debugm(N, task, 'email: \'%s\' is found in list: %s',
              key, rule['symbol'])
        end
      else
        local key = string.format('%s%s%s', addr.user, rule.delimiter, addr.domain)
        if rule['map']:get_key(key) then
          task:insert_result(rule['symbol'], 1.0, key)
          logger.debugm(N, task, 'email: \'%s\' is found in list: %s',
              key, rule['symbol'])
        end
      end
    end
  end

-- Check email
local function gen_check_emails(rule)
  return function(task)
    local emails = task:get_emails()
    local checked = {}
    if emails and not rule.skip_body then
      for _,addr in ipairs(emails) do
        local user_part = addr:get_user()
        local domain = addr:get_host()

        if (user_part and #user_part > 0) and (domain and #domain > 0) then
          local to_check = string.format('%s%s%s', addr:get_user(),
              rule.delimiter, addr:get_host())
          local naddr = {
            user = (addr:get_user() or ''):lower(),
            domain = (addr:get_host() or ''):lower(),
            addr = to_check:lower()
          }

          rspamd_lua_utils.remove_email_aliases(naddr)

          if not checked[naddr.addr] then
            check_email_rule(task, rule, naddr)
            checked[naddr.addr] = true
          end
        end
      end
    end

    if rule.check_replyto then
      local function get_raw_header(name)
        return ((task:get_header_full(name) or {})[1] or {})['value']
      end

      local replyto = get_raw_header('Reply-To')
      if replyto then
        local rt = util.parse_mail_address(replyto, task:get_mempool())

        if rt and rt[1] and (rt[1].addr and #rt[1].addr > 0) then
          rspamd_lua_utils.remove_email_aliases(rt[1])
          rt[1].addr = rt[1].addr:lower()
          if not checked[rt[1].addr] then
            check_email_rule(task, rule, rt[1])
            checked[rt[1].addr] = true
          end
        end
      end
    end
  end
end

local opts = rspamd_config:get_module_opt('emails', 'rules')
if opts and type(opts) == 'table' then
  for k,v in pairs(opts) do
    local rule = v
    if not rule['symbol'] then
      rule['symbol'] = k
    end

    if not rule['delimiter'] then
      rule['delimiter'] = "@"
    end

    if rule['whitelist'] then
      if type(rule['whitelist']) == 'string' then
        rule['whitelist'] = lua_maps.map_add_from_ucl(rule.whitelist,
            'set', 'Emails rule %s whitelist', rule['symbol'])
      else
        rule.whitelist_expr = lua_maps_expressions.create(rspamd_config,
            rule.whitelist, N)
        rule.whitelist = nil
      end
    end

    if rule['map'] then
      rule['name'] = rule['map']
      rule.map = lua_maps.map_add_from_ucl(rule.map,
          'regexp', 'Emails rule %s whitelist', rule['symbol'])
    end
    if not rule['symbol'] or (not rule['map'] and not rule['dnsbl']) then
      logger.errx(rspamd_config, 'incomplete rule: %s', rule)
    else
      table.insert(rules, rule)
      logger.infox(rspamd_config, 'add emails rule %s',
        rule['dnsbl'] or rule['name'] or '???')
    end
  end
end

if #rules > 0 then
  for _,rule in ipairs(rules) do
    local cb = gen_check_emails(rule)
    local id = rspamd_config:register_symbol({
      name = rule['symbol'],
      callback = cb,
    })

    if rule.returncodes then
      for k,_ in pairs(rule.returncodes) do
        if k ~= rule['symbol'] then
          rspamd_config:register_symbol({
            name = k,
            parent = id,
            type = 'virtual'
          })
        end
      end
    end
  end
else
  rspamd_lua_utils.disable_module(N, "conf")
end