discourse_mail_receiver 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []