From 7125101468c953fc74fe8f46ff3f9e117f8c09ec Mon Sep 17 00:00:00 2001 From: Chris Croome <chris@webarchitects.co.uk> Date: Wed, 3 May 2017 20:38:36 +0100 Subject: [PATCH] Switched from Postfix to Exim4 --- roles/api/tasks/main.yml | 6 +- roles/email/files/check_rcpt_local_acl | 14 +++ .../email/files/discourse-smtp-fast-rejection | 5 +- roles/email/files/discourse-smtp-rcpt-acl | 102 ++++++++++++++++ roles/email/files/receive-mail | 2 +- roles/email/tasks/main.yml | 109 +++--------------- roles/email/templates/transport.j2 | 1 - roles/email/templates/update-exim4.j2 | 32 +++++ 8 files changed, 173 insertions(+), 98 deletions(-) create mode 100644 roles/email/files/check_rcpt_local_acl create mode 100644 roles/email/files/discourse-smtp-rcpt-acl delete mode 100644 roles/email/templates/transport.j2 create mode 100644 roles/email/templates/update-exim4.j2 diff --git a/roles/api/tasks/main.yml b/roles/api/tasks/main.yml index 5a8d004..d35a584 100644 --- a/roles/api/tasks/main.yml +++ b/roles/api/tasks/main.yml @@ -1,7 +1,7 @@ --- -- name: Stat "/etc/postfix/mail-receiver-environment.json" +- name: Stat "/etc/exim/mail-receiver-environment.json" stat: - path: "/etc/postfix/mail-receiver-environment.json" + path: "/etc/exim/mail-receiver-environment.json" register: mail_receiver_environment - block: @@ -9,7 +9,7 @@ - name: Discourse scripts environmental variables file in place template: src: templates/mail-receiver-environment.json.j2 - dest: /etc/postfix/mail-receiver-environment.json + dest: /etc/exim/mail-receiver-environment.json mode: 0644 when: mail_receiver_environment.stat.exists == False diff --git a/roles/email/files/check_rcpt_local_acl b/roles/email/files/check_rcpt_local_acl new file mode 100644 index 0000000..024108e --- /dev/null +++ b/roles/email/files/check_rcpt_local_acl @@ -0,0 +1,14 @@ +# Local rcpt check + deny + message = No such discourse list + log_message = No such discourse list + !acl = acl_local_deny_exceptions + condition = ${run{/usr/local/bin/discourse-smtp-rcpt-acl $sender_address $local_part@$domain}{no}{${if eq {$runrc}{2}{yes}{no}}}} + + defer + message = Temporary error checking discourse + !acl = acl_local_deny_exceptions + condition = ${if eq {$runrc}{1}{yes}{no}} + + + diff --git a/roles/email/files/discourse-smtp-fast-rejection b/roles/email/files/discourse-smtp-fast-rejection index 1c0541a..ff6eeea 100644 --- a/roles/email/files/discourse-smtp-fast-rejection +++ b/roles/email/files/discourse-smtp-fast-rejection @@ -6,7 +6,7 @@ require 'uri' require 'cgi' require 'net/http' -ENV_FILE = "/etc/postfix/mail-receiver-environment.json" +ENV_FILE = "/etc/exim/mail-receiver-environment.json" def logger @logger ||= Syslog.open("smtp-reject", Syslog::LOG_PID, Syslog::LOG_MAIL) @@ -37,6 +37,7 @@ def process_requests(env) args = {} while line = gets # Fill up args with the request details. + logger.err "KDDEBUG line %s", line line = line.chomp if line.empty? process_single_request(args, env) @@ -49,6 +50,7 @@ def process_requests(env) end def process_single_request(args, env) + logger.err "KDDEBUG args %s", args action = 'dunno' if args['request'] != 'smtpd_access_policy' action = 'defer_if_permit Internal error, Request type invalid' @@ -85,6 +87,7 @@ def maybe_reject_email(from, to, env) begin http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" + logger.err "KDDEBUG request_uri %s", uri.request_uri get = Net::HTTP::Get.new(uri.request_uri) response = http.request(get) rescue StandardError => ex diff --git a/roles/email/files/discourse-smtp-rcpt-acl b/roles/email/files/discourse-smtp-rcpt-acl new file mode 100644 index 0000000..a2c163b --- /dev/null +++ b/roles/email/files/discourse-smtp-rcpt-acl @@ -0,0 +1,102 @@ +#!/usr/bin/env ruby + +require 'syslog' +require 'json' +require 'uri' +require 'cgi' +require 'net/http' + +# Returns 0 for accept +# Returns 1 for defer +# Returns 2 for reject + +ENV_FILE = "/etc/exim/mail-receiver-environment.json" + +def logger + @logger ||= Syslog.open("smtp-reject", Syslog::LOG_PID, Syslog::LOG_MAIL) +end + +def fatal(*args) + logger.crit *args + exit 1 +end + +def main + unless File.exists?(ENV_FILE) + fatal "Config file %s does not exist. Aborting.", ENV_FILE + end + + real_env = JSON.parse(File.read(ENV_FILE)) + + %w{DISCOURSE_BASE_URL DISCOURSE_API_KEY DISCOURSE_API_USERNAME}.each do |kw| + fatal "env var %s is required", kw unless real_env[kw] + end + + logger.err "KDDEBUG ARGV.lenght %s", ARGV.length + if ARGV.length != 2 + sender = 'test@example.com' + recipient = ARGV[0] + else + sender = ARGV[0] + recipient = ARGV[1] + end + process_single_request(sender, recipient, real_env) +end + +def process_single_request(sender,recipient, env) + action = 0 + if sender.nil? + action = 1 + elsif recipient.nil? + action = 1 + else + action = maybe_reject_email( sender, recipient, env) + end + + exit(action) +end + +def maybe_reject_email(from, to, env) + endpoint = "#{env['DISCOURSE_BASE_URL']}/admin/email/smtp_should_reject.json" + key = env["DISCOURSE_API_KEY"] + username = env["DISCOURSE_API_USERNAME"] + + uri = URI.parse(endpoint) + fromarg = CGI::escape(from) + toarg = CGI::escape(to) + + api_qs = "api_key=#{key}&api_username=#{username}&from=#{fromarg}&to=#{toarg}" + if uri.query and !uri.query.empty? + uri.query += "&#{api_qs}" + else + uri.query = api_qs + end + + begin + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == "https" + logger.err "KDDEBUG request_uri %s", uri.request_uri + get = Net::HTTP::Get.new(uri.request_uri) + 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 1 + ensure + http.finish if http && http.started? + end + + if Net::HTTPSuccess === response + reply = JSON.parse(response.body) + if reply['reject'] + return 2 + end + else + logger.err "Failed to GET smtp_should_reject answer from %s: %s", endpoint, response.code + return 1 + end + + return 0 # let future tests also be allowed to reject this one. +end + +main if __FILE__ == $0 diff --git a/roles/email/files/receive-mail b/roles/email/files/receive-mail index 5164af9..37aeb41 100644 --- a/roles/email/files/receive-mail +++ b/roles/email/files/receive-mail @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -ENV_FILE = "/etc/postfix/mail-receiver-environment.json" +ENV_FILE = "/etc/exim/mail-receiver-environment.json" EX_TEMPFAIL = 75 EX_SUCCESS = 0 diff --git a/roles/email/tasks/main.yml b/roles/email/tasks/main.yml index 4cd15f4..a4c8e86 100644 --- a/roles/email/tasks/main.yml +++ b/roles/email/tasks/main.yml @@ -23,26 +23,13 @@ dest: /usr/local/bin/discourse-smtp-fast-rejection mode: 0755 -- name: debconf-utils installed for Ansible - apt: - name: debconf-utils - state: present - -- name: Debconf Postfix hostname set - debconf: - name: postifx - question: "postfix/mailname" - value: "{{ hostname }}" - vtype: string - -- name: Debconf Postfix set to be a internet server - debconf: - name: postfix - question: "postfix/main_mailer_type" - value: "Internet Site" - vtype: string +- name: Ruby script discourse-smtp-rcpt-acl in place + copy: + src: files/discourse-smtp-rcpt-acl + dest: /usr/local/bin/discourse-smtp-rcpt-acl + mode: 0755 -- name: Postfix and related email packages installed +- name: Exim and related email packages installed apt: pkg: "{{ item }}" state: latest @@ -51,12 +38,17 @@ - curl - debian-archive-keyring - dnsutils + - exim4-daemon-light - mailutils - mutt - - postfix - pwgen - whois +- name: Exim check_rcpt_local_acl in place + copy: + src: files/add check_rcpt_local_acl + dest: /etc/exim4/check_rcpt_local_acl + - name: Get the app container IP address command: "docker inspect --format '{''{ .NetworkSettings.IPAddress }''}' app" register: app_ip_address @@ -64,82 +56,15 @@ - debug: msg: "The Discourse app Docker container has the IP address {{ app_ip_address.stdout }}" -- name: Postfix my networks set to include {{ app_ip_address.stdout }} - command: postconf -e "mynetworks = 127.0.0.0/8, {{ app_ip_address.stdout }}" - -- name: Postfix mydestination set to contain {{ hostname }} - # command: postconf -e "mydestination = {{ hostname }}, localhost.localnetwork, localhost" - command: postconf -e "mydestination = localhost" - -- name: Postfix relay domains set to {{ hostname }} - command: postconf -e "relay_domains = {{ hostname }}" - -- name: Postfix set not to use /etc/aliases - command: postconf -e "alias_maps = " - -- name: Postfix set for ipv4 only - command: postconf -e "inet_protocols = ipv4" - -- name: Postfix stopped for inet_protocols change - command: postfix stop - -- name: Postfix started after inet_protocols change - command: postfix start +- name: Exim config in place + template: templates/update-exim4.j2 + dest: /etc/exim4/update-exim4.conf.conf -- name: Postfix opportunistic TLS enabled - command: postconf -e "smtp_tls_security_level = may" - -- name: Postfix set to use sub-addresing - command: postconf -e "recipient_delimiter = +" - -- name: Postfix disable UTF-8 SMTP input - command: postconf -e "smtputf8_enable=no" - -- name: Postfix Time Zone and Lang set - command: postconf -e "export_environment='TZ LANG'" - -- name: Postfix set to use /usr/local/bin/receive-mail - command: postconf -M -e "discourse/unix=discourse unix - n n - - pipe user=nobody:nogroup argv=/usr/local/bin/receive-mail ${recipient}" - -- name: Postfix transport in place - template: - src: templates/transport.j2 - dest: /etc/postfix/transport - mode: 0644 - -- name: Postfix Transport Maps file set - command: postconf -e "transport_maps=hash:/etc/postfix/transport" - -- name: Postmap run with Transport Maps file - command: postmap /etc/postfix/transport -# -# This breaks outgoing email for the Discourse Docker app -#- name: Postfix smtpd_recipient_restrictions set -# command: postconf -e "smtpd_recipient_restrictions = check_policy_service unix:private/policy" - -- name: Postfix set to reject incorrect email addresses - command: postconf -M -e "policy/unix=policy unix - n n - - spawn user=nobody argv=/usr/local/bin/discourse-smtp-fast-rejection" - -- name: Stat "/var/discourse/shared/standalone/letsencrypt/{{ hostname }}/{{ hostname }}.cer" - stat: - path: "/var/discourse/shared/standalone/letsencrypt/{{ hostname }}/{{ hostname }}.cer" - register: le_cert - -- block: - - - name: Postfix configured to use Let's Encrypt RSA cert for incoming email - command: postconf -e "smtpd_tls_cert_file = /var/discourse/shared/standalone/letsencrypt/{{ hostname }}/{{ hostname }}.cer" - - - name: Postfix configured to use Let's Encrypt RSA key for incoming email - command: postconf -e "smtpd_tls_key_file = /var/discourse/shared/standalone/letsencrypt/{{ hostname }}/{{ hostname }}.key" - - when: le_cert.stat.exists == True +- name: Exim reconfigured + command: dpkg-reconfigure exim4-config - name: Root .forward in place template: src: templates/forward.j2 dest: /root/.forward -- name: Postfix reloaded - command: postfix reload - diff --git a/roles/email/templates/transport.j2 b/roles/email/templates/transport.j2 deleted file mode 100644 index e4f9e67..0000000 --- a/roles/email/templates/transport.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ hostname }} discourse: diff --git a/roles/email/templates/update-exim4.j2 b/roles/email/templates/update-exim4.j2 new file mode 100644 index 0000000..883a8ab --- /dev/null +++ b/roles/email/templates/update-exim4.j2 @@ -0,0 +1,32 @@ +# /etc/exim4/update-exim4.conf.conf +# +# Edit this file and /etc/mailname by hand and execute update-exim4.conf +# yourself or use 'dpkg-reconfigure exim4-config' +# +# Please note that this is _not_ a dpkg-conffile and that automatic changes +# to this file might happen. The code handling this will honor your local +# changes, so this is usually fine, but will break local schemes that mess +# around with multiple versions of the file. +# +# update-exim4.conf uses this file to determine variable values to generate +# exim configuration macros for the configuration file. +# +# Most settings found in here do have corresponding questions in the +# Debconf configuration, but not all of them. +# +# This is a Debian specific file + +dc_eximconfig_configtype='internet' +dc_other_hostnames='{{ hostname }}' +dc_local_interfaces='' +dc_readhost='' +dc_relay_domains='' +dc_minimaldns='false' +dc_relay_nets='{{ app_ip_address.stdout }}/32' +dc_smarthost='' +CFILEMODE='644' +dc_use_split_config='true' +dc_hide_mailname='' +dc_mailname_in_oh='true' +dc_localdelivery='mail_spool' + -- GitLab