From 87ce53d1d307a2560f1d874cdc17e31722408d3a Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 17:24:59 -0300 Subject: [PATCH 01/11] feat: add support for Dovecot IMAP/POP3 configuration Introduces functionality to install and configure Dovecot alongside Postfix to provide IMAP/POP3 services. Changes include: - Added tasks to install Dovecot packages (core, imapd, pop3d, lmtpd). - Added templates for main configuration and conf.d files (auth, master, ssl, mail). - Defined default variables for protocols, SSL settings, and Maildir location. - Enabled Postfix SASL and LMTP integration options. - Added a handler to restart the Dovecot service. - Updated README.md with the new configuration variables and usage instructions. --- .ansible/.lock | 0 README.md | 16 ++++++++++++ defaults/main.yml | 26 ++++++++++++++++++- handlers/main.yml | 5 ++++ tasks/main.yml | 39 +++++++++++++++++++++++++++- templates/10-auth.conf.j2 | 7 +++++ templates/10-mail.conf.j2 | 7 +++++ templates/10-master.conf.j2 | 51 +++++++++++++++++++++++++++++++++++++ templates/10-ssl.conf.j2 | 6 +++++ templates/dovecot.conf.j2 | 7 +++++ templates/main.cf.j2 | 18 +++++++++++++ tests/test.yml | 2 +- 12 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 .ansible/.lock create mode 100644 templates/10-auth.conf.j2 create mode 100644 templates/10-mail.conf.j2 create mode 100644 templates/10-master.conf.j2 create mode 100644 templates/10-ssl.conf.j2 create mode 100644 templates/dovecot.conf.j2 diff --git a/.ansible/.lock b/.ansible/.lock new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index eaf998e..c65da5b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,22 @@ The role's behavior can be customized using the following variables. The default | postfix_inet_interfaces | all | The network interfaces Postfix listens on. Set to loopback-only to only accept mail from the server itself. | | postfix_inet_protocols | all | The IP protocols to use (ipv4, ipv6, or all). | +### **Dovecot Configuration** + +The role now supports installing and configuring Dovecot for IMAP/POP3 services. + +| Variable | Default Value | Description | +| :---- | :---- | :---- | +| dovecot_enabled | true | Whether to install and configure Dovecot. | +| dovecot_protocols | "imap pop3 lmtp" | Protocols to enable. | +| dovecot_mail_location | "maildir:~/Maildir" | Mail storage location. | +| dovecot_ssl | "yes" | SSL/TLS configuration (yes, no, required). | +| dovecot_ssl_cert | snakeoil | Path to SSL certificate. | +| dovecot_ssl_key | snakeoil | Path to SSL key. | +| dovecot_auth_mechanisms | "plain login" | Authentication mechanisms. | +| dovecot_postfix_sasl_enable | true | Enable Postfix SASL authentication via Dovecot. | +| dovecot_postfix_lmtp_enable | true | Enable Postfix delivery via Dovecot LMTP. | + ### **SASL Authentication** SASL authentication for the smarthost is **automatically enabled** if both postfix_relayhost_user and postfix_relayhost_password are defined. If they are not defined, Postfix will attempt to send mail without authentication. diff --git a/defaults/main.yml b/defaults/main.yml index 4a95e62..b6ad1c6 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -34,4 +34,28 @@ postfix_relayhost: "" # Optional credentials for the relayhost. If these are defined, # SASL authentication will be automatically configured. # postfix_relayhost_user: "apikey" -# postfix_relayhost_password: "YourVeryLongAndComplexApiKey" \ No newline at end of file +# postfix_relayhost_password: "YourVeryLongAndComplexApiKey" + +# --- Dovecot Configuration --- + +# Whether to install and configure Dovecot +dovecot_enabled: true + +# Protocols to enable (imap, pop3, lmtp) +dovecot_protocols: "imap pop3 lmtp" + +# Mail storage location. Using Maildir in the user's home directory. +dovecot_mail_location: "maildir:~/Maildir" + +# SSL/TLS configuration +# Use 'yes', 'no' or 'required'. 'required' is recommended for production. +dovecot_ssl: "yes" +dovecot_ssl_cert: " Date: Tue, 10 Feb 2026 17:32:47 -0300 Subject: [PATCH 02/11] fix(dovecot): update auth-userdb listener permissions for postfix Update the `unix_listener auth-userdb` configuration in `templates/10-master.conf.j2`. This change switches the socket ownership from `vmail` to the `postfix` user and explicitly sets the group to `postfix`. This ensures that the Postfix service has the necessary permissions to access the Dovecot authentication socket for user lookups. --- templates/10-master.conf.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/10-master.conf.j2 b/templates/10-master.conf.j2 index 6f70385..5131440 100644 --- a/templates/10-master.conf.j2 +++ b/templates/10-master.conf.j2 @@ -38,7 +38,8 @@ service auth { unix_listener auth-userdb { mode = 0600 - user = vmail + user = postfix + group = postfix } } From dfd5d899053afb744b4470b9b1dd538164659c2b Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 17:51:23 -0300 Subject: [PATCH 03/11] feat: add support for local dovecot users via passwd-file This introduces functionality to manage local Dovecot users utilizing a static `vmail` system user and a flat password file. Key changes: - Added `dovecot_users` configuration list to defaults. - Implemented creation of `vmail` user and group (uid/gid 5000). - Added logic to generate a random security token using `pwgen` to prefix user passwords. - Created `auth-dovecot-users.conf.ext` and `dovecot-users.j2` templates to handle `passwd-file` authentication. - Updated `10-auth.conf` to include the new local users configuration. - Updated README with usage instructions and token details. --- README.md | 14 ++++++ defaults/main.yml | 9 +++- tasks/main.yml | 61 ++++++++++++++++++++++++ templates/10-auth.conf.j2 | 1 + templates/auth-dovecot-users.conf.ext.j2 | 12 +++++ templates/dovecot-users.j2 | 6 +++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 templates/auth-dovecot-users.conf.ext.j2 create mode 100644 templates/dovecot-users.j2 diff --git a/README.md b/README.md index c65da5b..389a940 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,20 @@ The role now supports installing and configuring Dovecot for IMAP/POP3 services. | dovecot_auth_mechanisms | "plain login" | Authentication mechanisms. | | dovecot_postfix_sasl_enable | true | Enable Postfix SASL authentication via Dovecot. | | dovecot_postfix_lmtp_enable | true | Enable Postfix delivery via Dovecot LMTP. | +| dovecot_users | [] | List of local users to create. See below. | + +### **Local Dovecot Users** + +You can define local users for Dovecot (e.g., for service accounts). These users are managed in a separate password file and use a generated token for security. + +```yaml +dovecot_users: + - name: "service1" + pass: "mysecretpassword" +``` + +The role will generate a random 16-character token on the server (stored in `/etc/dovecot/dovecot_token`). The actual password for the user will be `token + password`. +For example, if the token is `He5rN5SPH33AbFLn` and the password is `mysecretpassword`, the service must authenticate with `He5rN5SPH33AbFLnmysecretpassword`. ### **SASL Authentication** diff --git a/defaults/main.yml b/defaults/main.yml index b6ad1c6..cbcb9bc 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -58,4 +58,11 @@ dovecot_auth_mechanisms: "plain login" # Postfix integration dovecot_postfix_sasl_enable: true -dovecot_postfix_lmtp_enable: true \ No newline at end of file +dovecot_postfix_lmtp_enable: true + +# Local Dovecot Users +# Example: +# dovecot_users: +# - name: "service1" +# pass: "secret123" +dovecot_users: [] \ No newline at end of file diff --git a/tasks/main.yml b/tasks/main.yml index cf78e64..a2a6390 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -62,6 +62,66 @@ tags: - dovecot_install +- name: "DOVECOT | Install pwgen" + when: dovecot_enabled | default(false) + ansible.builtin.apt: + name: pwgen + state: present + tags: + - dovecot_install + +- name: "DOVECOT | Generate Dovecot token" + when: dovecot_enabled | default(false) + ansible.builtin.shell: + cmd: "pwgen -s 16 1 > /etc/dovecot/dovecot_token" + creates: /etc/dovecot/dovecot_token + tags: + - dovecot_config + +- name: "DOVECOT | Read Dovecot token" + when: dovecot_enabled | default(false) + ansible.builtin.slurp: + src: /etc/dovecot/dovecot_token + register: dovecot_token_file + tags: + - dovecot_config + +- name: "DOVECOT | Create vmail group" + when: dovecot_enabled | default(false) + ansible.builtin.group: + name: vmail + gid: 5000 + state: present + tags: + - dovecot_config + +- name: "DOVECOT | Create vmail user" + when: dovecot_enabled | default(false) + ansible.builtin.user: + name: vmail + uid: 5000 + group: vmail + home: /var/vmail + create_home: true + system: true + shell: /usr/sbin/nologin + tags: + - dovecot_config + +- name: "DOVECOT | Create users password file" + when: dovecot_enabled | default(false) + ansible.builtin.template: + src: dovecot-users.j2 + dest: /etc/dovecot/users + owner: root + group: dovecot + mode: '0640' + vars: + dovecot_token_value: "{{ dovecot_token_file['content'] | b64decode | trim }}" + notify: Restart Dovecot + tags: + - dovecot_config + - name: "DOVECOT | Configure dovecot.conf" when: dovecot_enabled | default(false) ansible.builtin.template: @@ -84,6 +144,7 @@ mode: '0644' loop: - { src: '10-auth.conf.j2', dest: '10-auth.conf' } + - { src: 'auth-dovecot-users.conf.ext.j2', dest: 'auth-dovecot-users.conf.ext' } - { src: '10-master.conf.j2', dest: '10-master.conf' } - { src: '10-ssl.conf.j2', dest: '10-ssl.conf' } - { src: '10-mail.conf.j2', dest: '10-mail.conf' } diff --git a/templates/10-auth.conf.j2 b/templates/10-auth.conf.j2 index 007ce56..b7e36ac 100644 --- a/templates/10-auth.conf.j2 +++ b/templates/10-auth.conf.j2 @@ -4,4 +4,5 @@ disable_plaintext_auth = {{ 'yes' if dovecot_ssl == 'required' else 'no' }} auth_mechanisms = {{ dovecot_auth_mechanisms }} +!include auth-dovecot-users.conf.ext !include auth-system.conf.ext diff --git a/templates/auth-dovecot-users.conf.ext.j2 b/templates/auth-dovecot-users.conf.ext.j2 new file mode 100644 index 0000000..6bd69cc --- /dev/null +++ b/templates/auth-dovecot-users.conf.ext.j2 @@ -0,0 +1,12 @@ +# Dovecot local users authentication +# Ansible managed: {{ ansible_managed }} + +passdb { + driver = passwd-file + args = scheme=SHA512-CRYPT username_format=%u /etc/dovecot/users +} + +userdb { + driver = static + args = uid=vmail gid=vmail home=/var/vmail/%u +} diff --git a/templates/dovecot-users.j2 b/templates/dovecot-users.j2 new file mode 100644 index 0000000..ac90e0a --- /dev/null +++ b/templates/dovecot-users.j2 @@ -0,0 +1,6 @@ +# Dovecot users file +# Ansible managed: {{ ansible_managed }} +# user:{scheme}hash:uid:gid:gecos:home:shell:extra_fields +{% for user in dovecot_users %} +{{ user.name }}:{SHA512-CRYPT}{{ (dovecot_token_value + user.pass) | password_hash('sha512', dovecot_token_value) }}:::::: +{% endfor %} From 589d3e0d12611bec36472bb509e3ecc9ee6b6ce1 Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 17:51:44 -0300 Subject: [PATCH 04/11] docs: add agent guidelines and update role title in readme - Create AGENTS.md to define contribution rules, specifically mandating the use of ansible.builtin namespace and ensuring documentation updates. - Update the README.md header from "Postfix" to "Mail" to align with the repository name. --- AGENTS.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9d9173b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,4 @@ +# AGENTS.md + +- Only use `ansible.builtin` +- Always update `README.md` \ No newline at end of file diff --git a/README.md b/README.md index 389a940..8b88514 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# **Ansible Role: Postfix** +# **Ansible Role: Mail** An Ansible role to install and configure Postfix on Debian-based systems. From e350a39a29491a9f1646465136de32c3b637161c Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 18:10:01 -0300 Subject: [PATCH 05/11] refactor(dovecot): replace template hashing with openssl command - Add `openssl` to the list of installed packages to ensure CLI availability. - Introduce a new task to generate user password hashes using `openssl passwd -6` on the target host instead of relying on the Jinja2 `password_hash` filter. - Update `dovecot-users.j2` template to utilize the registered output from the new OpenSSL task. - This ensures consistent SHA512-CRYPT hash generation independent of the controller's Python environment or hashing libraries. --- tasks/main.yml | 14 +++++++++++++- templates/dovecot-users.j2 | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tasks/main.yml b/tasks/main.yml index a2a6390..ae73e8d 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -57,7 +57,7 @@ - name: "DOVECOT | Install Dovecot packages" when: dovecot_enabled | default(false) ansible.builtin.apt: - name: "{{ ['dovecot-core', 'dovecot-imapd', 'dovecot-pop3d'] + (['dovecot-lmtpd'] if dovecot_postfix_lmtp_enable | default(false) else []) }}" + name: "{{ ['dovecot-core', 'dovecot-imapd', 'dovecot-pop3d', 'openssl'] + (['dovecot-lmtpd'] if dovecot_postfix_lmtp_enable | default(false) else []) }}" state: present tags: - dovecot_install @@ -108,6 +108,18 @@ tags: - dovecot_config +- name: "DOVECOT | Generate user password hashes" + when: dovecot_enabled | default(false) and dovecot_users | length > 0 + ansible.builtin.command: + cmd: "openssl passwd -6 -salt {{ dovecot_token_value | quote }} {{ (dovecot_token_value + item.pass) | quote }}" + loop: "{{ dovecot_users }}" + register: dovecot_user_hashes + changed_when: false + vars: + dovecot_token_value: "{{ dovecot_token_file['content'] | b64decode | trim }}" + tags: + - dovecot_config + - name: "DOVECOT | Create users password file" when: dovecot_enabled | default(false) ansible.builtin.template: diff --git a/templates/dovecot-users.j2 b/templates/dovecot-users.j2 index ac90e0a..1f6db51 100644 --- a/templates/dovecot-users.j2 +++ b/templates/dovecot-users.j2 @@ -1,6 +1,8 @@ # Dovecot users file # Ansible managed: {{ ansible_managed }} # user:{scheme}hash:uid:gid:gecos:home:shell:extra_fields -{% for user in dovecot_users %} -{{ user.name }}:{SHA512-CRYPT}{{ (dovecot_token_value + user.pass) | password_hash('sha512', dovecot_token_value) }}:::::: +{% if dovecot_user_hashes.results is defined %} +{% for res in dovecot_user_hashes.results %} +{{ res.item.name }}:{SHA512-CRYPT}{{ res.stdout | trim }}:::::: {% endfor %} +{% endif %} \ No newline at end of file From a24007383da28101f5dee763f7303a2a7754e184 Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 18:41:44 -0300 Subject: [PATCH 06/11] fix(dovecot): use %n instead of %u for username and home path Update the Dovecot authentication template to use `%n` (user part only) instead of `%u` (full username) in both the `passdb` username format and the `userdb` home directory path. This ensures that user lookups and home directory generation rely specifically on the username portion, excluding the domain part. --- templates/auth-dovecot-users.conf.ext.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/auth-dovecot-users.conf.ext.j2 b/templates/auth-dovecot-users.conf.ext.j2 index 6bd69cc..cc72d53 100644 --- a/templates/auth-dovecot-users.conf.ext.j2 +++ b/templates/auth-dovecot-users.conf.ext.j2 @@ -3,10 +3,10 @@ passdb { driver = passwd-file - args = scheme=SHA512-CRYPT username_format=%u /etc/dovecot/users + args = scheme=SHA512-CRYPT username_format=%n /etc/dovecot/users } userdb { driver = static - args = uid=vmail gid=vmail home=/var/vmail/%u + args = uid=vmail gid=vmail home=/var/vmail/%n } From 672082cf6449876ade5933de033bee892e49e210 Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 18:48:00 -0300 Subject: [PATCH 07/11] feat(dovecot): ensure strict permissions on vmail directory Added a new task to ensure the `/var/vmail` directory exists with specific ownership and permissions. - Sets owner and group to `vmail` - Enforces mode `0700` for security - Applied only when dovecot is enabled --- tasks/main.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tasks/main.yml b/tasks/main.yml index ae73e8d..91cfcd0 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -108,6 +108,17 @@ tags: - dovecot_config +- name: "DOVECOT | Ensure vmail directory permissions" + when: dovecot_enabled | default(false) + ansible.builtin.file: + path: /var/vmail + state: directory + owner: vmail + group: vmail + mode: '0700' + tags: + - dovecot_config + - name: "DOVECOT | Generate user password hashes" when: dovecot_enabled | default(false) and dovecot_users | length > 0 ansible.builtin.command: From c9892b9e51d037e4688285405da8b8532f374129 Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Tue, 10 Feb 2026 19:06:41 -0300 Subject: [PATCH 08/11] feat: configure standard special-use mailboxes in dovecot Update templates/10-mail.conf.j2 to explicitly define standard mailboxes (Drafts, Junk, Trash, Sent) within the inbox namespace. This change ensures that: - These folders are automatically subscribed (`auto = subscribe`). - They are correctly flagged with `special_use` attributes, improving compatibility with IMAP clients. - "Sent Messages" is also recognized as a Sent folder for broader client support. --- templates/10-mail.conf.j2 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/templates/10-mail.conf.j2 b/templates/10-mail.conf.j2 index 640586a..edddf85 100644 --- a/templates/10-mail.conf.j2 +++ b/templates/10-mail.conf.j2 @@ -4,4 +4,24 @@ mail_location = {{ dovecot_mail_location }} namespace inbox { inbox = yes + + mailbox Drafts { + special_use = \Drafts + auto = subscribe + } + mailbox Junk { + special_use = \Junk + auto = subscribe + } + mailbox Trash { + special_use = \Trash + auto = subscribe + } + mailbox Sent { + special_use = \Sent + auto = subscribe + } + mailbox "Sent Messages" { + special_use = \Sent + } } From f76e0a31ae5b517b69a3afab0cd082daec74cddc Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Wed, 11 Feb 2026 14:49:22 -0300 Subject: [PATCH 09/11] feat(postfix): implement virtual mailbox configuration for Dovecot LMTP Updates the Postfix configuration to correctly handle virtual domains when Dovecot LMTP is enabled, moving away from local system delivery settings. - Removes `postfix_mail_domain` from `postfix_mydestination` to prevent conflicts with virtual domain handling. - Updates `main.cf` to set `virtual_transport`, `virtual_mailbox_domains`, and `virtual_mailbox_maps` instead of `mailbox_transport`. - Adds a new template `virtual_mailbox_maps.j2` to authorize specific users defined in `dovecot_users`. - Adds tasks to generate the virtual mailbox map file and run `postmap` upon changes. --- defaults/main.yml | 7 ++++--- tasks/main.yml | 23 +++++++++++++++++++++++ templates/main.cf.j2 | 9 ++++----- templates/virtual_mailbox_maps.j2 | 8 ++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 templates/virtual_mailbox_maps.j2 diff --git a/defaults/main.yml b/defaults/main.yml index cbcb9bc..472860f 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -18,9 +18,10 @@ postfix_mail_domain: "{{ ansible_domain | default('internal.local') }}" # The Fully Qualified Domain Name of the mail server. postfix_myhostname: "mail.{{ postfix_mail_domain }}" -# Comma-separated list of domains this server accepts mail for. -# It's critical that this includes the server's own hostname and mail domain. -postfix_mydestination: "$myhostname, localhost.{{ postfix_mail_domain }}, localhost, {{ postfix_mail_domain }}" +# Comma-separated list of domains this server accepts mail for locally. +# When using Dovecot with LMTP (virtual mailboxes), the mail domain is handled +# separately via virtual_mailbox_domains, so it should NOT be included here. +postfix_mydestination: "$myhostname, localhost.{{ postfix_mail_domain }}, localhost" # The list of "trusted" remote SMTP clients that have more privileges than "strangers". postfix_mynetworks: "127.0.0.0/8 [::1]/128" diff --git a/tasks/main.yml b/tasks/main.yml index 91cfcd0..441b1ba 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -54,6 +54,29 @@ - postfix_config - postfix_smarthost +- name: "POSTFIX | Configure virtual mailbox maps" + when: dovecot_enabled | default(false) and dovecot_postfix_lmtp_enable | default(false) + ansible.builtin.template: + src: virtual_mailbox_maps.j2 + dest: /etc/postfix/virtual_mailbox_maps + owner: root + group: root + mode: '0644' + notify: Restart Postfix + tags: + - postfix_config + - dovecot_config + +- name: "POSTFIX | Create hash map for virtual mailbox maps" + when: dovecot_enabled | default(false) and dovecot_postfix_lmtp_enable | default(false) + ansible.builtin.command: + cmd: postmap hash:/etc/postfix/virtual_mailbox_maps + changed_when: true + notify: Restart Postfix + tags: + - postfix_config + - dovecot_config + - name: "DOVECOT | Install Dovecot packages" when: dovecot_enabled | default(false) ansible.builtin.apt: diff --git a/templates/main.cf.j2 b/templates/main.cf.j2 index df24ecf..04bce3d 100644 --- a/templates/main.cf.j2 +++ b/templates/main.cf.j2 @@ -51,9 +51,6 @@ alias_database = hash:/etc/aliases # Dovecot Integration {% if dovecot_enabled | default(false) %} -# Use Maildir format -home_mailbox = Maildir/ - {% if dovecot_postfix_sasl_enable | default(false) %} # SASL Authentication via Dovecot smtpd_sasl_type = dovecot @@ -62,7 +59,9 @@ smtpd_sasl_auth_enable = yes {% endif %} {% if dovecot_postfix_lmtp_enable | default(false) %} -# Delivery via LMTP -mailbox_transport = lmtp:unix:private/dovecot-lmtp +# Virtual mailbox configuration for Dovecot users +virtual_mailbox_domains = {{ postfix_mail_domain }} +virtual_mailbox_maps = hash:/etc/postfix/virtual_mailbox_maps +virtual_transport = lmtp:unix:private/dovecot-lmtp {% endif %} {% endif %} diff --git a/templates/virtual_mailbox_maps.j2 b/templates/virtual_mailbox_maps.j2 new file mode 100644 index 0000000..50fdbf6 --- /dev/null +++ b/templates/virtual_mailbox_maps.j2 @@ -0,0 +1,8 @@ +# Virtual mailbox maps for Postfix +# Ansible managed: {{ ansible_managed }} +# Format: user@domain OK +{% if dovecot_users is defined and dovecot_users | length > 0 %} +{% for user in dovecot_users %} +{{ user.name }}@{{ postfix_mail_domain }} OK +{% endfor %} +{% endif %} From b4930c3c7db764d370695c9b893c653987d6fbed Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Wed, 11 Feb 2026 16:36:01 -0300 Subject: [PATCH 10/11] feat(dovecot): add support for configuring imap_capability This introduces the `dovecot_imap_capability` variable to allow customization of advertised IMAP capabilities. - Add `dovecot_imap_capability` to `defaults/main.yml` (defaulting to empty). - Update `templates/dovecot.conf.j2` to conditionally include the `protocol imap` block if the capability string is provided. - This enables operators to add or remove specific IMAP extensions (e.g., disabling `LITERAL+` or `NOTIFY`) for client compatibility. --- defaults/main.yml | 7 +++++++ templates/dovecot.conf.j2 | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/defaults/main.yml b/defaults/main.yml index 472860f..90a1a8b 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -45,6 +45,13 @@ dovecot_enabled: true # Protocols to enable (imap, pop3, lmtp) dovecot_protocols: "imap pop3 lmtp" +# IMAP capability adjustments. Set to modify advertised IMAP capabilities. +# Use +CAPABILITY to add, -CAPABILITY to remove. +# Example: "+IMAP4rev1 -LITERAL+ -NOTIFY" removes modern extensions that +# might suppress standard untagged responses. +# Leave empty to use Dovecot defaults. +dovecot_imap_capability: "" + # Mail storage location. Using Maildir in the user's home directory. dovecot_mail_location: "maildir:~/Maildir" diff --git a/templates/dovecot.conf.j2 b/templates/dovecot.conf.j2 index 49e85b1..36604db 100644 --- a/templates/dovecot.conf.j2 +++ b/templates/dovecot.conf.j2 @@ -2,6 +2,12 @@ # Ansible managed: {{ ansible_managed }} protocols = {{ dovecot_protocols }} +{% if dovecot_imap_capability | default('') | length > 0 %} + +protocol imap { + imap_capability = {{ dovecot_imap_capability }} +} +{% endif %} # Dictionary of configuration files !include conf.d/*.conf From fc1900838b61f46aea0732ffa0a33e56a0eb995e Mon Sep 17 00:00:00 2001 From: Luciano Giacchetta Date: Wed, 11 Feb 2026 18:01:51 -0300 Subject: [PATCH 11/11] docs: update README to reflect internal mail server scope Refactor the documentation to accurately describe the role as a complete internal mail server stack including Postfix and Dovecot. Changes include: - Expanded the description to explicitly list components and internal use cases. - Added a section clarifying excluded features (antispam/antivirus). - Reorganized role variables into General and Postfix configuration tables. - Cleaned up Markdown formatting and removed excessive bold styling. --- README.md | 146 +++++++++++++++++++++------------------ defaults/main.yml | 4 +- templates/10-ssl.conf.j2 | 4 +- templates/main.cf.j2 | 5 +- 4 files changed, 85 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 8b88514..a6c250d 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,76 @@ -# **Ansible Role: Mail** +# Ansible Role: Mail -An Ansible role to install and configure Postfix on Debian-based systems. +An Ansible role to deploy and configure an internal mail server on Debian-based systems. -## **Description** +## Description -This role sets up Postfix to function as a local mail server designed for internal use. Its primary function is to accept mail from local services and relay all outbound messages through a configured **smarthost**. +This role sets up a complete internal mail server stack, currently including: -This is the perfect setup for environments where internal applications (like cron, monitoring systems, or web applications) need to send email notifications without the complexity of managing a full, internet-facing mail server. +- **Postfix** - Mail Transfer Agent (MTA) for sending and receiving mail +- **Dovecot** - IMAP/POP3 server for mail retrieval -This role performs the following actions: +The role is designed for **internal use cases** where applications, services, and users within your infrastructure need to send and receive email. Outbound mail is relayed through a configured smarthost (e.g., SendGrid, Mailgun, or your ISP's SMTP server). -* Installs the Postfix package and necessary SASL modules on Debian/Ubuntu. -* Manages the main Postfix configuration file (/etc/postfix/main.cf) via a template. -* Manages the /etc/mailname file for defining the mail domain. -* Configures Postfix to route all outgoing mail through a specified smarthost. -* Securely configures SASL authentication for the smarthost if credentials are provided. +### Use Cases -## **Requirements** +- Internal applications sending notifications (cron jobs, monitoring, CI/CD pipelines) +- Service accounts that need to receive and process email +- Development and testing environments +- Private mail infrastructure for small teams -* **Target OS**: This role is designed exclusively for **Debian-based** distributions (e.g., Debian, Ubuntu). -* **Ansible**: Version 2.10 or newer. +### What This Role Does NOT Include -## **Role Variables** +This role intentionally omits antispam and antivirus components. Since it's designed for internal mail that doesn't interact with external/untrusted sources, these features are unnecessary and would add complexity. -The role's behavior can be customized using the following variables. The default values are defined in defaults/main.yml. +## Requirements -| Variable | Default Value | Description | -| :---- | :---- | :---- | -| postfix_relayhost | "" (empty string) | **Required.** The smarthost for relaying all mail. Use square brackets [] to prevent MX lookups (e.g., \[smtp.sendgrid.net\]:587). | -| postfix_relayhost_user | (undefined) | The username for SASL authentication with the smarthost. If defined with a password, SASL auth will be enabled. | -| postfix_relayhost_password | (undefined) | The password or API key for the smarthost user. **It** is strongly recommended to store this in Ansible **Vault.** | -| postfix_mail_domain | `{{ ansible_domain \| default('internal.local') }}` | The primary mail domain for this server | -| postfix_myhostname | `mail.{{ postfix_mail_domain }}` | The fully qualified domain name (FQDN) of the mail server itself (e.g., mail.example.com). | -| postfix_mydestination | `$myhostname, localhost.{{ postfix_mail_domain }}, localhost, {{ postfix_mail_domain }}` | A comma-separated list of domains this server will accept mail for. The default is usually sufficient for an internal relay. | -| postfix_mynetworks | `"127.0.0.0/8 [::1]/128"` | The list of "trusted" remote SMTP clients that have more privileges than "strangers"| -| postfix_inet_interfaces | all | The network interfaces Postfix listens on. Set to loopback-only to only accept mail from the server itself. | -| postfix_inet_protocols | all | The IP protocols to use (ipv4, ipv6, or all). | +- **Target OS**: Debian-based distributions (Debian, Ubuntu) +- **Ansible**: Version 2.10 or newer -### **Dovecot Configuration** +## Role Variables -The role now supports installing and configuring Dovecot for IMAP/POP3 services. +Default values are defined in `defaults/main.yml`. -| Variable | Default Value | Description | -| :---- | :---- | :---- | -| dovecot_enabled | true | Whether to install and configure Dovecot. | +### General Settings + +| Variable | Default | Description | +| :--- | :--- | :--- | +| mail_ssl_cert | snakeoil | Path to SSL certificate (shared by Postfix and Dovecot). | +| mail_ssl_key | snakeoil | Path to SSL private key (shared by Postfix and Dovecot). | + +### Postfix Configuration + +| Variable | Default | Description | +| :--- | :--- | :--- | +| postfix_relayhost | "" | **Required.** Smarthost for relaying outbound mail. Use brackets to skip MX lookups (e.g., `[smtp.sendgrid.net]:587`). | +| postfix_relayhost_user | (undefined) | Username for smarthost SASL authentication. | +| postfix_relayhost_password | (undefined) | Password/API key for smarthost. Store in Ansible Vault. | +| postfix_mail_domain | `{{ ansible_domain }}` | Primary mail domain for this server. | +| postfix_myhostname | `mail.{{ postfix_mail_domain }}` | FQDN of the mail server. | +| postfix_mydestination | `$myhostname, localhost...` | Domains accepted for local delivery. | +| postfix_mynetworks | `127.0.0.0/8 [::1]/128` | Trusted networks allowed to relay. | +| postfix_inet_interfaces | all | Network interfaces to listen on. Use `loopback-only` for local-only access. | +| postfix_inet_protocols | all | IP protocols to use (ipv4, ipv6, or all). | + +SASL authentication for the smarthost is automatically enabled when both `postfix_relayhost_user` and `postfix_relayhost_password` are defined. + +### Dovecot Configuration + +| Variable | Default | Description | +| :--- | :--- | :--- | +| dovecot_enabled | true | Install and configure Dovecot. | | dovecot_protocols | "imap pop3 lmtp" | Protocols to enable. | -| dovecot_mail_location | "maildir:~/Maildir" | Mail storage location. | -| dovecot_ssl | "yes" | SSL/TLS configuration (yes, no, required). | -| dovecot_ssl_cert | snakeoil | Path to SSL certificate. | -| dovecot_ssl_key | snakeoil | Path to SSL key. | -| dovecot_auth_mechanisms | "plain login" | Authentication mechanisms. | -| dovecot_postfix_sasl_enable | true | Enable Postfix SASL authentication via Dovecot. | -| dovecot_postfix_lmtp_enable | true | Enable Postfix delivery via Dovecot LMTP. | -| dovecot_users | [] | List of local users to create. See below. | +| dovecot_mail_location | "maildir:~/Maildir" | Mail storage format and location. | +| dovecot_ssl | "yes" | SSL/TLS mode: `yes`, `no`, or `required`. | +| dovecot_auth_mechanisms | "plain login" | Allowed authentication mechanisms. | +| dovecot_postfix_sasl_enable | true | Allow Postfix to authenticate users via Dovecot. | +| dovecot_postfix_lmtp_enable | true | Deliver mail to Dovecot via LMTP. | +| dovecot_imap_capability | "" | Adjust advertised IMAP capabilities (e.g., `+IMAP4rev1 -LITERAL+`). | +| dovecot_users | [] | List of virtual mailbox users. See below. | -### **Local Dovecot Users** +### Virtual Mailbox Users -You can define local users for Dovecot (e.g., for service accounts). These users are managed in a separate password file and use a generated token for security. +Define users for Dovecot virtual mailboxes: ```yaml dovecot_users: @@ -64,39 +78,37 @@ dovecot_users: pass: "mysecretpassword" ``` -The role will generate a random 16-character token on the server (stored in `/etc/dovecot/dovecot_token`). The actual password for the user will be `token + password`. -For example, if the token is `He5rN5SPH33AbFLn` and the password is `mysecretpassword`, the service must authenticate with `He5rN5SPH33AbFLnmysecretpassword`. +For security, the role generates a random 16-character token on the server (stored in `/etc/dovecot/dovecot_token`). The actual password is `token + password`. For example, if the token is `He5rN5SPH33AbFLn`, the user must authenticate with `He5rN5SPH33AbFLnmysecretpassword`. -### **SASL Authentication** +## Dependencies -SASL authentication for the smarthost is **automatically enabled** if both postfix_relayhost_user and postfix_relayhost_password are defined. If they are not defined, Postfix will attempt to send mail without authentication. +None. -## **Dependencies** +## Example Playbook -This role has no dependencies on other Ansible roles or collections beyond the standard ansible.builtin modules. - -## **Example Playbook** - -Here is a basic example of how to use this role in your playbook. You must define postfix_relayhost. It is also highly recommended to use Ansible Vault to encrypt the smarthost password. - -``` ---- -- hosts: all - become: true - roles: - - role: your_username.postfix - vars: - postfix_relayhost: "[smtp.mailgun.org\]:587" - postfix_relayhost_user: "postmaster@mg.example.com" - postfix_relayhost_password: "{{ vaulted_mailgun_password }}" # Stored in Ansible Vault - postfix_inet_interfaces: "loopback-only" +```yaml +--- +- hosts: mail_servers + become: true + roles: + - role: giacchetta.mail + vars: postfix_mail_domain: "example.com" + postfix_relayhost: "[smtp.mailgun.org]:587" + postfix_relayhost_user: "postmaster@mg.example.com" + postfix_relayhost_password: "{{ vault_mailgun_password }}" + mail_ssl_cert: "/etc/letsencrypt/live/mail.example.com/fullchain.pem" + mail_ssl_key: "/etc/letsencrypt/live/mail.example.com/privkey.pem" + dovecot_ssl: "required" + dovecot_users: + - name: "alerts" + pass: "{{ vault_alerts_password }}" ``` -## **License** +## License GPL-3.0-only -## **Author Information** +## Author Information -This role was created by Giacchetta Networks. \ No newline at end of file +This role was created by Giacchetta Networks. diff --git a/defaults/main.yml b/defaults/main.yml index 90a1a8b..63694d3 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -58,8 +58,8 @@ dovecot_mail_location: "maildir:~/Maildir" # SSL/TLS configuration # Use 'yes', 'no' or 'required'. 'required' is recommended for production. dovecot_ssl: "yes" -dovecot_ssl_cert: "