diff --git a/software/erp5/instance-smtp-schema.json b/software/erp5/instance-smtp-schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..fd3b1f4599511c81036d0519b88af11845671a2e
--- /dev/null
+++ b/software/erp5/instance-smtp-schema.json
@@ -0,0 +1,56 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "extends": "./schema-definitions.json#",
+  "required": ["tcpv4-port"],
+  "properties": {
+    "tcpv4-port": {
+      "allOf": [{
+        "$ref": "#/definitions/tcpv4port"
+      }, {
+        "description": "Start allocating ports at this value, going upward"
+      }]
+    },
+    "postmaster": {
+      "description": "Mail address to send technical mails to. Non-empty value required for smptd relay service to be deployed. Values will be put in alias-dict as 'postmaster' key (alias-dict takes precedence)",
+      "default": "",
+      "type": "string"
+    },
+    "alias-dict": {
+      "description": "Mail alias support",
+      "default": {},
+      "patternProperties": {
+        ".*": {
+          "description": "List of addresses alias expands to",
+          "type": "array"
+        }
+      },
+      "type": "object"
+    },
+    "relay": {
+      "description": "Forward outgoing mails to a specific relay. If enabled, relay must support TLS-encrypted SASL authentication.",
+      "dependencies": {
+        "host": ["sasl-credential"]
+      },
+      "properties": {
+        "host": {
+          "description": "Host name or address of relay, with optional port (ex: '[example.com]:submissionu'). Enclosing hostname with [] prevents MX lookup.",
+          "type": "string"
+        },
+        "sasl-credential": {
+          "description": "SASL credential, in the login:password form",
+          "type": "string"
+        }
+      },
+      "default": {},
+      "type": "object"
+    },
+    "divert": {
+      "description": "Intercept all mails and send them to given addresses instead of original recipient",
+      "type": "array",
+      "items": {
+        "type": "string"
+      },
+      "uniqueItems": true
+    }
+  }
+}
diff --git a/stack/erp5/TODO b/stack/erp5/TODO
index 4d9b4e427e7ce66a7c2cdf6bf83abab4da78ed31..11ab49b0b6159e75bc9870b08569ec389622e4e2 100644
--- a/stack/erp5/TODO
+++ b/stack/erp5/TODO
@@ -6,7 +6,8 @@ General:
 - resilience
 - make mariadb user accounts accept connections only from relevant IPs
   or make x509 mandatory (needs ZMySQLD*A support)
-- postfix
+- make postfix log inside partition
+- document postfix parameters (only once it actually works)
 
 Monitoring:
 - daily slow-query digest
diff --git a/stack/erp5/buildout.cfg b/stack/erp5/buildout.cfg
index fd8fe1f2469f2a7cff450204222d968376906ff0..772c80e4f1e1c7b847d766887d0799ae6f3ded7b 100644
--- a/stack/erp5/buildout.cfg
+++ b/stack/erp5/buildout.cfg
@@ -202,24 +202,29 @@ md5sum = 79f789360e71146486c82a7a10834bae
 [template-postfix]
 < = download-base
 filename = instance-postfix.cfg.in
-md5sum = a1b131b5bf6a749b0c853e9577f43cca
+md5sum = 90a017581116f14014a039d38ef36ffd
 
 [template-postfix-master-cf]
 < = download-base
 filename = postfix_master.cf.in
-md5sum = 67f06ed63c5d3fa47908a83071b76bd3
+md5sum = 9ac81647368068a1a98a785d08074b43
 
 [template-postfix-main-cf]
 < = download-base
 filename = postfix_main.cf.in
-md5sum = b6f44d8507aa15d71fdff75bf3eeddb9
+md5sum = d51897728755e14d8005344608098009
+
+[template-postfix-aliases]
+< = download-base
+filename = postfix_aliases.in
+md5sum = 0969fbb25b05c02ef3c2d437b2f4e1a0
 
 [template]
 recipe = slapos.recipe.template:jinja2
 # XXX: "template.cfg" is hardcoded in instanciation recipe
 rendered = ${buildout:directory}/template.cfg
 template = ${:_profile_base_location_}/instance.cfg.in
-md5sum = 4b7ef360e34de488daab0d44973fd598
+md5sum = 4d043c96d70b35d1fbbd8120d8edee7c
 mode = 640
 context =
     key mariadb_link_binary template-mariadb:link-binary
@@ -232,6 +237,7 @@ context =
     key coreutils_location coreutils:location
     key cups_location cups:location
     key curl_location curl:location
+    key cyrus_sasl_location cyrus-sasl:location
     key dash_location dash:location
     key dbus_glib_location dbus-glib:location
     key dbus_location dbus:location
@@ -286,6 +292,7 @@ context =
     key template_mariadb_initial_setup template-mariadb-initial-setup:target
     key template_my_cnf template-my-cnf:target
     key template_postfix template-postfix:target
+    key template_postfix_aliases template-postfix-aliases:target
     key template_postfix_main_cf template-postfix-main-cf:target
     key template_postfix_master_cf template-postfix-master-cf:target
     key template_runzope_userhosts_preloaded template-runzope-userhosts-preloaded:target
@@ -301,7 +308,7 @@ context =
 [template-erp5]
 <= download-base
 filename = instance-erp5.cfg.in
-md5sum = bb951a205def0235f3c041045abd7801
+md5sum = 60cdf98d996f220d66daa11452c3f4bf
 
 [template-zeo]
 <= download-base
@@ -311,7 +318,7 @@ md5sum = 9670cf63099e2c520017a23defff51a4
 [template-zope]
 <= download-base
 filename = instance-zope.cfg.in
-md5sum = 74f2fbd7d653b0e6cfe29efcd1042ace
+md5sum = 995257c4d08365db7ac0d1b40936ef8b
 link-binary =
   ${aspell:location}/bin/aspell
   ${dmtx-utils:location}/bin/dmtxwrite
diff --git a/stack/erp5/instance-erp5.cfg.in b/stack/erp5/instance-erp5.cfg.in
index ebdc5930c2a977be4ca4db6a6aa2fe256850a7d0..34b859359809415a1a70ff3539e649ec652d57d5 100644
--- a/stack/erp5/instance-erp5.cfg.in
+++ b/stack/erp5/instance-erp5.cfg.in
@@ -4,11 +4,12 @@
 {% set site_id = slapparameter_dict.get('site-id', 'erp5') -%}
 {% set inituser_login = slapparameter_dict.get('inituser-login', 'zope') -%}
 {% set publish_dict = {'site-id': site_id, 'inituser-login': inituser_login} -%}
+{% set has_posftix = slapparameter_dict.get('smtp', {}).get('postmaster') -%}
 [request-common]
 <= request-common-base
 config-use-ipv6 = {{ dumps(slapparameter_dict.get('use-ipv6', False)) }}
 
-{% macro request(name, software_type, config_key, config, ret={'url': True}) -%}
+{% macro request(name, software_type, config_key, config, ret={'url': True}, key_config={}) -%}
 {% do config.update(slapparameter_dict.get(config_key, {})) -%}
 {% set section = 'request-' ~ name -%}
 [{{ section }}]
@@ -25,13 +26,22 @@ return = {{ ' '.join(ret) }}
 {% for k, v in config.iteritems() -%}
 config-{{ k }} = {{ dumps(v) }}
 {% endfor -%}
+{% for k, v in key_config.iteritems() -%}
+config-{{ k }} = {{ '${' ~ v ~ '}' }}
+{% endfor -%}
 {% endmacro -%}
 
 {{ request('memcached-persistent', 'kumofs', 'kumofs', {'tcpv4-port': 2000}) }}
 {{ request('memcached-volatile', 'kumofs', 'memcached', {'tcpv4-port': 2010, 'ram-storage-size': 64}) }}
 {{ request('cloudooo', 'cloudooo', 'cloudooo', {'tcpv4-port': 2020}) }}
 {{ request('mariadb', 'mariadb', 'mariadb', {'tcpv4-port': 2099}, {'database-list': True, 'test-database-list': True}) }}
-{{ request('postfix', 'postfix', 'postfix', {'tcpv4-port': 2025}) }}
+{% if has_posftix -%}
+{{   request('smtp', 'postfix', 'smtp', {'tcpv4-port': 2025, 'smtpd-sasl-user': 'erp5@nowhere'}, key_config={'smtpd-sasl-password': 'publish-early:smtpd-sasl-password'}) }}
+{%- else %}
+[request-smtp]
+# Placeholder smtp service URL
+connection-url = smtp://127.0.0.2:0/
+{%- endif %}
 
 {# ZODB -#}
 {% set zodb_dict = {} -%}
@@ -66,6 +76,9 @@ recipe = slapos.cookbook:publish-early
 -init =
   inituser-password gen-password:passwd
   deadlock-debugger-password gen-deadlock-debugger-password:passwd
+{%- if has_posftix %}
+  smtpd-sasl-password gen-smtpd-sasl-password:passwd
+{%- endif %}
 {%- if neo %}
   neo-cluster gen-neo-cluster:name
 {%-  if neo[0] %}
@@ -94,6 +107,9 @@ storage-path =
 [gen-neo-cluster]
 name = neo-${gen-neo-cluster-base:passwd}
 
+[gen-smtpd-sasl-password]
+< = gen-password
+
 [request-zope-base]
 <= request-common
 return =
@@ -113,7 +129,7 @@ config-memcached-url = ${request-memcached-volatile:connection-url}
 config-mysql-test-url-list = ${request-mariadb:connection-test-database-list}
 config-mysql-url-list = ${request-mariadb:connection-database-list}
 config-site-id = {{ dumps(site_id) }}
-config-smtp-url = {{ dumps(slapparameter_dict.get('smtp-url', 'smtp://localhost:25/')) }}
+config-smtp-url = ${request-smtp:connection-url}
 config-timezone = {{ dumps(slapparameter_dict.get('timezone', 'UTC')) }}
 config-zodb-dict = {{ dumps(zodb_dict) }}
 {% for server_type, server_dict in storage_dict.iteritems() -%}
diff --git a/stack/erp5/instance-postfix.cfg.in b/stack/erp5/instance-postfix.cfg.in
index 7f8de5173b6bae9d6ff78bc4ede3e18461cfc066..23f98cbbec798b9bbb2ae36e0cdf840161d0561f 100644
--- a/stack/erp5/instance-postfix.cfg.in
+++ b/stack/erp5/instance-postfix.cfg.in
@@ -1,168 +1,251 @@
-{% if software_type == slap_software_type -%}
 {% set part_list = [] -%}
 {% macro section(name) %}{% do part_list.append(name) %}{{ name }}{% endmacro -%}
-{% set ipv4 = (ipv4_set | list)[0] -%}
+{% if slapparameter_dict['use-ipv6'] -%}
+{%   set ip = '[' ~ (ipv6_set | list)[0] ~ ']' -%}
+{% else -%}
+{%   set ip = (ipv4_set | list)[0] -%}
+{% endif -%}
 {% set tcpv4_port = slapparameter_dict['tcpv4-port'] -%}
-
-[buildout]
-parts =
-  publish-postfix-connection-information
-  postfix-main-cf
-  postfix-master-cf
-  postfix-symlinks-bin
-  postfix-symlinks-sbin
-  postfix-symlinks-libexec
-  service-postfix-master
-  sh-postfix-environment
-  newaliases
-  {{ part_list | join('\n  ') }}
-
-eggs-directory = {{ eggs_directory }}
-develop-eggs-directory = {{ develop_eggs_directory }}
-
-
-[publish-postfix-connection-information]
+{% set relay = slapparameter_dict.get('relay', {}) -%}
+{% set divert = slapparameter_dict.get('divert', []) -%}
+{% set alias_dict = slapparameter_dict.get('alias-dict', {}) -%}
+{% do alias_dict.setdefault('postmaster', [slapparameter_dict['postmaster']]) -%}
+{% set smtpd_sasl_user = slapparameter_dict['smtpd-sasl-user'] -%}
+{% set smtpd_sasl_password = slapparameter_dict['smtpd-sasl-password'] -%}
+
+[smtpd-password]
+recipe = slapos.cookbook:generate.password
+storage-path =
+
+[{{ section('publish') }}]
 recipe = slapos.cookbook:publish.serialised
-url = smtp://${:ipv4}:${:port}/
-ipv4 = {{ ipv4 }}
-port = ${postfix-master-cf-parameters:smtp_port}
-
+url = {{ dumps('smtp://' ~ urllib.quote_plus(smtpd_sasl_user) ~ ':' ~ urllib.quote_plus(smtpd_sasl_password) ~ '@' ~ ip ~ ':' ~ tcpv4_port) }}
 
 [directory]
 recipe = slapos.cookbook:mkdirectory
 etc = ${buildout:directory}/etc
-etc_postfix = ${:etc}/postfix
-script = ${:etc}/run
-service = ${:etc}/service
 promise = ${:etc}/promise
+etc-postfix = ${:etc}/postfix
+etc-cyrus = ${:etc}/cyrus
+run = ${:etc}/run
+bin = ${buildout:directory}/bin
 usr = ${buildout:directory}/usr
-usr_bin = ${:usr}/bin
-usr_sbin = ${:usr}/sbin
 var = ${buildout:directory}/var
-var_spool = ${:var}/spool
-var_spool_postfix = ${:var_spool}/postfix
-var_spool_postfix_active = ${:var_spool_postfix}/active
-var_spool_postfix_bounce = ${:var_spool_postfix}/bounce
-var_spool_postfix_corrupt = ${:var_spool_postfix}/corrupt
-var_spool_postfix_defer = ${:var_spool_postfix}/defer
-var_spool_postfix_deferred = ${:var_spool_postfix}/deferred
-var_spool_postfix_flush = ${:var_spool_postfix}/flush
-var_spool_postfix_hold = ${:var_spool_postfix}/hold
-var_spool_postfix_incoming = ${:var_spool_postfix}/incoming
-var_spool_postfix_maildrop = ${:var_spool_postfix}/maildrop
-var_spool_postfix_pid = ${:var_spool_postfix}/pid
-var_spool_postfix_private = ${:var_spool_postfix}/private
-var_spool_postfix_public = ${:var_spool_postfix}/public
-var_spool_postfix_saved = ${:var_spool_postfix}/saved
-var_spool_postfix_trace = ${:var_spool_postfix}/trace
-var_lib = ${:var}/lib
-var_mail = ${:var}/mail
-var_lib_postfix = ${:var_lib}/postfix
-
+var-lib = ${:var}/lib
+var-lib-postfix = ${:var-lib}/postfix
+var-spool = ${:var}/spool
+var-spool-postfix = ${:var-spool}/postfix
+# Not used at buildout level, presence needed by postfix.
+var-spool-postfix-active = ${:var-spool-postfix}/active
+var-spool-postfix-bounce = ${:var-spool-postfix}/bounce
+var-spool-postfix-corrupt = ${:var-spool-postfix}/corrupt
+var-spool-postfix-defer = ${:var-spool-postfix}/defer
+var-spool-postfix-deferred = ${:var-spool-postfix}/deferred
+var-spool-postfix-flush = ${:var-spool-postfix}/flush
+var-spool-postfix-hold = ${:var-spool-postfix}/hold
+var-spool-postfix-incoming = ${:var-spool-postfix}/incoming
+var-spool-postfix-maildrop = ${:var-spool-postfix}/maildrop
+var-spool-postfix-pid = ${:var-spool-postfix}/pid
+var-spool-postfix-private = ${:var-spool-postfix}/private
+var-spool-postfix-public = ${:var-spool-postfix}/public
+var-spool-postfix-saved = ${:var-spool-postfix}/saved
+var-spool-postfix-trace = ${:var-spool-postfix}/trace
+
+[configuration]
+smtp = {{ dumps(tcpv4_port) }}
+inet-interfaces = {{ dumps(ip) }}
+alias-dict = {{ dumps(alias_dict) }}
+relayhost = {{ dumps(relay.get('host')) }}
+relay-sasl-credential = {{ dumps(relay.get('sasl-credential')) }}
+divert = {{ dumps(divert) }}
+cyrus-sasldb = ${directory:etc-cyrus}/postfix.gdbm
 
 [userinfo]
 recipe = slapos.cookbook:userinfo
 
+[smtp-sasl-passwd]
+recipe = slapos.recipe.template:jinja2
+rendered = ${directory:etc-postfix}/sasl_passwd
+{% if relay -%}
+template = inline:{{ "{{ host }} {{ sasl_credential }}" }}
+{%- else -%}
+template = inline:
+{%- endif %}
+context =
+    key host configuration:relayhost
+    key sasl_credential configuration:relay-sasl-credential
+mode = 600
 
-[postfix-main-cf]
+[{{ section('cyrus-smtpd-conf') }}]
 recipe = slapos.recipe.template:jinja2
-rendered = ${directory:etc_postfix}/main.cf
-template = {{ parameter_dict['template-postfix-main-cf'] }}
+rendered = ${directory:etc-cyrus}/smtpd.conf
+template = inline:
+    pwcheck_method: auxprop
+    mech_list: PLAIN LOGIN
+    sasldb_path: {{ '{{ sasldb }}' }}
 context =
-    raw queue_directory ${directory:var_spool_postfix}
-    raw command_directory ${directory:usr_sbin}
-    raw daemon_directory {{ parameter_dict['postfix-location'] }}/usr/libexec/postfix
-    raw data_directory ${directory:var_lib_postfix}
-    raw mail_owner ${userinfo:pw_name}
-    raw alias_database hash:${aliases:rendered}
-    raw alias_maps hash:${aliases:rendered}, nis:mail.aliases
-    raw mail_spool_directory ${directory:var_mail}
-    raw mydomain localdomain
-    raw myhostname test.localdomain
-    raw setgid_group ${userinfo:gr_name}
-    raw inet_interfaces {{ ipv4 }}
+    key sasldb configuration:cyrus-sasldb
 
+[{{ section('cyrus-smtpd-password') }}]
+recipe = plone.recipe.command
+stop-on-error = true
+command =
+  rm -f '${configuration:cyrus-sasldb}' &&
+  echo '{{ smtpd_sasl_password }}' | '${wrapper-postfix-saslpasswd2:wrapper-path}' -pc '{{ smtpd_sasl_user }}'
+update-command = ${:command}
 
-[postfix-master-cf-parameters]
-smtp_port = {{ tcpv4_port }}
+[divert]
+recipe = slapos.recipe.template:jinja2
+rendered = ${directory:etc-postfix}/divert
+{% if divert -%}
+template = inline:{{ "/.*/ {{ ', '.join(divert) }}" }}
+{%- else -%}
+template = inline:
+{%- endif %}
+context =
+    key divert configuration:divert
 
+[smtpd-ssl]
+recipe = plone.recipe.command
+stop-on-error = true
+openssl = '{{ parameter_dict['openssl'] }}/bin/openssl'
+cert = ${directory:etc-postfix}/smtpd.crt
+key = ${directory:etc-postfix}/smtpd.pem
+dh-512 = ${directory:etc-postfix}/dh512.pem
+dh-2048 = ${directory:etc-postfix}/dh2048.pem
+command =
+  ${:openssl} dhparam -out '${:dh-512}' 512 &&
+  ${:openssl} dhparam -out '${:dh-2048}' 2048 &&
+  ${:update}
+update =
+  ${:openssl} req -newkey rsa -batch -new -x509 -days 3650 -nodes -keyout '${:key}' -out '${:cert}'
 
-[postfix-master-cf]
+[{{ section('postfix-main-cf') }}]
+recipe = slapos.recipe.template:jinja2
+rendered = ${directory:etc-postfix}/main.cf
+template = {{ parameter_dict['template-postfix-main-cf'] }}
+context =
+    key bin_directory directory:bin
+    key usr_directory directory:usr
+    key queue_directory directory:var-spool-postfix
+    key data_directory directory:var-lib-postfix
+    key spool_directory directory:var-spool
+    key mail_owner userinfo:pw-name
+    key setgid_group userinfo:gr-name
+    key inet_interfaces configuration:inet-interfaces
+    key relayhost configuration:relayhost
+    key sasl_passwd typed-paths:smtp-sasl-passwd
+    key aliases typed-paths:aliases
+    key divert typed-paths:divert
+    key cyrus_directory directory:etc-cyrus
+    key cert smtpd-ssl:cert
+    key key smtpd-ssl:key
+    key dh_512 smtpd-ssl:dh-512
+    key dh_2048 smtpd-ssl:dh-2048
+
+[{{ section('postfix-master-cf') }}]
 recipe = slapos.recipe.template:jinja2
-rendered = ${directory:etc_postfix}/master.cf
+rendered = ${directory:etc-postfix}/master.cf
 template = {{ parameter_dict['template-postfix-master-cf'] }}
-context = section parameter_dict postfix-master-cf-parameters
-
+context = key smtp configuration:smtp
 
 [aliases]
 recipe = slapos.recipe.template:jinja2
-template = inline:
-  # See http://www.postfix.org/aliases.5.html for format
-rendered = ${directory:etc_postfix}/aliases
-mode = 644
+template = {{ parameter_dict['template-postfix-aliases'] }}
+rendered = ${directory:etc-postfix}/aliases
+context =
+    key alias_dict configuration:alias-dict
 
+[typed-paths]
+# Postfix-friendly rendering of file paths, prefixed with database type.
+aliases = hash:${aliases:rendered}
+smtp-sasl-passwd = hash:${smtp-sasl-passwd:rendered}
+divert = pcre:${divert:rendered}
 
-[newaliases]
+[{{ section('postalias-db') }}]
 recipe = plone.recipe.command
 stop-on-error = true
-command =
-  MAIL_CONFIG="${directory:etc_postfix}" {{ parameter_dict['postfix-location'] }}/usr/bin/newaliases
+command = '${wrapper-postalias:wrapper-path}' '${typed-paths:aliases}' '${typed-paths:smtp-sasl-passwd}'
 update-command = ${:command}
 
+[wrapper-postfix-saslpasswd2]
+recipe = slapos.cookbook:wrapper
+parameters-extra = true
+command-line = '{{ parameter_dict['cyrus-sasl-location'] }}/sbin/saslpasswd2' -f '${configuration:cyrus-sasldb}'
+wrapper-path = ${directory:bin}/saslpasswd2
 
-[postfix-symlinks-bin]
-recipe = slapos.cookbook:symbolic.link
-target-directory = ${directory:usr_bin}
-link-binary =
-    {{ parameter_dict['postfix-location'] }}/usr/bin/mailq
-    {{ parameter_dict['postfix-location'] }}/usr/bin/newaliases
-
-
-[postfix-symlinks-sbin]
-recipe = slapos.cookbook:symbolic.link
-target-directory = ${directory:usr_sbin}
-link-binary =
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postalias
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postcat
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postconf
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postdrop
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postfix
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postkick
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postlock
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postlog
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postmap
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postmulti
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postqueue
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/postsuper
-    {{ parameter_dict['postfix-location'] }}/usr/sbin/sendmail
-
-
-[postfix-symlinks-libexec]
+[base-wrapper]
+recipe = slapos.cookbook:wrapper
+environment =
+  MAIL_CONFIG=${directory:etc-postfix}
+  SASL_CONF_PATH=${directory:etc-cyrus}
+parameters-extra = true
+
+[base-bin-wrapper]
+< = base-wrapper
+command-line = ${:path}/${:basename}
+wrapper-path = ${directory:bin}/${:basename}
+
+[base-bin-bin-wrapper]
+< = base-bin-wrapper
+path = {{ parameter_dict['postfix-location'] }}/usr/bin
+
+[base-sbin-bin-wrapper]
+< = base-bin-wrapper
+path = {{ parameter_dict['postfix-location'] }}/usr/sbin
+
+{% for extend, basename_list in (
+  (
+    'base-bin-bin-wrapper',
+    (
+      'mailq',
+      'newaliases',
+    ),
+  ),
+  (
+    'base-sbin-bin-wrapper',
+    (
+      'postalias',
+      'postcat',
+      'postconf',
+      'postdrop',
+      'postfix',
+      'postkick',
+      'postlock',
+      'postlog',
+      'postmap',
+      'postmulti',
+      'postqueue',
+      'postsuper',
+      'sendmail',
+    ),
+  ),
+) %}
+{%   for basename in basename_list -%}
+[{{ section('wrapper-' ~ basename) }}]
+< = {{ extend }}
+basename = {{ basename }}
+{%   endfor %}
+{% endfor %}
+
+[{{ section('postfix-symlinks-libexec') }}]
 recipe = slapos.cookbook:symbolic.link
 target-directory = ${directory:usr}
 link-binary =
     {{ parameter_dict['postfix-location'] }}/usr/libexec
 
+[{{ section('service-postfix-master') }}]
+< = base-wrapper
+command-line = ${directory:usr}/libexec/postfix/master
+wrapper-path = ${directory:run}/postfix-master
 
-[service-postfix-master]
-recipe = slapos.cookbook:wrapper
-command-line = {{ parameter_dict['postfix-location'] }}/usr/libexec/postfix/master
-wrapper-path = ${directory:service}/start-postfix-master
-environment = MAIL_CONFIG=${directory:etc_postfix}
-
-
-[postfix-environment]
-MAIL_CONFIG=${directory:etc_postfix}
+[{{ section('postfix-promise') }}]
+recipe = slapos.cookbook:check_port_listening
+path = ${directory:promise}/postfix
+hostname = {{ ip }}
+port = {{ tcpv4_port }}
 
-
-[sh-postfix-environment]
-recipe = slapos.recipe.template:jinja2
-template = inline:
-  export MAIL_CONFIG="${directory:etc_postfix}"
-rendered = ${buildout:directory}/postfix-environment.sh
-context =
-  section postfix_environment postfix-environment
-mode = 755
-
-
-{% endif %}
+[buildout]
+extends = {{ logrotate_cfg }}
+parts =
+    {{ part_list | join('\n  ') }}
diff --git a/stack/erp5/instance-zope.cfg.in b/stack/erp5/instance-zope.cfg.in
index 97df0b32564e6ddf59d0b71845749af8835c44d3..8a82b97a1a9426494224dc4e8ef1189a9a4d5ea0 100644
--- a/stack/erp5/instance-zope.cfg.in
+++ b/stack/erp5/instance-zope.cfg.in
@@ -114,6 +114,7 @@ ipv6 = {{ ipv6 }}
     ('erp5-memcached-volatile', slapparameter_dict['memcached-url']),
     ('erp5-memcached-persistent', slapparameter_dict['kumofs-url']),
     ('erp5-cloudooo', slapparameter_dict['cloudooo-url']),
+    ('erp5-smtp', slapparameter_dict['smtp-url']),
   ) -%}
 {%   do hosts_dict.__setitem__(
        alias,
diff --git a/stack/erp5/instance.cfg.in b/stack/erp5/instance.cfg.in
index d591b05fe2b4cacafb2b8acd43b7dc5f1dac8fe3..908662864efd38be2cadf47efda8458a6bca46dd 100644
--- a/stack/erp5/instance.cfg.in
+++ b/stack/erp5/instance.cfg.in
@@ -45,7 +45,11 @@ extra-context =
     section parameter_dict dynamic-template-cloudooo-parameters
 
 [dynamic-template-postfix-parameters]
+bin-directory = {{ bin_directory }}
+cyrus-sasl-location = {{ cyrus_sasl_location }}
+openssl = {{ openssl_location }}
 postfix-location = {{ postfix_location }}
+template-postfix-aliases = {{ template_postfix_aliases }}
 template-postfix-main-cf = {{ template_postfix_main_cf }}
 template-postfix-master-cf = {{ template_postfix_master_cf }}
 
@@ -56,6 +60,7 @@ filename = instance-postfix.cfg
 extensions = jinja2.ext.do
 extra-context =
     section parameter_dict dynamic-template-postfix-parameters
+    import urllib urllib
 
 [dynamic-template-erp5-parameters]
 local-bt5-repository = {{ local_bt5_repository }}
diff --git a/stack/erp5/postfix_aliases.in b/stack/erp5/postfix_aliases.in
new file mode 100644
index 0000000000000000000000000000000000000000..dfb8b3cdb6b44fea0c795b6efa7d6bbd7c220679
--- /dev/null
+++ b/stack/erp5/postfix_aliases.in
@@ -0,0 +1,4 @@
+# See http://www.postfix.org/aliases.5.html for format
+{% for name, alias_list in alias_dict.items() -%}
+{{ name }}: {{ alias_list | join(', ') }}
+{% endfor %}
diff --git a/stack/erp5/postfix_main.cf.in b/stack/erp5/postfix_main.cf.in
index 8ad9e178ab67ab19e7e3f01ac5c53ab7ed752804..e438f59949e3f88957160468a01efe0088c32119 100644
--- a/stack/erp5/postfix_main.cf.in
+++ b/stack/erp5/postfix_main.cf.in
@@ -1,17 +1,13 @@
 # http://www.postfix.org/STANDARD_CONFIGURATION_README.html
 # http://www.postfix.org/postconf.5.html
 queue_directory = {{ queue_directory }}
-command_directory = {{ command_directory }}
-daemon_directory = {{ daemon_directory }}
+command_directory = {{ bin_directory }}
+daemon_directory = {{ usr_directory }}/libexec/postfix
 data_directory = {{ data_directory }}
 mail_owner = {{ mail_owner }}
-myhostname = {{ myhostname }}
-mydomain = {{ mydomain }}
-unknown_local_recipient_reject_code = 550
-alias_maps = {{ alias_maps }}
-alias_database = {{ alias_database }}
-mail_spool_directory = {{ mail_spool_directory }}
-debug_peer_level = 2
+alias_maps = {{ aliases }}
+alias_database = {{ aliases }}
+mail_spool_directory = {{ spool_directory }}
 sendmail_path =
 newaliases_path =
 mailq_path =
@@ -20,5 +16,45 @@ html_directory =
 manpage_directory =
 sample_directory =
 readme_directory =
-inet_protocols = ipv4
 inet_interfaces = {{ inet_interfaces }}
+virtual_alias_maps = {{ divert }}
+
+# Compared to default:
+# - remove X-related variables, irrelevant for slapos, to be concise
+# - add SASL_CONF_PATH to have per-partition cyrus-sasl configuration
+import_environment =
+  MAIL_CONFIG MAIL_DEBUG MAIL_LOGTAG TZ LANG=C
+  SASL_CONF_PATH
+
+# Mandatory sasl auth over TLS
+# XXX: no man-in-the-middle protection
+smtpd_tls_cert_file = {{ cert }}
+smtpd_tls_key_file = {{ key }}
+smtpd_tls_dh512_param_file = {{ dh_512 }}
+{#
+  Note: 1024 vs. 2048 is not a typo, but what is actually recommended in
+  postfix documentation
+-#}
+smtpd_tls_dh1024_param_file = {{ dh_2048 }}
+
+smtpd_tls_security_level = encrypt
+smtpd_sasl_auth_enable = yes
+# Reject as many bogus cases as soon as possible, so errors are visible to ERP5
+# developper rather than relying on bounces.
+smtpd_recipient_restrictions =
+  reject_non_fqdn_recipient
+  reject_unknown_recipient_domain
+  permit_sasl_authenticated
+  reject
+
+# Disable local delivery
+local_transport = error
+
+{% if relayhost -%}
+relayhost = {{ relayhost }}
+smtp_tls_security_level = encrypt
+smtp_tls_session_cache_database = btree:{{ data_directory }}/smtp_scache
+smtp_sasl_auth_enable = yes
+smtp_sasl_password_maps = {{ sasl_passwd }}
+smtp_sasl_tls_security_options = noanonymous
+{%- endif %}
diff --git a/stack/erp5/postfix_master.cf.in b/stack/erp5/postfix_master.cf.in
index 512798f2cabc074902060569bbd2db3c2a6c887c..d31f20f0b35cdd9905e7a67bc84ba18cf55953cb 100644
--- a/stack/erp5/postfix_master.cf.in
+++ b/stack/erp5/postfix_master.cf.in
@@ -1,10 +1,9 @@
-{% set smtp = parameter_dict['smtp_port'] -%}
+# http://www.postfix.org/master.5.html
 # ==========================================================================
 # service type  private unpriv  chroot  wakeup  maxproc command + args
 #               (yes)   (yes)   (yes)   (never) (100)
 # ==========================================================================
-{{smtp}}      inet  n       -       n       -       -       smtpd
-#submission inet n       -       n       -       -       smtpd
+{{ smtp }}      inet  n       -       n       -       -       smtpd
 pickup    unix  n       -       n       60      1       pickup
 cleanup   unix  n       -       n       -       0       cleanup
 qmgr      unix  n       -       n       300     1       qmgr