mail_relay: postfix relays all mail through the mailcow submission port with a mailbox per host, replaces mail_aliases
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
# Entries of /etc/aliases managed here: name -> recipient.
|
|
||||||
# Root mail goes to the admin mailbox (doc/host-mail-relay.md in the kubernetes repo).
|
|
||||||
mail_aliases:
|
|
||||||
root: admin@ruekov.eu
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Rebuild alias database
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: newaliases
|
|
||||||
changed_when: true
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
# Postfix reads /etc/aliases through the compiled /etc/aliases.db. Without the db, local delivery
|
|
||||||
# defers every mail with "alias database unavailable".
|
|
||||||
- name: Alias entries
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/aliases
|
|
||||||
regexp: "^{{ item.key }}:"
|
|
||||||
line: "{{ item.key }}: {{ item.value }}"
|
|
||||||
create: true
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
loop: "{{ mail_aliases | dict2items }}"
|
|
||||||
notify: Rebuild alias database
|
|
||||||
|
|
||||||
- name: Alias database exists
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: newaliases
|
|
||||||
creates: /etc/aliases.db
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: Restart postfix
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: postfix
|
||||||
|
state: restarted
|
||||||
@@ -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]
|
||||||
@@ -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 }}
|
||||||
Reference in New Issue
Block a user