discourse_mail_receiver 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/mail_receiver/discourse_mail_receiver.rb +58 -0
- data/lib/mail_receiver/fast_rejection.rb +144 -0
- data/lib/mail_receiver/mail.rb +10 -0
- data/lib/mail_receiver/mail_receiver_base.rb +42 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 532ee9db86fe12040fa11fa377241c2a56d45c00bac101517aa7f3a8f572031b
|
4
|
+
data.tar.gz: 438ef4054e93f209531c23162ceab7c2ad79c3c6561274eda962b313d97dd386
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9ac58c1df1f8f94000496d385ccad8d81f4a2aa22c356fccc6c16944011bb1742abeaf28f70496df6000cbb7e932c6e122920a8c20e9b3fe5ba69358e17566d0
|
7
|
+
data.tar.gz: 5fd6014a1e401bf23e1aece57e95c4f4bfae9a36f8059d3a9acbbc3e6a1fc09170f2832d8a4453e82ebd6b9f770d55cc4401f17cf2e18a3cab7f5f6cc0bd071c
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'syslog'
|
2
|
+
require 'json'
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
require_relative 'mail_receiver_base'
|
6
|
+
|
7
|
+
class DiscourseMailReceiver < MailReceiverBase
|
8
|
+
|
9
|
+
def initialize(env_file = nil, recipient = nil, mail = nil)
|
10
|
+
super(env_file)
|
11
|
+
|
12
|
+
@recipient = recipient
|
13
|
+
@mail = mail
|
14
|
+
|
15
|
+
logger.debug "Recipient: #{@recipient}"
|
16
|
+
fatal "No recipient passed on command line." unless @recipient
|
17
|
+
fatal "No message passed on stdin." if @mail.nil? || @mail.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def endpoint
|
21
|
+
return @endpoint if @endpoint
|
22
|
+
|
23
|
+
@endpoint = @env["DISCOURSE_MAIL_ENDPOINT"]
|
24
|
+
|
25
|
+
if @env['DISCOURSE_BASE_URL']
|
26
|
+
@endpoint = "#{@env['DISCOURSE_BASE_URL']}/admin/email/handle_mail"
|
27
|
+
end
|
28
|
+
@endpoint
|
29
|
+
end
|
30
|
+
|
31
|
+
def process
|
32
|
+
uri = URI.parse(endpoint)
|
33
|
+
|
34
|
+
begin
|
35
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
36
|
+
http.use_ssl = uri.scheme == "https"
|
37
|
+
post = Net::HTTP::Post.new(uri.request_uri)
|
38
|
+
post["Api-Username"] = username
|
39
|
+
post["Api-Key"] = key
|
40
|
+
post.set_form_data(email: @mail)
|
41
|
+
|
42
|
+
response = http.request(post)
|
43
|
+
rescue StandardError => ex
|
44
|
+
logger.err "Failed to POST the e-mail to %s: %s (%s)", endpoint, ex.message, ex.class
|
45
|
+
logger.err ex.backtrace.map { |l| " #{l}" }.join("\n")
|
46
|
+
|
47
|
+
return :failure
|
48
|
+
ensure
|
49
|
+
http.finish if http && http.started?
|
50
|
+
end
|
51
|
+
|
52
|
+
return :success if Net::HTTPSuccess === response
|
53
|
+
|
54
|
+
logger.err "Failed to POST the e-mail to %s: %s", endpoint, response.code
|
55
|
+
:failure
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'syslog'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'cgi'
|
6
|
+
require 'net/http'
|
7
|
+
|
8
|
+
require_relative 'mail'
|
9
|
+
require_relative 'mail_receiver_base'
|
10
|
+
|
11
|
+
class FastRejection < MailReceiverBase
|
12
|
+
|
13
|
+
def initialize(env_file)
|
14
|
+
super(env_file)
|
15
|
+
|
16
|
+
@disabled = @env['DISCOURSE_FAST_REJECTION_DISABLED'] || !@env['DISCOURSE_BASE_URL']
|
17
|
+
|
18
|
+
@blacklisted_sender_domains = @env.fetch('BLACKLISTED_SENDER_DOMAINS', "").split(" ").map(&:downcase).to_set
|
19
|
+
end
|
20
|
+
|
21
|
+
def disabled?
|
22
|
+
!!@disabled
|
23
|
+
end
|
24
|
+
|
25
|
+
def process
|
26
|
+
$stdout.sync = true # unbuffered output
|
27
|
+
|
28
|
+
args = {}
|
29
|
+
while line = gets
|
30
|
+
# Fill up args with the request details.
|
31
|
+
line = line.chomp
|
32
|
+
if line.empty?
|
33
|
+
puts "action=#{process_single_request(args)}"
|
34
|
+
puts ''
|
35
|
+
|
36
|
+
args = {} # reset for next request.
|
37
|
+
else
|
38
|
+
k, v = line.chomp.split('=', 2)
|
39
|
+
args[k] = v
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_single_request(args)
|
45
|
+
return 'dunno' if disabled?
|
46
|
+
|
47
|
+
if args['request'] != 'smtpd_access_policy'
|
48
|
+
return 'defer_if_permit Internal error, Request type invalid'
|
49
|
+
elsif args['protocol_state'] != 'RCPT'
|
50
|
+
return 'dunno'
|
51
|
+
elsif args['sender'].nil?
|
52
|
+
# Note that while this key should always exist, its value may be the empty
|
53
|
+
# string. Postfix will convert the "<>" null sender to "".
|
54
|
+
return 'defer_if_permit No sender specified'
|
55
|
+
elsif args['recipient'].nil?
|
56
|
+
return 'defer_if_permit No recipient specified'
|
57
|
+
end
|
58
|
+
|
59
|
+
run_filters(args)
|
60
|
+
end
|
61
|
+
|
62
|
+
def maybe_reject_email(from, to)
|
63
|
+
uri = URI.parse(endpoint)
|
64
|
+
fromarg = CGI::escape(from)
|
65
|
+
toarg = CGI::escape(to)
|
66
|
+
|
67
|
+
qs = "from=#{fromarg}&to=#{toarg}"
|
68
|
+
if uri.query && !uri.query.empty?
|
69
|
+
uri.query += "&#{qs}"
|
70
|
+
else
|
71
|
+
uri.query = qs
|
72
|
+
end
|
73
|
+
|
74
|
+
begin
|
75
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
76
|
+
http.use_ssl = uri.scheme == "https"
|
77
|
+
get = Net::HTTP::Get.new(uri.request_uri)
|
78
|
+
get["Api-Username"] = username
|
79
|
+
get["Api-Key"] = key
|
80
|
+
response = http.request(get)
|
81
|
+
rescue StandardError => ex
|
82
|
+
logger.err "Failed to GET smtp_should_reject answer from %s: %s (%s)", endpoint, ex.message, ex.class
|
83
|
+
logger.err ex.backtrace.map { |l| " #{l}" }.join("\n")
|
84
|
+
return "defer_if_permit Internal error, API request preparation failed"
|
85
|
+
ensure
|
86
|
+
http.finish if http && http.started?
|
87
|
+
end
|
88
|
+
|
89
|
+
if Net::HTTPSuccess === response
|
90
|
+
reply = JSON.parse(response.body)
|
91
|
+
if reply['reject']
|
92
|
+
return "reject #{reply['reason']}"
|
93
|
+
end
|
94
|
+
else
|
95
|
+
logger.err "Failed to GET smtp_should_reject answer from %s: %s", endpoint, response.code
|
96
|
+
return "defer_if_permit Internal error, API request failed"
|
97
|
+
end
|
98
|
+
|
99
|
+
"dunno" # let future tests also be allowed to reject this one.
|
100
|
+
end
|
101
|
+
|
102
|
+
def endpoint
|
103
|
+
"#{@env['DISCOURSE_BASE_URL']}/admin/email/smtp_should_reject.json"
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def run_filters(args)
|
109
|
+
filters = [
|
110
|
+
:maybe_reject_by_sender_domain,
|
111
|
+
:maybe_reject_by_api,
|
112
|
+
]
|
113
|
+
|
114
|
+
filters.each do |f|
|
115
|
+
action = send(f, args)
|
116
|
+
return action if action != "dunno"
|
117
|
+
end
|
118
|
+
|
119
|
+
"dunno"
|
120
|
+
end
|
121
|
+
|
122
|
+
def maybe_reject_by_sender_domain(args)
|
123
|
+
sender = args['sender']
|
124
|
+
|
125
|
+
return "dunno" if sender.empty?
|
126
|
+
|
127
|
+
domain = domain_from_addrspec(sender)
|
128
|
+
if domain.empty?
|
129
|
+
logger.info("deferred mail with domainless sender #{sender}")
|
130
|
+
return 'defer_if_permit Invalid sender'
|
131
|
+
end
|
132
|
+
if @blacklisted_sender_domains.include? domain
|
133
|
+
logger.info("rejected mail from blacklisted sender domain #{domain} (from #{sender})")
|
134
|
+
return 'reject Invalid sender'
|
135
|
+
end
|
136
|
+
|
137
|
+
"dunno"
|
138
|
+
end
|
139
|
+
|
140
|
+
def maybe_reject_by_api(args)
|
141
|
+
maybe_reject_email(args['sender'], args['recipient'])
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Parse and return the domain from an Internet addr-spec. Return value is
|
2
|
+
# normalised to lowercase.
|
3
|
+
#
|
4
|
+
# This implementation is the simplest thing that could possibly work. Do
|
5
|
+
# not use this function to parse generalised mailbox or group addresses.
|
6
|
+
#
|
7
|
+
# See section 3.4 of RFC 2822.
|
8
|
+
def domain_from_addrspec(addrspec)
|
9
|
+
(addrspec.split("@", 2)[1] || "").downcase
|
10
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class MailReceiverBase
|
2
|
+
class ReceiverException < StandardError; end;
|
3
|
+
|
4
|
+
attr_reader :env
|
5
|
+
|
6
|
+
def initialize(env_file)
|
7
|
+
unless File.exists?(env_file)
|
8
|
+
fatal "Config file %s does not exist. Aborting.", env_file
|
9
|
+
end
|
10
|
+
|
11
|
+
@env = JSON.parse(File.read(env_file))
|
12
|
+
|
13
|
+
%w{DISCOURSE_API_KEY DISCOURSE_API_USERNAME}.each do |kw|
|
14
|
+
fatal "env var %s is required", kw unless @env[kw]
|
15
|
+
end
|
16
|
+
|
17
|
+
if @env['DISCOURSE_MAIL_ENDPOINT'].nil? && @env['DISCOURSE_BASE_URL'].nil?
|
18
|
+
fatal "DISCOURSE_MAIL_ENDPOINT and DISCOURSE_BASE_URL env var missing"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.logger
|
23
|
+
@logger ||= Syslog.open(File.basename($0), Syslog::LOG_PID, Syslog::LOG_MAIL)
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
MailReceiverBase.logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def key
|
31
|
+
@env['DISCOURSE_API_KEY']
|
32
|
+
end
|
33
|
+
|
34
|
+
def username
|
35
|
+
@env['DISCOURSE_API_USERNAME']
|
36
|
+
end
|
37
|
+
|
38
|
+
def fatal(*args)
|
39
|
+
logger.crit(*args)
|
40
|
+
raise ReceiverException.new(sprintf(*args))
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: discourse_mail_receiver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Discourse Team
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-08-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mail
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.7.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.7.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mail
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.7.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.7.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
description: A gem used to package the core .rb files of the mail-receiver.
|
56
|
+
email:
|
57
|
+
- team@discourse.org
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- lib/mail_receiver/discourse_mail_receiver.rb
|
63
|
+
- lib/mail_receiver/fast_rejection.rb
|
64
|
+
- lib/mail_receiver/mail.rb
|
65
|
+
- lib/mail_receiver/mail_receiver_base.rb
|
66
|
+
homepage: https://github.com/discourse/mail-receiver
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 2.4.0
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubygems_version: 3.0.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: A gem used to package the core .rb files of the mail-receiver.
|
89
|
+
test_files: []
|