initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
75891c3271
129 changed files with 8046 additions and 0 deletions
6
roles/mail/defaults/main.yml
Normal file
6
roles/mail/defaults/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
# Rainloop webmail port (internal to docker network)
|
||||
rainloop_port: 8888
|
||||
|
||||
# Rspamd web UI port (internal to docker network)
|
||||
rspamd_port: 11334
|
||||
21
roles/mail/handlers/main.yml
Normal file
21
roles/mail/handlers/main.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
- name: Restart mailserver
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /srv/mail
|
||||
services:
|
||||
- mailserver
|
||||
state: restarted
|
||||
build: never
|
||||
|
||||
- name: Restart Rainloop
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /srv/mail
|
||||
services:
|
||||
- rainloop
|
||||
state: restarted
|
||||
build: never
|
||||
|
||||
- name: Restart mail stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /srv/mail
|
||||
state: restarted
|
||||
build: never
|
||||
19
roles/mail/tasks/aliases.yml
Normal file
19
roles/mail/tasks/aliases.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# read-only docker exec always reports changed; changed_when: false suppresses spurious output
|
||||
- name: List existing mail aliases
|
||||
command: docker exec mailserver setup alias list
|
||||
register: mail_alias_list
|
||||
changed_when: false
|
||||
tags:
|
||||
- users
|
||||
|
||||
- name: Create mail aliases if missing
|
||||
command: >
|
||||
docker exec mailserver
|
||||
setup alias add {{ item.from }}
|
||||
{{ item.to if item.to is string else item.to | join(',') }}
|
||||
loop: "{{ mail_aliases }}"
|
||||
when: item.from not in mail_alias_list.stdout
|
||||
tags:
|
||||
- users
|
||||
|
||||
|
||||
139
roles/mail/tasks/main.yml
Normal file
139
roles/mail/tasks/main.yml
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
- name: Allow SMTP traffic
|
||||
ufw:
|
||||
rule: allow
|
||||
port: 25
|
||||
proto: tcp
|
||||
|
||||
- name: Allow mail submission traffic
|
||||
ufw:
|
||||
rule: allow
|
||||
port: 587
|
||||
proto: tcp
|
||||
|
||||
- name: Allow IMAP over TLS traffic
|
||||
ufw:
|
||||
rule: allow
|
||||
port: 993
|
||||
proto: tcp
|
||||
|
||||
- name: Create docker-mailserver directory
|
||||
file:
|
||||
path: "/srv/mail"
|
||||
state: directory
|
||||
owner: root
|
||||
group: docker # to allow access to the compose file
|
||||
mode: '0755'
|
||||
|
||||
- name: Create docker-mailserver directories
|
||||
file:
|
||||
path: "/srv/mail/{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: docker
|
||||
mode: '0750'
|
||||
loop:
|
||||
- env
|
||||
- config/rspamd/override.d
|
||||
|
||||
- name: Create maillogs directory
|
||||
file:
|
||||
path: /srv/mail/maillogs
|
||||
state: directory
|
||||
mode: '0755' # container startup script needs to traverse and chown subdirs
|
||||
|
||||
# stat+chown: avoids UID/GID lookup warnings for container-internal UIDs not present on host
|
||||
- name: Stat maillogs directory
|
||||
stat:
|
||||
path: /srv/mail/maillogs
|
||||
register: maillogs_stat
|
||||
|
||||
- name: Set maillogs directory ownership
|
||||
command: chown 113:0 /srv/mail/maillogs
|
||||
when: maillogs_stat.stat.uid != 113 or maillogs_stat.stat.gid != 0
|
||||
|
||||
- name: Create mailstate directory
|
||||
file:
|
||||
path: /srv/mail/mailstate
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755' # container startup script needs to traverse and chown subdirs
|
||||
|
||||
- name: Create maildata directory
|
||||
file:
|
||||
path: /srv/mail/maildata
|
||||
state: directory
|
||||
mode: '0751' # container startup script needs to traverse and chown subdirs
|
||||
|
||||
- name: Create config directory
|
||||
file:
|
||||
path: /srv/mail/config
|
||||
state: directory
|
||||
mode: '0751' # container startup script needs to traverse and chown subdirs
|
||||
|
||||
- name: Create rainloop data directory
|
||||
file:
|
||||
path: /srv/mail/rainloop/data
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
# stat+chown: avoids UID/GID lookup warnings for container-internal UIDs not present on host
|
||||
- name: Stat rainloop data directory
|
||||
stat:
|
||||
path: /srv/mail/rainloop/data
|
||||
register: rainloop_data_stat
|
||||
|
||||
- name: Set rainloop data directory ownership
|
||||
command: chown 991:991 /srv/mail/rainloop/data
|
||||
when: rainloop_data_stat.stat.uid != 991 or rainloop_data_stat.stat.gid != 991
|
||||
|
||||
- name: Ensure certbot is installed
|
||||
apt:
|
||||
name: certbot
|
||||
state: present
|
||||
|
||||
- name: Obtain a Let's Encrypt certificate for {{ mail_hostname }}
|
||||
command: >
|
||||
certbot certonly --standalone
|
||||
-d {{ mail_hostname }}
|
||||
--non-interactive --agree-tos -m postmaster@{{ domain }}
|
||||
args:
|
||||
creates: /etc/letsencrypt/live/{{ mail_hostname }}/fullchain.pem
|
||||
tags: config
|
||||
|
||||
- name: Deploy mail compose file
|
||||
template:
|
||||
src: compose.yml.j2
|
||||
dest: /srv/mail/compose.yml
|
||||
notify: Restart mail stack
|
||||
tags: config
|
||||
|
||||
- name: Deploy mailserver environment file
|
||||
template:
|
||||
src: mailserver.env.j2
|
||||
dest: /srv/mail/env/mailserver.env
|
||||
mode: '0640'
|
||||
owner: root
|
||||
group: docker
|
||||
notify: Restart mailserver
|
||||
tags: config
|
||||
|
||||
- name: Deploy rspamd web UI config
|
||||
template:
|
||||
src: worker-controller.inc.j2
|
||||
dest: /srv/mail/config/rspamd/override.d/worker-controller.inc
|
||||
mode: '0644'
|
||||
notify: Restart mailserver
|
||||
tags: config
|
||||
|
||||
- name: Start mailserver
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /srv/mail
|
||||
state: present
|
||||
build: never
|
||||
tags: config
|
||||
|
||||
- import_tasks: users.yml
|
||||
- import_tasks: aliases.yml
|
||||
# webmail interface
|
||||
- import_tasks: rainloop.yml
|
||||
21
roles/mail/tasks/rainloop.yml
Normal file
21
roles/mail/tasks/rainloop.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
- name: Ensure Rainloop allowed domains are set
|
||||
ini_file:
|
||||
path: /srv/mail/rainloop/data/_data_/_default_/configs/application.ini
|
||||
section: security
|
||||
option: AllowedDomains
|
||||
value: "{{ mail_domains | join(',') }}"
|
||||
backup: yes
|
||||
notify:
|
||||
- Restart Rainloop
|
||||
|
||||
- name: Set proper mode of Rainloop data directory
|
||||
file:
|
||||
path: /srv/mail/rainloop
|
||||
state: directory
|
||||
recurse: yes
|
||||
mode: u+rwX,g+rX
|
||||
|
||||
# chown -R always exits 0; changed_when: false suppresses spurious "changed" in playbook output
|
||||
- name: Set proper ownership of Rainloop data directory
|
||||
command: chown -R 991:991 /srv/mail/rainloop
|
||||
changed_when: false
|
||||
26
roles/mail/tasks/users.yml
Normal file
26
roles/mail/tasks/users.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# read-only docker exec always reports changed; changed_when: false suppresses spurious output
|
||||
- name: Check if mail user exists
|
||||
command: docker exec mailserver setup email list
|
||||
register: mail_user_list
|
||||
changed_when: false
|
||||
tags:
|
||||
- users
|
||||
|
||||
- name: Create mail users if missing
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- exec
|
||||
- mailserver
|
||||
- setup
|
||||
- email
|
||||
- add
|
||||
- "{{ item.address }}"
|
||||
- "{{ item.password }}"
|
||||
loop: "{{ mail_users }}"
|
||||
when: item.address not in mail_user_list.stdout
|
||||
no_log: true
|
||||
ignore_errors: yes
|
||||
tags:
|
||||
- users
|
||||
|
||||
60
roles/mail/templates/compose.yml.j2
Normal file
60
roles/mail/templates/compose.yml.j2
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
services:
|
||||
|
||||
mailserver:
|
||||
image: docker.io/mailserver/docker-mailserver:{{ mailserver_version }}
|
||||
container_name: mailserver
|
||||
hostname: {{ mail_hostname.split('.')[0] }}
|
||||
domainname: {{ domain }}
|
||||
env_file: env/mailserver.env
|
||||
ports:
|
||||
- "25:25"
|
||||
- "587:587"
|
||||
- "993:993"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "supervisorctl status | grep -E 'postfix|dovecot' | grep -q RUNNING"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 120s
|
||||
volumes:
|
||||
- /srv/mail/config:/tmp/docker-mailserver
|
||||
- /srv/mail/maildata:/var/mail
|
||||
- /srv/mail/mailstate:/var/mail-state
|
||||
- /srv/mail/maillogs:/var/log/mail
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
networks:
|
||||
- mail
|
||||
- webmail
|
||||
|
||||
rainloop:
|
||||
image: hardware/rainloop:{{ rainloop_version }}
|
||||
container_name: rainloop
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mailserver:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:{{ rainloop_port }}/ || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
environment:
|
||||
- RAINLOOP_ADMIN=admin
|
||||
- RAINLOOP_ADMIN_PASSWORD={{ rainloop_admin_password }}
|
||||
volumes:
|
||||
- /srv/mail/rainloop/data:/rainloop/data
|
||||
ports: [] # no host ports, only accessible via Docker network
|
||||
networks:
|
||||
- mail
|
||||
- webmail
|
||||
|
||||
networks:
|
||||
mail:
|
||||
external: true
|
||||
webmail:
|
||||
external: true
|
||||
33
roles/mail/templates/mailserver.env.j2
Normal file
33
roles/mail/templates/mailserver.env.j2
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
DMS_DEBUG=0
|
||||
# PERMIT_DOCKER=network
|
||||
|
||||
# Hostname + primary domain (split from mail_hostname variable)
|
||||
HOSTNAME={{ mail_hostname.split('.')[0] }}
|
||||
DOMAINNAME={{ domain }}
|
||||
|
||||
# Let's Encrypt
|
||||
ENABLE_TLS=1
|
||||
SSL_TYPE=letsencrypt
|
||||
LETSENCRYPT_DOMAIN={{ mail_hostname }}
|
||||
LETSENCRYPT_EMAIL={{ admin_user }}@{{ domain }}
|
||||
|
||||
# Override hostname (FQDN) and domains handled by this server
|
||||
OVERRIDE_HOSTNAME={{ mail_hostname }}
|
||||
OVERRIDE_DOMAIN={{ mail_domains | join(',') }}
|
||||
|
||||
# Other docker-mailserver options
|
||||
POSTMASTER_ADDRESS=postmaster@{{ domain }}
|
||||
ONE_DIR=1
|
||||
ENABLE_OPENDKIM=0
|
||||
# rspamd handles DMARC and SPF natively; enabling these would duplicate validation
|
||||
ENABLE_OPENDMARC=0
|
||||
ENABLE_POLICYD_SPF=0
|
||||
ENABLE_AMAVIS=0
|
||||
ENABLE_CLAMAV=0
|
||||
ENABLE_FAIL2BAN=0
|
||||
ENABLE_SPAMASSASSIN=0
|
||||
ENABLE_RSPAMD=1
|
||||
|
||||
RSPAMD_LEARN=1
|
||||
|
||||
POSTFIX_MESSAGE_SIZE_LIMIT=26214400
|
||||
2
roles/mail/templates/worker-controller.inc.j2
Normal file
2
roles/mail/templates/worker-controller.inc.j2
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Rspamd web UI password
|
||||
password = "{{ rspamd_web_password }}";
|
||||
Loading…
Add table
Add a link
Reference in a new issue