mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-20 18:55:01 +02:00
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
This commit is contained in:
parent
e4b8c97e03
commit
3d7bbabd7b
@ -7,7 +7,7 @@ features:
|
|||||||
css: false
|
css: false
|
||||||
desktop: true
|
desktop: true
|
||||||
oidc: 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
|
logout: true
|
||||||
server:
|
server:
|
||||||
domains:
|
domains:
|
||||||
|
90
roles/web-app-mailu/docs/Move_Domain.md
Normal file
90
roles/web-app-mailu/docs/Move_Domain.md
Normal file
@ -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.
|
@ -38,20 +38,28 @@
|
|||||||
|
|
||||||
- name: "Create API token for '{{ mailu_user_key }};{{ mailu_user_name }}' if no local token defined"
|
- name: "Create API token for '{{ mailu_user_key }};{{ mailu_user_name }}' if no local token defined"
|
||||||
command: >-
|
command: >-
|
||||||
docker compose exec -T admin \
|
docker compose exec -T admin
|
||||||
curl -s -X POST {{ mailu_api_base_url }}/token \
|
curl -sS -f -X POST {{ mailu_api_base_url }}/token
|
||||||
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}" \
|
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}"
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json"
|
||||||
-d '{{ {
|
-d '{{ {
|
||||||
"comment": mailu_user_key ~ " - ansible.infinito",
|
"comment": mailu_user_key ~ " - ansible.infinito",
|
||||||
"email": users[mailu_user_key].email,
|
"email": users[mailu_user_key].email,
|
||||||
"ip": mailu_token_ip
|
"ip": mailu_token_ip
|
||||||
} | to_json }}'
|
} | to_json }}'
|
||||||
args:
|
args:
|
||||||
chdir: "{{ MAILU_DOCKER_DIR }}"
|
chdir: "{{ MAILU_DOCKER_DIR }}"
|
||||||
when: users[mailu_user_key].mailu_token is not defined
|
when: users[mailu_user_key].mailu_token is not defined
|
||||||
register: mailu_token_creation
|
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 }}"
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
- name: "Set mailu_token for '{{ mailu_user_key }};{{ mailu_user_name }}' in users dict if newly created"
|
- name: "Set mailu_token for '{{ mailu_user_key }};{{ mailu_user_name }}' in users dict if newly created"
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
- "{{ MAILU_IP4_PUBLIC }}:993:993"
|
- "{{ MAILU_IP4_PUBLIC }}:993:993"
|
||||||
- "{{ MAILU_IP4_PUBLIC }}:4190:4190"
|
- "{{ MAILU_IP4_PUBLIC }}:4190:4190"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{docker_compose.directories.volumes}}overrides/nginx:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides/nginx:/overrides:ro"
|
||||||
- "{{ cert_mount_directory }}:/certs:ro"
|
- "{{ cert_mount_directory }}:/certs:ro"
|
||||||
{% include 'roles/docker-container/templates/depends_on/dmbs_incl.yml.j2' %}
|
{% include 'roles/docker-container/templates/depends_on/dmbs_incl.yml.j2' %}
|
||||||
resolver:
|
resolver:
|
||||||
@ -56,7 +56,7 @@
|
|||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
volumes:
|
volumes:
|
||||||
- "dovecot_mail:/mail"
|
- "dovecot_mail:/mail"
|
||||||
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides:/overrides:ro"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
- resolver
|
- resolver
|
||||||
@ -69,7 +69,7 @@
|
|||||||
image: {{ MAILU_DOCKER_FLAVOR }}/postfix:{{ MAILU_VERSION }}
|
image: {{ MAILU_DOCKER_FLAVOR }}/postfix:{{ MAILU_VERSION }}
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
volumes:
|
volumes:
|
||||||
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides:/overrides:ro"
|
||||||
- "smtp_queue:/queue"
|
- "smtp_queue:/queue"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
@ -97,7 +97,7 @@
|
|||||||
volumes:
|
volumes:
|
||||||
- "filter:/var/lib/rspamd"
|
- "filter:/var/lib/rspamd"
|
||||||
- "dkim:/dkim"
|
- "dkim:/dkim"
|
||||||
- "{{docker_compose.directories.volumes}}overrides/rspamd:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides/rspamd:/overrides:ro"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
- redis
|
- redis
|
||||||
@ -156,7 +156,7 @@
|
|||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
volumes:
|
volumes:
|
||||||
- "webmail_data:/data"
|
- "webmail_data:/data"
|
||||||
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides:/overrides:ro"
|
||||||
depends_on:
|
depends_on:
|
||||||
- imap
|
- imap
|
||||||
- front
|
- front
|
||||||
|
@ -136,7 +136,7 @@ REAL_IP_FROM=
|
|||||||
REJECT_UNLISTED_RECIPIENT=
|
REJECT_UNLISTED_RECIPIENT=
|
||||||
|
|
||||||
# Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
|
# 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
|
# Database settings
|
||||||
|
Loading…
x
Reference in New Issue
Block a user