Skip to content

Commit a1277ba

Browse files
committed
feat: support for using rspamd for spam filtering
1 parent 724325a commit a1277ba

File tree

4 files changed

+86
-1
lines changed

4 files changed

+86
-1
lines changed

config/postal.defaults.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ rails:
102102
environment: production
103103
secret_key:
104104

105+
rspamd:
106+
enabled: false
107+
host: 127.0.0.1
108+
port: 11334
109+
ssl: false
110+
password: null
111+
flags: null
112+
105113
spamd:
106114
enabled: false
107115
host: 127.0.0.1

lib/postal/message_inspector.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ class << self
2222
def inspectors
2323
Array.new.tap do |inspectors|
2424

25-
if Postal.config.spamd&.enabled
25+
if Postal.config.rspamd&.enabled
26+
inspectors << MessageInspectors::Rspamd.new(Postal.config.rspamd)
27+
elsif Postal.config.spamd&.enabled
2628
inspectors << MessageInspectors::SpamAssassin.new(Postal.config.spamd)
2729
end
2830

lib/postal/message_inspectors.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module MessageInspectors
33
extend ActiveSupport::Autoload
44
eager_autoload do
55
autoload :Clamav
6+
autoload :Rspamd
67
autoload :SpamAssassin
78
end
89
end
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
require 'net/http'
2+
3+
module Postal
4+
module MessageInspectors
5+
class Rspamd < MessageInspector
6+
7+
class Error < StandardError
8+
end
9+
10+
def inspect_message(inspection)
11+
response = request(inspection.message, inspection.scope)
12+
response = JSON.parse(response.body)
13+
return unless response['symbols'].is_a?(Hash)
14+
15+
response['symbols'].values.each do |symbol|
16+
next if symbol['description'].blank?
17+
18+
inspection.spam_checks << SpamCheck.new(symbol['name'], symbol['score'], symbol['description'])
19+
end
20+
rescue Error => e
21+
inspection.spam_checks << SpamCheck.new("ERROR", 0, e.message)
22+
end
23+
24+
private
25+
26+
def request(message, scope)
27+
http = Net::HTTP.new(@config.host, @config.port)
28+
http.use_ssl = true if @config.ssl
29+
http.read_timeout = 10
30+
http.open_timeout = 10
31+
32+
raw_message = message.raw_message
33+
34+
request = Net::HTTP::Post.new('/checkv2')
35+
request.body = raw_message
36+
request['Content-Length'] = raw_message.bytesize.to_s
37+
request['Password'] = @config.password if @config.password
38+
request['Flags'] = @config.flags if @config.flags
39+
request['User-Agent'] = 'Postal'
40+
request['Deliver-To'] = message.rcpt_to
41+
request['From'] = message.mail_from
42+
request['Rcpt'] = message.rcpt_to
43+
request['Queue-Id'] = message.token
44+
45+
if scope == :outgoing
46+
request['User'] = ''
47+
# We don't actually know the IP but an empty input here will
48+
# still trigger rspamd to treat this as an outbound email
49+
# and disable certain checks.
50+
# https://rspamd.com/doc/tutorials/scanning_outbound.html
51+
request['Ip'] = ''
52+
end
53+
54+
response = nil
55+
begin
56+
response = http.request(request)
57+
rescue Exception => e
58+
logger.error "Error talking to rspamd: #{e.class} (#{e.message})"
59+
logger.error e.backtrace[0,5]
60+
61+
raise Error, "Error when scanning with rspamd (#{e.class})"
62+
end
63+
64+
unless response.is_a?(Net::HTTPOK)
65+
logger.info "Got #{response.code} status from rspamd, wanted 200"
66+
raise Error, "Error when scanning with rspamd (got #{response.code})"
67+
end
68+
69+
response
70+
end
71+
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)