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.
@@ -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: []