--- - name: Create tools root directory ansible.builtin.file: path: "{{ tools_root }}" state: directory owner: "{{ deploy_user }}" group: "{{ deploy_group }}" mode: "0750" - name: Create mailserver directories ansible.builtin.file: path: "{{ tools_root }}/mailserver/{{ item }}" state: directory owner: root group: root mode: "0755" loop: - mail-data - mail-state - mail-logs - config - name: Create snappymail data directory ansible.builtin.file: path: "{{ tools_root }}/snappymail/data" state: directory owner: "82" # www-data uid in Alpine (SnappyMail container user) group: "82" mode: "0755" # ── TLS certificate for mail.csrx.ru (via certbot + Cloudflare DNS-01) ─────── - name: Install certbot and Cloudflare DNS plugin ansible.builtin.apt: name: - certbot - python3-certbot-dns-cloudflare state: present update_cache: false - name: Deploy Cloudflare credentials for certbot ansible.builtin.copy: content: | dns_cloudflare_api_token = {{ cloudflare_dns_api_token }} dest: /etc/letsencrypt/cloudflare.ini mode: "0600" owner: root group: root - name: Obtain TLS certificate for mail.{{ domain_base }} ansible.builtin.command: > certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini --email {{ acme_email }} --agree-tos --no-eff-email -d mail.{{ domain_base }} --non-interactive register: certbot_result changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout" failed_when: - certbot_result.rc != 0 - "'Certificate not yet due for renewal' not in certbot_result.stdout" - "'Certificate not yet due for renewal' not in certbot_result.stderr" # ── Open mail ports in UFW ──────────────────────────────────────────────────── - name: Allow SMTP inbound (port 25) community.general.ufw: rule: allow port: "25" proto: tcp - name: Allow SMTP submission (port 587) community.general.ufw: rule: allow port: "587" proto: tcp - name: Allow IMAPS (port 993) community.general.ufw: rule: allow port: "993" proto: tcp - name: Allow SMTPS (port 465) community.general.ufw: rule: allow port: "465" proto: tcp # ── Deploy configs and start stack ──────────────────────────────────────────── - name: Deploy docker-compose.yml ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ tools_root }}/docker-compose.yml" owner: "{{ deploy_user }}" group: "{{ deploy_group }}" mode: "0640" - name: Deploy .env ansible.builtin.template: src: env.j2 dest: "{{ tools_root }}/.env" owner: "{{ deploy_user }}" group: "{{ deploy_group }}" mode: "0600" - name: Pull images community.docker.docker_image: name: "{{ item }}" source: pull loop: - "{{ outline_image }}" - "{{ outline_db_image }}" - "{{ outline_redis_image }}" - "{{ n8n_image }}" - "{{ mailserver_image }}" - "{{ snappymail_image }}" - name: Start tools stack community.docker.docker_compose_v2: project_src: "{{ tools_root }}" state: present pull: missing # ── Mail accounts (idempotent: check host-side config file) ────────────────── - name: Wait for mailserver to be ready ansible.builtin.command: docker exec mailserver postfix status register: postfix_status changed_when: false retries: 18 delay: 10 until: postfix_status.rc == 0 - name: Create mail accounts ansible.builtin.command: > docker exec mailserver setup email add {{ item.address }} {{ item.password }} loop: - { address: "noreply@{{ domain_base }}", password: "{{ mailserver_noreply_password }}" } - { address: "admin@{{ domain_base }}", password: "{{ mailserver_admin_password }}" } - { address: "jack@{{ domain_base }}", password: "{{ mailserver_jack_password }}" } register: mail_account_result changed_when: mail_account_result.rc == 0 failed_when: > mail_account_result.rc != 0 and 'already exists' not in mail_account_result.stderr # ── DKIM ───────────────────────────────────────────────────────────────────── - name: Check if DKIM key exists ansible.builtin.stat: path: "{{ tools_root }}/mailserver/config/opendkim/keys/{{ domain_base }}/mail.private" register: dkim_key - name: Generate DKIM key ansible.builtin.command: > docker exec mailserver setup config dkim domain {{ domain_base }} when: not dkim_key.stat.exists register: dkim_generated - name: Read DKIM DNS record ansible.builtin.command: > cat {{ tools_root }}/mailserver/config/opendkim/keys/{{ domain_base }}/mail.txt when: dkim_generated is changed register: dkim_record - name: Print DKIM DNS instructions ansible.builtin.debug: msg: | ══════════════════════════════════════════════════════════════════ DKIM key generated! Add this TXT record to Cloudflare DNS: {{ dkim_record.stdout }} ══════════════════════════════════════════════════════════════════ when: dkim_generated is changed