mail_relay: postfix relays all mail through the mailcow submission port with a mailbox per host, replaces mail_aliases

This commit is contained in:
2026-10-26 09:44:37 +00:00
parent c91c56b153
commit 885e6f3338
7 changed files with 235 additions and 29 deletions
+18
View File
@@ -0,0 +1,18 @@
---
# Every host sends its mail through the mailcow submission port with its own mailbox
# (doc/host-mail-relay.md in the kubernetes repo). The password is set in host_vars.
mail_relay:
host: tranio.ruekov.eu
port: 587
# SASL login of the mailcow mailbox; the password is an SMTP-only app password of it.
username: "{{ inventory_hostname_short }}.system@mail.ruekov.eu"
# Sender address every local address is rewritten to; mailcow accepts only addresses of the login.
sender: "{{ mail_relay.username }}"
password: ""
# Root mail goes to this address: /etc/aliases and, on Proxmox hosts, the email of root@pam.
root_mail: admin@ruekov.eu
myhostname: "{{ inventory_hostname }}"
# The local postfix only takes mail from the host itself.
inet_interfaces: loopback-only
# The smtp inet service of master.cf; off where another daemon holds port 25 (tranio: mailcow).
smtpd_listener: true
+5
View File
@@ -0,0 +1,5 @@
---
- name: Restart postfix
ansible.builtin.service:
name: postfix
state: restarted
+208
View File
@@ -0,0 +1,208 @@
---
- name: Mailbox password is set
ansible.builtin.assert:
that: mail_relay.password | length > 0
fail_msg: "mail_relay.password of {{ inventory_hostname }} is empty (host_vars)"
quiet: true
- name: Postfix state
ansible.builtin.stat:
path: /usr/sbin/postfix
register: mail_relay_postfix
- name: Exim state
ansible.builtin.stat:
path: /usr/sbin/exim4
register: mail_relay_exim
# Postfix replaces exim4 as the mail-transport-agent; the package scripts do not stop a running exim4
# while policy-rc.d denies service actions, and its listener would block postfix on port 25.
- name: Stop exim4
ansible.builtin.systemd_service:
name: exim4
state: stopped
enabled: false
when: mail_relay_exim.stat.exists and not mail_relay_postfix.stat.exists
- name: Preseed postfix
ansible.builtin.debconf:
name: postfix
question: "{{ item.question }}"
vtype: "{{ item.vtype }}"
value: "{{ item.value }}"
loop:
- { question: postfix/main_mailer_type, vtype: select, value: Internet Site }
- { question: postfix/mailname, vtype: string, value: "{{ mail_relay.myhostname }}" }
loop_control:
label: "{{ item.question }}"
when: not mail_relay_postfix.stat.exists
# A third-party repository with a bad signature makes the refresh fail on some hosts; the install
# below still fails if the index lacks the packages.
- name: Refresh apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
failed_when: false
# The package starts postfix with the Debian defaults, which listen on all interfaces; on a host whose
# port 25 is taken (tranio: mailcow) that start fails. policy-rc.d keeps the start until the
# configuration is in place.
- name: Install postfix
ansible.builtin.apt:
name:
- postfix
- libsasl2-modules
- ca-certificates
policy_rc_d: "{{ omit if mail_relay_postfix.stat.exists else 101 }}"
- name: Postfix configuration
when: not ansible_check_mode or mail_relay_postfix.stat.exists
block:
- name: Local names of the host
ansible.builtin.set_fact:
mail_relay_local_names: >-
{{ [mail_relay.myhostname, inventory_hostname, inventory_hostname_short, ansible_facts['hostname'],
'localhost', 'localhost.localdomain'] | unique }}
- name: Sender rewrite map
ansible.builtin.template:
src: generic.regexp.j2
dest: /etc/postfix/generic
owner: root
group: root
mode: "0644"
notify: Restart postfix
- name: Relay settings in main.cf
ansible.builtin.lineinfile:
path: /etc/postfix/main.cf
regexp: "^{{ item.key }}\\s*="
line: "{{ item.key }} = {{ item.value }}"
loop: "{{ mail_relay_settings | dict2items }}"
vars:
mail_relay_settings:
myhostname: "{{ mail_relay.myhostname }}"
myorigin: $myhostname
inet_interfaces: "{{ mail_relay.inet_interfaces }}"
relayhost: "[{{ mail_relay.host }}]:{{ mail_relay.port }}"
smtp_sasl_auth_enable: "yes"
smtp_sasl_password_maps: hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options: noanonymous
smtp_tls_security_level: verify
smtp_tls_CAfile: /etc/ssl/certs/ca-certificates.crt
smtp_generic_maps: regexp:/etc/postfix/generic
loop_control:
label: "{{ item.key }}"
notify: Restart postfix
- name: SASL credentials
ansible.builtin.copy:
dest: /etc/postfix/sasl_passwd
content: "[{{ mail_relay.host }}]:{{ mail_relay.port }} {{ mail_relay.username }}:{{ mail_relay.password }}\n"
owner: root
group: root
mode: "0600"
no_log: true
register: mail_relay_sasl
- name: SASL database state
ansible.builtin.stat:
path: /etc/postfix/sasl_passwd.db
register: mail_relay_sasl_db
# A task instead of a handler: a failed run would drop the notification and leave the database missing.
- name: Rebuild sasl_passwd database
ansible.builtin.command:
cmd: postmap /etc/postfix/sasl_passwd
when: mail_relay_sasl.changed or not mail_relay_sasl_db.stat.exists
changed_when: true
notify: Restart postfix
# inet_interfaces cannot switch the listener off ("none" is resolved as a host name): the service
# is commented out in master.cf instead.
- name: SMTP listener in master.cf
ansible.builtin.lineinfile:
path: /etc/postfix/master.cf
regexp: "^#?smtp\\s+inet\\s+n\\s+-\\s+y\\s+-\\s+-\\s+smtpd\\s*$"
line: "{{ '' if mail_relay.smtpd_listener else '#' }}smtp inet n - y - - smtpd"
notify: Restart postfix
- name: Root alias
ansible.builtin.lineinfile:
path: /etc/aliases
regexp: "^root:"
line: "root: {{ mail_relay.root_mail }}"
create: true
owner: root
group: root
mode: "0644"
- name: Alias file state
ansible.builtin.stat:
path: /etc/aliases
register: mail_relay_aliases
- name: Alias database state
ansible.builtin.stat:
path: /etc/aliases.db
register: mail_relay_aliases_db
# Postfix reads /etc/aliases through the compiled /etc/aliases.db; without a current db local
# delivery defers or misroutes. A task instead of a handler, so a failed run cannot lose the rebuild.
- name: Rebuild alias database
ansible.builtin.command:
cmd: newaliases
when: >-
not mail_relay_aliases_db.stat.exists
or mail_relay_aliases_db.stat.mtime < mail_relay_aliases.stat.mtime
changed_when: true
- name: Apply pending changes
ansible.builtin.meta: flush_handlers
- name: SASL database permissions
ansible.builtin.file:
path: /etc/postfix/sasl_passwd.db
owner: root
group: root
mode: "0600"
- name: Postfix running
ansible.builtin.service:
name: postfix
state: started
enabled: true
- name: Proxmox state
ansible.builtin.stat:
path: /usr/sbin/pveum
register: mail_relay_pve
# The built-in mail-to-root notification target mails the email of root@pam.
- name: Email of root@pam
when: mail_relay_pve.stat.exists
block:
- name: Read root@pam
ansible.builtin.command:
cmd: pvesh get /access/users/root@pam --output-format json
register: mail_relay_pve_root
changed_when: false
check_mode: false
- name: Set the email of root@pam
ansible.builtin.command:
cmd: "pveum user modify root@pam --email {{ mail_relay.root_mail }}"
when: (mail_relay_pve_root.stdout | from_json).email | default('') != mail_relay.root_mail
# Run with --tags mail_relay_test: the mail takes the root alias through the relay.
- name: Test mail to root
ansible.builtin.command:
cmd: sendmail -t
stdin: |
To: root
Subject: mail_relay test from {{ inventory_hostname }}
Test mail of the mail_relay role.
changed_when: true
tags: [never, mail_relay_test]
+4
View File
@@ -0,0 +1,4 @@
# mailcow rejects a sender that does not belong to the login of the submission, so every address of
# this host is rewritten to the mailbox. The map also applies to recipient addresses: only the names
# of this host match, never a recipient such as admin@ruekov.eu.
/^[^@]+@({{ mail_relay_local_names | map('regex_escape') | join('|') }})$/ {{ mail_relay.sender }}