From 3d7bbabd7b42ea37fedd0287afccd786d99e7bce Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 18 Aug 2025 01:03:40 +0200 Subject: [PATCH] mailu: enable central database, improve token creation task, and add migration guide - Enabled central_database in Mailu config - Improved API token creation task: * use curl -f to fail on HTTP errors * added explicit failed_when and changed_when conditions - Adjusted docker-compose template spacing for readability - Made logging level configurable (DEBUG when MODE_DEBUG is set) - Added new documentation Move_Domain.md explaining safe procedure for migrating mailboxes to a new domain --- roles/web-app-mailu/config/main.yml | 2 +- roles/web-app-mailu/docs/Move_Domain.md | 90 +++++++++++++++++++ roles/web-app-mailu/tasks/03_create-token.yml | 28 +++--- .../templates/docker-compose.yml.j2 | 10 +-- roles/web-app-mailu/templates/env.j2 | 2 +- 5 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 roles/web-app-mailu/docs/Move_Domain.md diff --git a/roles/web-app-mailu/config/main.yml b/roles/web-app-mailu/config/main.yml index f2286a5a..bef7db49 100644 --- a/roles/web-app-mailu/config/main.yml +++ b/roles/web-app-mailu/config/main.yml @@ -7,7 +7,7 @@ features: css: false desktop: true oidc: true - central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary + central_database: true logout: true server: domains: diff --git a/roles/web-app-mailu/docs/Move_Domain.md b/roles/web-app-mailu/docs/Move_Domain.md new file mode 100644 index 00000000..1431a590 --- /dev/null +++ b/roles/web-app-mailu/docs/Move_Domain.md @@ -0,0 +1,90 @@ +# πŸ“– **How to Migrate Mailboxes to a New Domain in Mailu** + +When changing the primary email domain (e.g., from `cymais.cloud` to `infinito.nexus`), it is **not enough** to simply rename mailbox directories on disk. Mailu manages domain and user records in its internal database, and Dovecot maintains index files inside each Maildir. A blind rename will lead to login failures, rejected mail, or corrupted mail indices. + +This guide explains the **safe procedure** for migrating user mailboxes to a new domain. + +--- + +## πŸ”΄ Why renaming folders directly does not work + +* Mailu keeps **domains, users, and aliases** in the `admin_data` database. If you rename folders only, Mailu will not recognize the new accounts. +* Dovecot generates `.dovecot.index*` and `dovecot-uidlist` files in each mailbox. These must be rebuilt when moving mailboxes; otherwise, users may see missing or broken mail. +* Postfix will refuse to deliver or relay messages for unknown domains. + +--- + +## βœ… Correct migration procedure + +### 1. Add the new domain and users in Mailu + +Use the Mailu CLI inside the `admin` container: + +```bash +# Add new domain +docker compose exec admin flask mailu domain add infinito.nexus + +# Add new user (repeat for each account) +docker compose exec admin flask mailu user kevinveenbirkenbach infinito.nexus 'NEW_PASSWORD' +``` + +--- + +### 2. Copy or move existing Maildir contents + +Instead of renaming, copy the entire Maildir from the old domain to the new one: + +```bash +rsync -aHAX --numeric-ids \ + /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@cymais.cloud/ \ + /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus/ +``` + +--- + +### 3. Remove old Dovecot index files + +Ensure that Dovecot rebuilds indices cleanly: + +```bash +find /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus -type f -name '.dovecot*' -delete +find /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus -type f -name 'dovecot-uidlist*' -delete +``` + +--- + +### 4. Fix file permissions + +Make sure all mailbox files belong to the `mail:mail` user/group: + +```bash +chown -R mail:mail /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus +``` + +--- + +### 5. Restart services and test login + +After copying, restart Mailu services (or at least `imap` and `smtp`) and confirm that users can log in with their new addresses. + +--- + +### 6. (Optional) Keep the old domain as an alias + +To ensure incoming mail for the old domain is still accepted: + +```bash +docker compose exec admin flask mailu domain alias cymais.cloud infinito.nexus +``` + +This maps all `@cymais.cloud` addresses to their equivalents under `@infinito.nexus`. + +--- + +## πŸ’‘ Summary + +* Do **not** just rename mailbox folders. +* Always create the new domain and user in Mailu first. +* Copy existing Maildir contents into the new user’s directory. +* Remove Dovecot indices and fix permissions. +* Optionally configure a **domain alias** so old addresses remain valid. diff --git a/roles/web-app-mailu/tasks/03_create-token.yml b/roles/web-app-mailu/tasks/03_create-token.yml index 53cb4a59..c45de54d 100644 --- a/roles/web-app-mailu/tasks/03_create-token.yml +++ b/roles/web-app-mailu/tasks/03_create-token.yml @@ -38,20 +38,28 @@ - name: "Create API token for '{{ mailu_user_key }};{{ mailu_user_name }}' if no local token defined" command: >- - docker compose exec -T admin \ - curl -s -X POST {{ mailu_api_base_url }}/token \ - -H "Authorization: Bearer {{ MAILU_API_TOKEN }}" \ - -H "Content-Type: application/json" \ - -d '{{ { - "comment": mailu_user_key ~ " - ansible.infinito", - "email": users[mailu_user_key].email, - "ip": mailu_token_ip - } | to_json }}' + docker compose exec -T admin + curl -sS -f -X POST {{ mailu_api_base_url }}/token + -H "Authorization: Bearer {{ MAILU_API_TOKEN }}" + -H "Content-Type: application/json" + -d '{{ { + "comment": mailu_user_key ~ " - ansible.infinito", + "email": users[mailu_user_key].email, + "ip": mailu_token_ip + } | to_json }}' args: chdir: "{{ MAILU_DOCKER_DIR }}" when: users[mailu_user_key].mailu_token is not defined register: mailu_token_creation - changed_when: mailu_token_creation.rc == 0 + # If curl sees 4xx/5xx it returns non-zero due to -f β†’ fail the task. + failed_when: + - mailu_token_creation.rc != 0 + # Fallback: if some gateway returns 200 but embeds an error JSON. + - mailu_token_creation.rc == 0 and + (mailu_token_creation.stdout is search('"code"\\s*:\\s*4\\d\\d') or + mailu_token_creation.stdout is search('cannot be found')) + # Only mark changed when a token is actually present in the JSON. + changed_when: mailu_token_creation.stdout is search('"token"\\s*:') no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" - name: "Set mailu_token for '{{ mailu_user_key }};{{ mailu_user_name }}' in users dict if newly created" diff --git a/roles/web-app-mailu/templates/docker-compose.yml.j2 b/roles/web-app-mailu/templates/docker-compose.yml.j2 index 25bdc36d..a81fcf6c 100644 --- a/roles/web-app-mailu/templates/docker-compose.yml.j2 +++ b/roles/web-app-mailu/templates/docker-compose.yml.j2 @@ -23,7 +23,7 @@ - "{{ MAILU_IP4_PUBLIC }}:993:993" - "{{ MAILU_IP4_PUBLIC }}:4190:4190" volumes: - - "{{docker_compose.directories.volumes}}overrides/nginx:/overrides:ro" + - "{{ docker_compose.directories.volumes }}overrides/nginx:/overrides:ro" - "{{ cert_mount_directory }}:/certs:ro" {% include 'roles/docker-container/templates/depends_on/dmbs_incl.yml.j2' %} resolver: @@ -56,7 +56,7 @@ {% include 'roles/docker-container/templates/base.yml.j2' %} volumes: - "dovecot_mail:/mail" - - "{{docker_compose.directories.volumes}}overrides:/overrides:ro" + - "{{ docker_compose.directories.volumes }}overrides:/overrides:ro" depends_on: - front - resolver @@ -69,7 +69,7 @@ image: {{ MAILU_DOCKER_FLAVOR }}/postfix:{{ MAILU_VERSION }} {% include 'roles/docker-container/templates/base.yml.j2' %} volumes: - - "{{docker_compose.directories.volumes}}overrides:/overrides:ro" + - "{{ docker_compose.directories.volumes }}overrides:/overrides:ro" - "smtp_queue:/queue" depends_on: - front @@ -97,7 +97,7 @@ volumes: - "filter:/var/lib/rspamd" - "dkim:/dkim" - - "{{docker_compose.directories.volumes}}overrides/rspamd:/overrides:ro" + - "{{ docker_compose.directories.volumes }}overrides/rspamd:/overrides:ro" depends_on: - front - redis @@ -156,7 +156,7 @@ {% include 'roles/docker-container/templates/base.yml.j2' %} volumes: - "webmail_data:/data" - - "{{docker_compose.directories.volumes}}overrides:/overrides:ro" + - "{{ docker_compose.directories.volumes }}overrides:/overrides:ro" depends_on: - imap - front diff --git a/roles/web-app-mailu/templates/env.j2 b/roles/web-app-mailu/templates/env.j2 index da4598a9..a3aebd0a 100644 --- a/roles/web-app-mailu/templates/env.j2 +++ b/roles/web-app-mailu/templates/env.j2 @@ -136,7 +136,7 @@ REAL_IP_FROM= REJECT_UNLISTED_RECIPIENT= # Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET) -LOG_LEVEL=WARNING +LOG_LEVEL={{ 'DEBUG' if MODE_DEBUG else 'WARNING' }} ################################### # Database settings