diff --git a/defaults/main.yml b/defaults/main.yml index fcf8641..a1e3a32 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -15,7 +15,7 @@ postfix: transport_maps: [] sender_dependent_relayhost_maps: '' alias_maps: '' - virtual_alias_maps: '' + virtual_alias_maps: hash:/etc/postfix/virtual virtual_regex: [] virtual_mailbox_domains: [] mynetworks: [] @@ -41,6 +41,12 @@ postfix: mailboxes: mailMessageStore check_dovecot_quota: false postfixmaps: [] + aliases: [] + sender_verification: + enable: false # enable/disable the restriction, 'test' to warn instead of reject + match_sender_domain: '' + allow_sender_domains: [] + smtpd_sender_login_maps: hash:/etc/postfix/sender_logins add_header_checks: [] smtpd_milters: [] non_smtpd_milters: [] diff --git a/filter_plugins/filters.py b/filter_plugins/filters.py new file mode 100644 index 0000000..44a46b9 --- /dev/null +++ b/filter_plugins/filters.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +def validate_aliases(aliases): + for alias in aliases: + unknown_attributes = alias.keys() - {'addresses', 'senders', 'receivers', 'senders_and_receivers'} + if unknown_attributes: + raise ValueError('Unknown alias attributes: ' + ', '.join(unknown_attributes)) + for address in alias['addresses']: + if not address.islower(): + raise ValueError(f'Aliases are not case-sensitive. Make "{address}" all-lowercase!') + if address.strip() != address: + raise ValueError(f'Remove surrounding whitespace in "{address}"!') + +class FilterModule(object): + def filters(self): + return { + 'postfix_resolve_senders': self.postfix_resolve_senders, + 'postfix_resolve_receivers': self.postfix_resolve_receivers, + } + + def postfix_resolve_senders(self, aliases, match_sender_domain, allow_sender_domains=tuple()): + validate_aliases(aliases) + sender_suffix = '@' + match_sender_domain + address_sender_address_map = {} + for alias in aliases: + for address in alias['addresses']: + address_sender_address_map.setdefault(address, set()) + address_sender_address_map[address] |= set(alias.get('senders', [])) + address_sender_address_map[address] |= set(alias.get('senders_and_receivers', [])) + for _ in range(100): + done = True + for address, senders in address_sender_address_map.items(): + for sender in set(senders): + if sender.endswith(sender_suffix): + continue + done = False + if sender not in address_sender_address_map: + raise Exception(f'Address does not resolve to sender address: {sender}') + senders.remove(sender) + senders |= address_sender_address_map[sender] + if done: + break + if not done: + raise Exception('Recursion limit reached') + + address_sender_name_map = {} + for address, senders in address_sender_address_map.items(): + address_sender_name_map[address] = set() + for sender in senders: + username = sender[:-len(sender_suffix)] + address_sender_name_map[address].add(username) + for domain in allow_sender_domains: + address_sender_name_map[address].add(f'{username}@{domain}') + return address_sender_name_map + + def postfix_resolve_receivers(self, aliases): + validate_aliases(aliases) + address_receiver_address_map = {} + for alias in aliases: + for address in alias['addresses']: + address_receiver_address_map.setdefault(address, set()) + address_receiver_address_map[address] |= set(alias.get('receivers', [])) + address_receiver_address_map[address] |= set(alias.get('senders_and_receivers', [])) + return address_receiver_address_map diff --git a/tasks/main.yml b/tasks/main.yml index 22d83fd..1bdc0d4 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -95,3 +95,49 @@ with_items: "{{ postfix.postfixmaps }}" notify: - restart postfix + +- name: create virtual table + template: + src: virtual.in.j2 + dest: /etc/postfix/virtual.in + owner: root + group: root + mode: 0644 + register: create_virtual_table + +- name: check virtual table db + loop: + - /etc/postfix/virtual.in + - /etc/postfix/virtual.db + ansible.builtin.stat: + path: '{{ item }}' + register: virtual_table_stats + +- name: update virtual table db + when: create_virtual_table.changed + or not virtual_table_stats.results[1].stat.exists + or virtual_table_stats.results[0].stat.mtime > virtual_table_stats.results[1].stat.mtime + ansible.builtin.shell: 'postmap hash:/etc/postfix/virtual.in && mv /etc/postfix/virtual.in.db /etc/postfix/virtual.db' + +- name: create sender logins table + template: + src: sender_logins.in.j2 + dest: /etc/postfix/sender_logins.in + owner: root + group: root + mode: 0644 + register: create_sender_logins_table + +- name: check sender logins table db + loop: + - /etc/postfix/sender_logins.in + - /etc/postfix/sender_logins.db + ansible.builtin.stat: + path: '{{ item }}' + register: sender_logins_table_stats + +- name: update sender logins table db + when: create_sender_logins_table.changed + or not sender_logins_table_stats.results[1].stat.exists + or sender_logins_table_stats.results[0].stat.mtime > sender_logins_table_stats.results[1].stat.mtime + ansible.builtin.shell: 'postmap hash:/etc/postfix/sender_logins.in && mv /etc/postfix/sender_logins.in.db /etc/postfix/sender_logins.db' diff --git a/templates/main.cf.j2 b/templates/main.cf.j2 index 30f190a..be88708 100644 --- a/templates/main.cf.j2 +++ b/templates/main.cf.j2 @@ -69,7 +69,12 @@ smtpd_helo_restrictions = permit_mynetworks, smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain, - # reject_sender_login_mismatch, # Disabled because we don’t map correctly + {% if postfix.sender_verification.enable == 'test' -%} + warn_if_reject, + reject_authenticated_sender_login_mismatch, + {% elif postfix.sender_verification.enable -%} + reject_authenticated_sender_login_mismatch, + {% endif -%} permit_mynetworks, permit_sasl_authenticated @@ -91,12 +96,20 @@ mua_helo_restrictions = permit_mynetworks, mua_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain, + {% if postfix.sender_verification.enable == 'test' -%} + warn_if_reject, + reject_authenticated_sender_login_mismatch, + {% elif postfix.sender_verification.enable -%} + reject_authenticated_sender_login_mismatch, + {% endif -%} permit_mynetworks, permit_sasl_authenticated mua_client_restrictions = permit_sasl_authenticated, reject +smtpd_sender_login_maps = {{ postfix.smtpd_sender_login_maps }} + {% if ("mailbox_transport" in postfix and postfix.mailbox_transport == "dovecot") or postfix.ldap.enable %} diff --git a/templates/sender_logins.in.j2 b/templates/sender_logins.in.j2 new file mode 100644 index 0000000..83ff5af --- /dev/null +++ b/templates/sender_logins.in.j2 @@ -0,0 +1,7 @@ +# {{ ansible_managed }} +{% for alias, senders in postfix.aliases|postfix_resolve_senders(postfix.sender_verification.match_sender_domain, postfix.sender_verification.allow_sender_domains)|dictsort if senders %} +{{ alias }} +{% for sender in senders|sort %} + {{ sender }} +{% endfor %} +{% endfor %} diff --git a/templates/virtual.in.j2 b/templates/virtual.in.j2 new file mode 100644 index 0000000..68473ab --- /dev/null +++ b/templates/virtual.in.j2 @@ -0,0 +1,7 @@ +# {{ ansible_managed }} +{% for alias, receivers in postfix.aliases|postfix_resolve_receivers|dictsort if receivers %} +{{ alias }} +{% for receiver in receivers|sort %} + {{ receiver }} +{% endfor %} +{% endfor %}