diff --git a/Dockerfile b/Dockerfile index 61be009..181a1d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,6 @@ RUN >/etc/postfix/main.cf \ fi \ && postconf -M -e 'smtp/inet=smtp inet n - n - - smtpd' \ && postconf -M -e 'discourse/unix=discourse unix - n n - - pipe user=nobody:nogroup argv=/usr/local/bin/receive-mail ${recipient}' \ - && postconf -M -e 'policy/unix=policy unix - n n - - spawn user=nobody argv=/usr/local/bin/discourse-smtp-fast-rejection' \ && if [ "$INCLUDE_DMARC" = "true" ]; then \ postconf -M -e 'policyd-spf/unix=policyd-spf unix - n n - - spawn user=nobody argv=/usr/bin/policyd-spf'; \ fi \ @@ -46,7 +45,7 @@ COPY policyd-spf.conf /etc/postfix-policyd-spf-python/policyd-spf.conf COPY opendkim.conf /etc/opendkim.conf COPY opendmarc.conf /etc/opendmarc.conf -COPY receive-mail discourse-smtp-fast-rejection /usr/local/bin/ +COPY receive-mail /usr/local/bin/ COPY lib/ /usr/local/lib/site_ruby/ COPY boot /sbin/ COPY fake-pups /pups/bin/pups diff --git a/discourse-smtp-fast-rejection b/discourse-smtp-fast-rejection deleted file mode 100755 index ff15d54..0000000 --- a/discourse-smtp-fast-rejection +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -ENV_FILE = "/etc/postfix/mail-receiver-environment.json" - -require 'mail_receiver/fast_rejection' - -if __FILE__ == $0 - receiver = FastRejection.new(ENV_FILE) - receiver.process -end diff --git a/lib/mail_receiver/fast_rejection.rb b/lib/mail_receiver/fast_rejection.rb deleted file mode 100644 index f11ea0e..0000000 --- a/lib/mail_receiver/fast_rejection.rb +++ /dev/null @@ -1,145 +0,0 @@ -# frozen_string_literal: true - -# rubocop:disable Lint/RedundantRequireStatement -# require "set" is needed for Set -require "set" -require "syslog" -require "json" -require "uri" -require "cgi" -require "net/http" - -require_relative "mail" -require_relative "mail_receiver_base" - -class FastRejection < MailReceiverBase - def initialize(env_file) - super(env_file) - - @disabled = @env["DISCOURSE_FAST_REJECTION_DISABLED"] || !@env["DISCOURSE_BASE_URL"] - - @blacklisted_sender_domains = - Set.new(@env.fetch("BLACKLISTED_SENDER_DOMAINS", "").split(" ").map(&:downcase)) - end - - def disabled? - !!@disabled - end - - def process - $stdout.sync = true # unbuffered output - - args = {} - while line = gets - # Fill up args with the request details. - line = line.chomp - if line.empty? - puts "action=#{process_single_request(args)}" - puts "" - - args = {} # reset for next request. - else - k, v = line.chomp.split("=", 2) - args[k] = v - end - end - end - - def process_single_request(args) - return "dunno" if disabled? - - if args["request"] != "smtpd_access_policy" - return "defer_if_permit Internal error, Request type invalid" - elsif args["protocol_state"] != "RCPT" - return "dunno" - elsif args["sender"].nil? - # Note that while this key should always exist, its value may be the empty - # string. Postfix will convert the "<>" null sender to "". - return "defer_if_permit No sender specified" - elsif args["recipient"].nil? - return "defer_if_permit No recipient specified" - end - - run_filters(args) - end - - def maybe_reject_email(from, to) - uri = URI.parse(endpoint) - fromarg = CGI.escape(from) - toarg = CGI.escape(to) - - qs = "from=#{fromarg}&to=#{toarg}" - if uri.query && !uri.query.empty? - uri.query += "&#{qs}" - else - uri.query = qs - end - - begin - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = uri.scheme == "https" - get = Net::HTTP::Get.new(uri.request_uri) - get["Api-Username"] = username - get["Api-Key"] = key - response = http.request(get) - rescue StandardError => ex - logger.err "Failed to GET smtp_should_reject answer from %s: %s (%s)", - endpoint, - ex.message, - ex.class - logger.err ex.backtrace.map { |l| " #{l}" }.join("\n") - return "defer_if_permit Internal error, API request preparation failed" - ensure - http.finish if http && http.started? - end - - if Net::HTTPSuccess === response - reply = JSON.parse(response.body) - return "reject #{reply["reason"]}" if reply["reject"] - else - logger.err "Failed to GET smtp_should_reject answer from %s: %s", endpoint, response.code - return "defer_if_permit Internal error, API request failed" - end - - "dunno" # let future tests also be allowed to reject this one. - end - - def endpoint - "#{@env["DISCOURSE_BASE_URL"]}/admin/email/smtp_should_reject.json" - end - - private - - def run_filters(args) - filters = %i[maybe_reject_by_sender_domain maybe_reject_by_api] - - filters.each do |f| - action = send(f, args) - return action if action != "dunno" - end - - "dunno" - end - - def maybe_reject_by_sender_domain(args) - sender = args["sender"] - - return "dunno" if sender.empty? - - domain = domain_from_addrspec(sender) - if domain.empty? - logger.info("deferred mail with domainless sender #{sender}") - return "defer_if_permit Invalid sender" - end - if @blacklisted_sender_domains.include? domain - logger.info("rejected mail from blacklisted sender domain #{domain} (from #{sender})") - return "reject Invalid sender" - end - - "dunno" - end - - def maybe_reject_by_api(args) - maybe_reject_email(args["sender"], args["recipient"]) - end -end diff --git a/spec/lib/fast_rejection_spec.rb b/spec/lib/fast_rejection_spec.rb deleted file mode 100644 index 8969499..0000000 --- a/spec/lib/fast_rejection_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -# frozen_string_literal: true -require_relative "../../lib/mail_receiver/fast_rejection" - -describe FastRejection do - it "is enabled if BASE_URL is present" do - receiver = described_class.new(file_for(:standard)) - expect(receiver).not_to be_disabled - end - - it "is disabled if FAST_REJECTION_DISABLED is set" do - receiver = described_class.new(file_for(:fast_disabled)) - expect(receiver).to be_disabled - end - - it "is disabled if missing the base URL" do - receiver = described_class.new(file_for(:standard_deprecated)) - expect(receiver).to be_disabled - end - - it "has the correct endpoint" do - receiver = described_class.new(file_for(:standard)) - expect(receiver.endpoint).to eq("https://localhost:8080/admin/email/smtp_should_reject.json") - end - - describe "#process_single_request" do - let(:receiver) { described_class.new(file_for(:standard)) } - - it "returns defer_if_permit if not smtpd_access_policy" do - response = receiver.process_single_request("request" => "unexpected") - expect(response).to start_with("defer_if_permit") - end - - it "returns dunno if the protocol state is not RCPT" do - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "NOT_RCPT", - ) - expect(response).to eq("dunno") - end - - it "returns defer_if_permit if no sender" do - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "recipient" => "discourse@example.com", - ) - expect(response).to start_with("defer_if_permit") - end - - it "returns dunno if from the null sender" do - expect_any_instance_of(Net::HTTP).to receive(:request) do |http| - response = Net::HTTPSuccess.new(http, 200, "OK") - allow(response).to receive(:body).and_return("{}") - response - end - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "", - "recipient" => "discourse@example.com", - ) - expect(response).to eq("dunno") - end - - it "returns defer_if_permit if no recipient" do - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "eviltrout@example.com", - ) - expect(response).to start_with("defer_if_permit") - end - - it "returns defer_if_permit if sender addr-spec contains no domain" do - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "miscreant", - "recipient" => "discourse@example.com", - ) - expect(response).to start_with("defer_if_permit") - end - - it "returns reject if sender domain is blacklisted" do - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "miscreant@bad.com", - "recipient" => "discourse@example.com", - ) - expect(response).to start_with("reject") - end - - it "returns reject if sender domain is blacklisted with differing case" do - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "miscreant@SaD.NeT", - "recipient" => "discourse@example.com", - ) - expect(response).to start_with("reject") - end - - it "returns dunno if everything looks good" do - expect_any_instance_of(Net::HTTP).to receive(:request) do |http| - response = Net::HTTPSuccess.new(http, 200, "OK") - allow(response).to receive(:body).and_return("{}") - response - end - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "eviltrout@example.com", - "recipient" => "discourse@example.com", - ) - expect(response).to eq("dunno") - end - - it "rejects if there's an HTTP error" do - expect_any_instance_of(Net::HTTP).to receive(:request) do |http| - Net::HTTPServerError.new(http, 500, "Error") - end - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "eviltrout@example.com", - "recipient" => "discourse@example.com", - ) - expect(response).to start_with("defer_if_permit") - end - - it "rejects if the HTTP response has reject in the JSON" do - expect_any_instance_of(Net::HTTP).to receive(:request) do |http| - response = Net::HTTPSuccess.new(http, 200, "OK") - allow(response).to receive(:body).and_return( - '{"reject": true, "reason": "because I said so"}', - ) - response - end - response = - receiver.process_single_request( - "request" => "smtpd_access_policy", - "protocol_state" => "RCPT", - "sender" => "eviltrout@example.com", - "recipient" => "discourse@example.com", - ) - expect(response).to eq("reject because I said so") - end - end -end