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