Merge pull request '3-Implement-IMAP/POP' (#4) from 3-Implement-IMAP/POP into main

Reviewed-on: #4
Fix #3
This commit was merged in pull request #4.
This commit is contained in:
2026-02-11 22:40:10 +00:00
16 changed files with 439 additions and 55 deletions

0
.ansible/.lock Normal file
View File

4
AGENTS.md Normal file
View File

@@ -0,0 +1,4 @@
# AGENTS.md
- Only use `ansible.builtin`
- Always update `README.md`

134
README.md
View File

@@ -1,72 +1,114 @@
# **Ansible Role: Postfix**
# 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
### **SASL Authentication**
## Role Variables
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.
Default values are defined in `defaults/main.yml`.
## **Dependencies**
### General Settings
This role has no dependencies on other Ansible roles or collections beyond the standard ansible.builtin modules.
| 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). |
## **Example Playbook**
### Postfix Configuration
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.
| 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 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. |
### Virtual Mailbox Users
Define users for Dovecot virtual mailboxes:
```yaml
dovecot_users:
- name: "service1"
pass: "mysecretpassword"
```
---
- 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"
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`.
## Dependencies
None.
## Example Playbook
```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.
This role was created by Giacchetta Networks.

View File

@@ -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"
@@ -34,4 +35,42 @@ 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"
# 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"
# 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"
# SSL/TLS configuration
# Use 'yes', 'no' or 'required'. 'required' is recommended for production.
dovecot_ssl: "yes"
mail_ssl_cert: "/etc/ssl/certs/ssl-cert-snakeoil.pem"
mail_ssl_key: "/etc/ssl/private/ssl-cert-snakeoil.key"
# Authentication mechanisms
dovecot_auth_mechanisms: "plain login"
# Postfix integration
dovecot_postfix_sasl_enable: true
dovecot_postfix_lmtp_enable: true
# Local Dovecot Users
# Example:
# dovecot_users:
# - name: "service1"
# pass: "secret123"
dovecot_users: []

View File

@@ -2,4 +2,9 @@
- name: Restart Postfix
ansible.builtin.service:
name: postfix
state: restarted
- name: Restart Dovecot
ansible.builtin.service:
name: dovecot
state: restarted

View File

@@ -52,4 +52,148 @@
notify: Restart Postfix
tags:
- postfix_config
- postfix_smarthost
- 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:
name: "{{ ['dovecot-core', 'dovecot-imapd', 'dovecot-pop3d', 'openssl'] + (['dovecot-lmtpd'] if dovecot_postfix_lmtp_enable | default(false) else []) }}"
state: present
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 | 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:
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:
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:
src: dovecot.conf.j2
dest: /etc/dovecot/dovecot.conf
owner: root
group: dovecot
mode: '0644'
notify: Restart Dovecot
tags:
- dovecot_config
- name: "DOVECOT | Configure conf.d files"
when: dovecot_enabled | default(false)
ansible.builtin.template:
src: "{{ item.src }}"
dest: "/etc/dovecot/conf.d/{{ item.dest }}"
owner: root
group: dovecot
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' }
notify: Restart Dovecot
tags:
- dovecot_config

View File

@@ -0,0 +1,8 @@
# Dovecot authentication configuration
# Ansible managed: {{ ansible_managed }}
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

27
templates/10-mail.conf.j2 Normal file
View File

@@ -0,0 +1,27 @@
# Dovecot mail location configuration
# Ansible managed: {{ ansible_managed }}
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
}
}

View File

@@ -0,0 +1,52 @@
# Dovecot master configuration
# Ansible managed: {{ ansible_managed }}
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}
service pop3-login {
inet_listener pop3 {
port = 110
}
inet_listener pop3s {
port = 995
ssl = yes
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0600
user = postfix
group = postfix
}
}
service auth-worker {
}
service dict {
unix_listener dict {
}
}

6
templates/10-ssl.conf.j2 Normal file
View File

@@ -0,0 +1,6 @@
# Dovecot SSL configuration
# Ansible managed: {{ ansible_managed }}
ssl = {{ dovecot_ssl }}
ssl_cert = <{{ mail_ssl_cert }}
ssl_key = <{{ mail_ssl_key }}

View File

@@ -0,0 +1,12 @@
# Dovecot local users authentication
# Ansible managed: {{ ansible_managed }}
passdb {
driver = passwd-file
args = scheme=SHA512-CRYPT username_format=%n /etc/dovecot/users
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/var/vmail/%n
}

View File

@@ -0,0 +1,8 @@
# Dovecot users file
# Ansible managed: {{ ansible_managed }}
# user:{scheme}hash:uid:gid:gecos:home:shell:extra_fields
{% if dovecot_user_hashes.results is defined %}
{% for res in dovecot_user_hashes.results %}
{{ res.item.name }}:{SHA512-CRYPT}{{ res.stdout | trim }}::::::
{% endfor %}
{% endif %}

13
templates/dovecot.conf.j2 Normal file
View File

@@ -0,0 +1,13 @@
# Dovecot configuration file
# 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

View File

@@ -16,9 +16,8 @@ inet_interfaces = {{ postfix_inet_interfaces }}
recipient_delimiter = +
# TLS parameters for incoming connections
# For a production server, replace snakeoil with real certificates.
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_tls_cert_file={{ mail_ssl_cert }}
smtpd_tls_key_file={{ mail_ssl_key }}
smtpd_tls_security_level=may
smtpd_use_tls=yes
@@ -48,3 +47,20 @@ smtp_tls_security_level = may
# Other settings
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
# Dovecot Integration
{% if dovecot_enabled | default(false) %}
{% if dovecot_postfix_sasl_enable | default(false) %}
# SASL Authentication via Dovecot
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
{% endif %}
{% if dovecot_postfix_lmtp_enable | default(false) %}
# 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 %}

View File

@@ -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 %}

View File

@@ -2,4 +2,4 @@
- hosts: localhost
remote_user: root
roles:
- ansible_role_mailserver
- ansible_role_mail