diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..485dee6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea
diff --git a/README.md b/README.md
index b20d731..e748849 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,47 @@
-# role-common-send_only_mta
+[//]: # (SPDX-License-Identifier: MIT)
+# Role Name
-Configure Postfix on Debian as a send-only Mail Transfer Agent
\ No newline at end of file
+role-common-send_only_mta
+
+# Description
+
+Configure Postfix on Debian as a send-only Mail Transfer Agent.
+
+# Requirements
+
+Your target machines must be Debian.
+
+# Role Variables
+
+Per [defaults/main.yml](defaults/main.yml) this role expects both a recipient e-mail address and login credentials for an SMTP mail submission server to be available in a HashiCorp Vault instance. Feel free to override these variables as needed with host or group vars. If you stick with HashiCorp Vault the default path for SMTP credentials is `settings/comms/e-mail/default/sender`, the default path for a recipient address is `settings/comms/e-mail/default/recipient`.
+
+## Sender data
+
+- `addr-spec`: Your sender e-mail address rendered as an [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1) `addr-spec` string such as `noreply@example.com`.
+- `credentials-password-sasl-smtp-auth-login`: A password that Postfix will use to log in to the SMTP mail submission server via the `AUTH LOGIN` SMTP SASL authentication mechanism.
+- `credentials-username`: The SMTP username needed to log in to the SMTP mail submission server.
+- `submission-server-fqdn`: Your upstream SMTP mail submission server's fully qualified domain name such as `smtp.example.com`.
+- `submission-server-port`: The TCP port you want Postfix to use to connect to the SMTP mail submission server.
+
+## Recipient data
+
+- `addr-spec`: Same as above, a recipient e-mail address rendered as an [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1) `addr-spec` string such as `noreply@example.com`.
+
+# Dependencies
+
+None.
+
+# Example Playbook
+
+In your `playbook.yml` call it like so:
+
+```
+- name: 'Awesome playbook'
+ hosts: all
+ roles:
+ - 'role-common-send_only_mta'
+```
+
+# License
+
+MIT
diff --git a/defaults/main.yml b/defaults/main.yml
new file mode 100644
index 0000000..d5172f0
--- /dev/null
+++ b/defaults/main.yml
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: MIT
+somta__e_mail_default_recipient_addr_spec: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/recipient:addr-spec'') }}'
+somta__e_mail_default_sender_addr_spec: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:addr-spec'') }}'
+somta__e_mail_default_sender_credentials_password_smtp_auth_login: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:credentials-password-sasl-smtp-auth-login'') }}'
+somta__e_mail_default_sender_credentials_username: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:credentials-username'') }}'
+somta__e_mail_default_sender_submission_server_fqdn: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:submission-server-fqdn'') }}'
+somta__e_mail_default_sender_submission_server_port: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:submission-server-port'') }}'
diff --git a/handlers/main.yml b/handlers/main.yml
new file mode 100644
index 0000000..def3e82
--- /dev/null
+++ b/handlers/main.yml
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: MIT
+- name: 'Rebuild Postfix lookup tables'
+ loop_control:
+ loop_var: 'somta_postfix_postmap_handler'
+ label: 'Rebuild ''/etc/postfix/{{ somta_postfix_postmap_handler.lookup_table_source_file }}'' lookup table'
+ loop:
+ - { lookup_table_source_file: 'sasl_passwd.j2' }
+ - { lookup_table_source_file: 'sender_canonical_maps.j2' }
+ - { lookup_table_source_file: 'smtp_header_checks' }
+ ansible.builtin.shell: |
+ postmap '/etc/postfix/{{ somta_postfix_postmap_handler.lookup_table_source_file }}'
+ listen: 'Ensure that a Mail Transfer Agent is running with newest config'
+
+- name: 'Rebuild e-mail aliases lookup tables'
+ ansible.builtin.shell: |
+ newaliases
+ listen: 'Ensure that a Mail Transfer Agent is running with newest config'
+
+- name: 'Restart postfix.service'
+ ansible.builtin.service:
+ name: 'postfix.service'
+ state: 'restarted'
+ enabled: true
+ listen: 'Ensure that a Mail Transfer Agent is running with newest config'
diff --git a/tasks/main.yml b/tasks/main.yml
new file mode 100644
index 0000000..6f4441f
--- /dev/null
+++ b/tasks/main.yml
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: MIT
+- import_tasks: postfix-setup.yml
diff --git a/tasks/postfix-setup.yml b/tasks/postfix-setup.yml
new file mode 100644
index 0000000..dd3c0b3
--- /dev/null
+++ b/tasks/postfix-setup.yml
@@ -0,0 +1,78 @@
+# SPDX-License-Identifier: MIT
+- name: 'If OS is a Linux flavor install Postfix'
+ when: ansible_facts['system'] | lower == 'linux'
+ ansible.builtin.package:
+ name:
+ - 'postfix'
+ - 'postfix-pcre'
+ state: 'present'
+
+- name: 'Ensure Postfix lookup table files exist with correct perms'
+ loop_control:
+ loop_var: 'somta_postfix_postconf_lookup_table'
+ label: 'Copy lookup table file ''/etc/postfix/{{ somta_postfix_postconf_lookup_table.file }}'' and set perms'
+ loop:
+ - { mode: '0600', file: 'sasl_passwd' }
+ - { mode: '0644', file: 'sender_canonical_maps' }
+ - { mode: '0644', file: 'smtp_header_checks' }
+ ansible.builtin.template:
+ src: 'etc/postfix/{{ somta_postfix_postconf_lookup_table.file }}.j2'
+ dest: '/etc/postfix/{{ somta_postfix_postconf_lookup_table.file }}'
+ mode: '{{ somta_postfix_postconf_lookup_table.mode }}'
+ notify:
+ - 'Ensure that a Mail Transfer Agent is running with newest config'
+
+- name: 'Add e-mail alias for user ''root'''
+ ansible.builtin.lineinfile:
+ path: '/etc/aliases'
+ insertafter: 'EOF'
+ regexp: '^root:.*'
+ line: 'root: {{ somta__e_mail_default_recipient_addr_spec }}'
+ notify:
+ - 'Ensure that a Mail Transfer Agent is running with newest config'
+
+# Add our own config block to the end of Postfix' main.cf file. In
+# 'ansible.builtin.blockinfile' we use the default 'marker' param '#
+# {mark} ANSIBLE MANAGED BLOCK'. We 'insertafter: EOF' so we know for a
+# fact that our config block is the bottommost thing in main.cf. The
+# next task 'ansible.builtin.replace' uses the marker string as an
+# anchor to comment out any duplicate parameters /before/ the marker.
+- name: 'Configure Postfix main.cf to SMTP-deliver e-mails to an upstream mail gateway'
+ ansible.builtin.blockinfile:
+ block: "{{ lookup('ansible.builtin.template', 'etc/postfix/main.cf.blockinfile.j2') }}"
+ path: '/etc/postfix/main.cf'
+ create: true
+ insertafter: 'EOF'
+ prepend_newline: true
+ notify:
+ - 'Ensure that a Mail Transfer Agent is running with newest config'
+
+- name: 'In Postfix main.cf comment out params managed by this playbook; Postfix doesn''t like dupes'
+ loop_control:
+ label: 'Comment out unmanaged occurrences of param ''{{ item | regex_replace(''^(#\s?)?(?P[^=\s]+)([^\r\n\f]*)'', ''\g'') }}'''
+# Look up file content from our main.cf config template file. Split the
+# result by line delimiters into a list that contains each line as a
+# list item via Python string splitlines() method. Now that we have a
+# list apply the Jinja2 'select' filter to it. For each list item filter
+# it by using the Jinja2 built-in test 'search' against it to search for
+# an occurrence of the equals sign '=' in that list item. When a config
+# line (i.e. a list item) does not contain an equals sign we reject it
+# thus pruning it from the list. We lastly generate a new list from our
+# result, one that only contains lines where an equals sign appears.
+ loop: '{{ lookup(''ansible.builtin.template'', ''etc/postfix/main.cf.blockinfile.j2'').splitlines() | select(''search'', ''='') | list }}'
+ ansible.builtin.replace:
+ path: '/etc/postfix/main.cf'
+ before: '.*?# BEGIN ANSIBLE MANAGED BLOCK'
+ # regex_replace each {{ item }}. Instead of one complete line from
+ # the main.cf template file we only want the name of each parameter;
+ # that's whatever appears in front of the first equals sign ('=') in
+ # that line minus any comment markers ('#') we may have put in our
+ # our main.cf template. Store the param name in a named capture
+ # group (?P...) - with a capital letter P because this
+ # behavior is a Python-specific regex extension
+ # (https://stackoverflow.com/a/10060065) - and lastly reuse
+ # '\g' as our 'regexp:' string.
+ regexp: '^(#\s?)?({{ item | regex_replace(''^(#\s?)?(?P[^=\s]+)([^\r\n\f]*)'', ''\g'') }})'
+ replace: '# \2'
+ notify:
+ - 'Ensure that a Mail Transfer Agent is running with newest config'
diff --git a/templates/etc/postfix/main.cf.blockinfile.j2 b/templates/etc/postfix/main.cf.blockinfile.j2
new file mode 100644
index 0000000..b93d36c
--- /dev/null
+++ b/templates/etc/postfix/main.cf.blockinfile.j2
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: MIT
+# Per 'man 5 postconfig': When the same parameter is defined multiple
+# times, only the last instance is remembered.
+#
+# While that's true we still do not want to duplicate params that
+# originally exist in main.cf. If we did and inevitably set our params
+# to different values than the default main.cf params Postfix tools such
+# as mailq/sendmail, postqueue etc. would warn us that we're duplicating
+# and reconfiguring params multiple times. Tools would begin reporting
+# 'warning: main.cf, line : overriding earlier entry'. This is a
+# useful feature for when some config /actually/ goes wrong so we don't
+# want to contaminate troubleshooting by forcing this behavior with our
+# Ansible-managed config.
+#
+# For each of the params below we've gone ahead and commented out the
+# same param where it appeared before this Ansible-managed config block.
+# That way we're avoiding param duplicates.
+myhostname = {{ ansible_fqdn }}
+mydestination = $myhostname, localhost.$mydomain, localhost
+inet_interfaces = loopback-only
+relayhost = [{{ somta__e_mail_default_sender_submission_server_fqdn }}]:{{ somta__e_mail_default_sender_submission_server_port }}
+sender_canonical_classes = envelope_sender, header_sender
+sender_canonical_maps = pcre:/etc/postfix/sender_canonical_maps
+smtp_header_checks = pcre:/etc/postfix/smtp_header_checks
+smtp_sasl_auth_enable = yes
+smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
+smtp_sasl_tls_security_options = noanonymous
+smtp_tls_security_level = encrypt
+# debug_peer_list = example.com, mail.example.net, 1.2.3.4
diff --git a/templates/etc/postfix/sasl_passwd.j2 b/templates/etc/postfix/sasl_passwd.j2
new file mode 100644
index 0000000..838e43f
--- /dev/null
+++ b/templates/etc/postfix/sasl_passwd.j2
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: MIT
+[{{ somta__e_mail_default_sender_submission_server_fqdn }}]:{{ somta__e_mail_default_sender_submission_server_port }} {{ somta__e_mail_default_sender_credentials_username }}:{{ somta__e_mail_default_sender_credentials_password_smtp_auth_login }}
diff --git a/templates/etc/postfix/sender_canonical_maps.j2 b/templates/etc/postfix/sender_canonical_maps.j2
new file mode 100644
index 0000000..2f285c3
--- /dev/null
+++ b/templates/etc/postfix/sender_canonical_maps.j2
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: MIT
+/.+/ {{ somta__e_mail_default_sender_addr_spec }}
diff --git a/templates/etc/postfix/smtp_header_checks.j2 b/templates/etc/postfix/smtp_header_checks.j2
new file mode 100644
index 0000000..b780049
--- /dev/null
+++ b/templates/etc/postfix/smtp_header_checks.j2
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: MIT
+# In our 'From:' header reuse any mail display name then append our
+# official sender e-mail address.
+/^From:[[:space:]]*(.+?)([[:space:]]*<)/ REPLACE From: "${1}" <{{ somta__e_mail_default_sender_addr_spec }}>
+
+# Hide the sender's IP and user agent in the Received header
+# https://wiki.archlinux.org/title/Postfix
+/^Received:.*/ IGNORE
+/^User-Agent:.*/ IGNORE