discourse_mail_receiver 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|