diff --git a/conf/passwords.yml b/conf/passwords.yml
index 8ed70028fde0813dc2ecbd675afac1e294d6e7dd..bccad2175fa7355a3d3da4e4d5ba192c3ddc6414 100644
--- a/conf/passwords.yml
+++ b/conf/passwords.yml
@@ -43,6 +43,8 @@
   description: LDAP cn=account-automation password
 - name: ldap_authserver_dav_password
   description: LDAP cn=authserver-dav password
+- name: ldap_postfix_password
+  description: LDAP cn=postfix password
 
 - name: grafana_session_secret
   description: session secret for Grafana
diff --git a/conf/services.yml b/conf/services.yml
index 732ad62e225524cbe2bf6a6827619f90e11483d5..f22dcc17850b8ad1afa127b5c3d7dc16ed9b6681 100644
--- a/conf/services.yml
+++ b/conf/services.yml
@@ -240,3 +240,6 @@ account-automation:
   scheduling_group: backend
   ldap_credentials:
     - name: account-automation
+mail:
+  ldap_credentials:
+    - name: postfix
diff --git a/rules/roles/ldap/files/config/slapd.conf.acl b/rules/roles/ldap/files/config/slapd.conf.acl
index 995e3e5b0502eacb5318c688fe70ed98e347b8de..1a12bdb221310c7503f5e0f24487e95968024f3d 100644
--- a/rules/roles/ldap/files/config/slapd.conf.acl
+++ b/rules/roles/ldap/files/config/slapd.conf.acl
@@ -20,17 +20,17 @@ access to attrs=status,host,originalHost
   by dn="cn=account-automation,ou=Operators,dc=investici,dc=org,o=Anarchy" write
   by dn="cn=ring0op,ou=Operators,dc=investici,dc=org,o=Anarchy" write
   by dn="cn=dovecot,ou=Operators,dc=investici,dc=org,o=Anarchy" read
+  by dn="cn=postfix,ou=Operators,dc=investici,dc=org,o=Anarchy" read
   by * none
 
 # acl per i certificati e chiavi private SSL dei domini degli utenti
-# solo per cn=manager
 access to filter=(objectClass=acmeRequest)
-  by dn="cn=ring0op,ou=Operators,dc=investici,dc=org,o=Anarchy" read
+  by dn="cn=ring0op,ou=Operators,dc=investici,dc=org,o=Anarchy" write
   by dn="cn=replica,ou=Operators,dc=investici,dc=org,o=Anarchy" read
   by * none
 
 access to filter=(objectClass=sslCredentials)
-  by dn="cn=ring0op,ou=Operators,dc=investici,dc=org,o=Anarchy" read
+  by dn="cn=ring0op,ou=Operators,dc=investici,dc=org,o=Anarchy" write
   by dn="cn=replica,ou=Operators,dc=investici,dc=org,o=Anarchy" read
   by * none
 
@@ -57,5 +57,6 @@ access to *
   by dn="cn=authserver-dav,ou=Operators,dc=investici,dc=org,o=Anarchy" read
   by dn="cn=ring0op,ou=Operators,dc=investici,dc=org,o=Anarchy" read
   by dn="cn=replica,ou=Operators,dc=investici,dc=org,o=Anarchy" read
+  by dn="cn=postfix,ou=Operators,dc=investici,dc=org,o=Anarchy" read
   by sockurl=ldapi://%2frun%2fldap%2fldapi read
   by * none
diff --git a/rules/roles/mail/tasks/postfix_instance.yml b/rules/roles/mail/tasks/postfix_instance.yml
index 5f5433c8d0f9689713e0fd82b21c7e6075352e56..3bfce602ecfe7df4575ee6d8513765ed750bb5b3 100644
--- a/rules/roles/mail/tasks/postfix_instance.yml
+++ b/rules/roles/mail/tasks/postfix_instance.yml
@@ -53,8 +53,7 @@
   register: postfix_config_files
 
 - name: Regenerate all Postfix hash maps
-  shell: "postconf -c {{ postfix_dir }} -x | perl -nle 'print $2 if /(hash|cdb):(\\S+)/' | sort -u | grep -v /\\$ | xargs --no-run-if-empty -n 1 postmap"
-  when: "postfix_config_files|changed"
+  shell: "postconf -c {{ postfix_dir }} -x | perl -nle 'print $2 if /(hash|cdb):([^ ,]+)/' | sort -u | grep -v /\\$ | xargs --no-run-if-empty -n 1 postmap"
 
 - systemd:
     name: "{{ postfix_systemd_service }}"
diff --git a/rules/roles/mail/templates/ldap.base.j2 b/rules/roles/mail/templates/ldap.base.j2
index ef23fd2fbb93dbb5acbf1d8f64f04ef5a6b2ff20..72ccbbe590d32ba67838d3314cc30e2d86343281 100644
--- a/rules/roles/mail/templates/ldap.base.j2
+++ b/rules/roles/mail/templates/ldap.base.j2
@@ -2,9 +2,10 @@
 server_host = localhost
 server_port = 389
 timeout = 5
+version = 3
 bind = yes
-bind_dn = "{{ postfix_ldap_bind_dn }}"
-bind_pw = "{{ postfix_ldap_password }}"
+bind_dn = {{ postfix_ldap_bind_dn }}
+bind_pw = {{ ldap_postfix_password }}
 
 domain = cdb:/etc/postfix/domains
 
diff --git a/rules/roles/mail/templates/postfix-delivery/main.cf b/rules/roles/mail/templates/postfix-delivery/main.cf
new file mode 100644
index 0000000000000000000000000000000000000000..7c44b5a5bbbea9d33633b58f46b2999b89912c3c
--- /dev/null
+++ b/rules/roles/mail/templates/postfix-delivery/main.cf
@@ -0,0 +1,35 @@
+# Postfix configuration file for the instance handling inbound email
+# to user mailboxes. Doesn't do much except run the spam-filtering
+# milters and forwarding everything to Dovecot over LMTP.
+
+{% include "main.cf.base.j2" %}
+
+ldap = proxy:ldap:/etc/postfix/ldap/
+
+mynetworks = 127.0.0.0/8 [::1]/128 172.16.1.0/24
+
+# Don't anvil(8) control the internal port.
+smtpd_client_connection_count_limit = 0
+smtpd_client_event_limit_exceptions = $mynetworks
+
+# No local delivery (virtual-only).
+mydestination =
+alias_maps =
+alias_database =
+local_recipient_maps =
+local_transport = error:5.1.1 Mailbox unavailable
+
+# All internal connections are trusted.
+smtpd_relay_restrictions =
+smtpd_recipient_restrictions = permit_mynetworks, reject
+
+# Deliver all emails to Dovecot over LMTP.
+virtual_transport = lmtp:unix:private/dovecot-lmtp
+
+# Recipient domains that are sent to virtual_transport.
+virtual_mailbox_domains = ${indexed}domains
+
+# Aliases have already been resolved by the postfix-out instance.
+# The return value from the lookup is ignored, because we've set
+# virtual_transport and virtual_mailbox_domains.
+virtual_mailbox_maps = ${ldap}local-recipients
diff --git a/rules/roles/mail/templates/postfix-in/dnsbl-reply-map b/rules/roles/mail/templates/postfix-in/dnsbl-reply-map
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/rules/roles/mail/templates/postfix-in/domains b/rules/roles/mail/templates/postfix-in/domains
new file mode 100644
index 0000000000000000000000000000000000000000..bea68111a8d4af39ff0ec5e4cd0ca2a145ea6d31
--- /dev/null
+++ b/rules/roles/mail/templates/postfix-in/domains
@@ -0,0 +1,4 @@
+{{ domain }} OK
+{% for d in domain_public %}
+{{ d }} OK
+{% endfor %}
diff --git a/rules/roles/mail/templates/postfix-in/virtual b/rules/roles/mail/templates/postfix-in/virtual
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/rules/roles/mail/templates/postfix-out/main.cf b/rules/roles/mail/templates/postfix-out/main.cf
index 0f311d2a5b9357310b4f4868ba2a25a87ebd0500..1b38fc75e491b6f0863f3f8a6e3c4bc785a7594f 100644
--- a/rules/roles/mail/templates/postfix-out/main.cf
+++ b/rules/roles/mail/templates/postfix-out/main.cf
@@ -42,7 +42,9 @@ relay_domains = ${indexed}domains
 relay_recipient_maps = ${ldap}recipients
 relay_destination_recipient_limit = 1
 
-# Transport settings ...
+# Resolve aliases etc, we want all outbound email to the
+# postfix-delivery instances to have the final recipients.
+virtual_alias_maps = ${ldap}aliases
 
 # Message size limit
 message_size_limit = 15000000
diff --git a/rules/roles/mail/templates/postfix-smtp-auth/master.cf b/rules/roles/mail/templates/postfix-smtp-auth/master.cf
new file mode 100644
index 0000000000000000000000000000000000000000..65539dddc2ee20c9d2dc34e91071df1377bfb7b2
--- /dev/null
+++ b/rules/roles/mail/templates/postfix-smtp-auth/master.cf
@@ -0,0 +1,47 @@
+# Postfix master configuration file for the null-routing default instance.
+{{ ip }}:smtps      inet  n       -       n       -       1       postscreen
+        -o inet_interfaces={{ ip }}
+        -o smtpd_tls_wrappermode=yes
+{{ ip }}:submission      inet  n       -       n       -       1       postscreen
+        -o inet_interfaces={{ ip }}
+        -o smtpd_enforce_tls=yes
+{##
+{% if ip6 %}
+[{{ ip6 }}]:smtp      inet  n       -       n       -       1       postscreen
+        -o inet_interfaces={{ ip6 }}
+        -o smtpd_tls_wrappermode=yes
+[{{ ip6 }}]:submission      inet  n       -       n       -       1       postscreen
+        -o inet_interfaces={{ ip6 }}
+        -o smtpd_enforce_tls=yes
+{% endif %}
+##}
+
+smtpd     pass  -       -       n       -       -       smtpd
+tlsproxy  unix  -       -       n       -       0       tlsproxy
+dnsblog   unix  -       -       n       -       0       dnsblog
+
+tlsmgr    unix  -       -       y       1000?   1       tlsmgr
+pickup    unix  n       -       y       60      1       pickup
+cleanup   unix  n       -       y       -       0       cleanup
+qmgr      unix  n       -       n       300     1       qmgr
+tlsmgr    unix  -       -       y       1000?   1       tlsmgr
+rewrite   unix  -       -       y       -       -       trivial-rewrite
+bounce    unix  -       -       y       -       0       bounce
+defer     unix  -       -       y       -       0       bounce
+trace     unix  -       -       y       -       0       bounce
+verify    unix  -       -       y       -       1       verify
+flush     unix  n       -       y       1000?   0       flush
+proxymap  unix  -       -       n       -       -       proxymap
+proxywrite unix -       -       n       -       1       proxymap
+smtp      unix  -       -       y       -       -       smtp
+relay     unix  -       -       y       -       -       smtp
+        -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
+showq     unix  n       -       y       -       -       showq
+error     unix  -       -       y       -       -       error
+retry     unix  -       -       y       -       -       error
+discard   unix  -       -       y       -       -       discard
+local     unix  -       n       n       -       -       local
+virtual   unix  -       n       n       -       -       virtual
+lmtp      unix  -       -       y       -       -       lmtp
+anvil     unix  -       -       y       -       1       anvil
+scache    unix  -       -       y       -       1       scache
diff --git a/rules/roles/mail/templates/postfix/ldap/aliases b/rules/roles/mail/templates/postfix/ldap/aliases
index 4d148f61f3f507d7629c988ba0283d124bce6e52..29cac80d8c0b86173de2b949a9018c63c24d33ac 100644
--- a/rules/roles/mail/templates/postfix/ldap/aliases
+++ b/rules/roles/mail/templates/postfix/ldap/aliases
@@ -2,7 +2,7 @@
 
 {% include "ldap.base.j2" %}
 
-search_base = "{{ postfix_ldap_base }}"
-query_filter = "(&(mailAlternateAddress=%s)(objectClass=virtualMailUser)(|(status=active)(status=temporary)(status=readonly)))"
+search_base = {{ postfix_ldap_base }}
+query_filter = (&(mailAlternateAddress=%s)(objectClass=virtualMailUser)(|(status=active)(status=temporary)(status=readonly)))
 scope = sub
 result_attribute = mail
diff --git a/rules/roles/mail/templates/postfix/ldap/local-recipients b/rules/roles/mail/templates/postfix/ldap/local-recipients
new file mode 100644
index 0000000000000000000000000000000000000000..cfa1fc67209f4086e8984464780d748e8bf319bc
--- /dev/null
+++ b/rules/roles/mail/templates/postfix/ldap/local-recipients
@@ -0,0 +1,9 @@
+# LDAP query resolving valid mailbox users on this host
+
+{% include "ldap.base.j2" %}
+
+search_base = {{ postfix_ldap_base }}
+query_filter = (&(mail=%s)(host={{ ansible_hostname }})(objectClass=virtualMailUser)(|(status=active)(status=temporary)(status=readonly)))
+scope = sub
+result_attribute = mail
+
diff --git a/rules/roles/mail/templates/postfix/ldap/recipients b/rules/roles/mail/templates/postfix/ldap/recipients
index 970f7b0b1c4a6752f17d1a086ecba5ecace50d57..e7c1b438b360eec6fd8c26b416569a5ee49e47f9 100644
--- a/rules/roles/mail/templates/postfix/ldap/recipients
+++ b/rules/roles/mail/templates/postfix/ldap/recipients
@@ -2,8 +2,8 @@
 
 {% include "ldap.base.j2" %}
 
-search_base = "{{ postfix_ldap_base }}"
-query_filter = "(&(mail=%s)(objectClass=virtualMailUser)(|(status=active)(status=temporary)(status=readonly)))"
+search_base = {{ postfix_ldap_base }}
+query_filter = (&(mail=%s)(objectClass=virtualMailUser)(|(status=active)(status=temporary)(status=readonly)))
 scope = sub
 result_attribute = host
 result_format = relay:[%s.smtp-delivery.{{ domain }}]