Add migration scripts and other fixes
This commit is contained in:
@@ -16,6 +16,7 @@ STALWART_SUBMISSION_PORT=587
|
|||||||
STALWART_IMAPS_PORT=993
|
STALWART_IMAPS_PORT=993
|
||||||
STALWART_ADMIN_BIND=127.0.0.1
|
STALWART_ADMIN_BIND=127.0.0.1
|
||||||
STALWART_ADMIN_PORT=8081
|
STALWART_ADMIN_PORT=8081
|
||||||
|
STALWART_RECOVERY_ADMIN=
|
||||||
|
|
||||||
# Roundcube port bindings
|
# Roundcube port bindings
|
||||||
ROUNDCUBE_BIND=127.0.0.1
|
ROUNDCUBE_BIND=127.0.0.1
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# EternityMail
|
||||||
|
|
||||||
|
Docker Compose deployment for Stalwart Mail and Roundcube.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker Engine with the Compose plugin
|
||||||
|
- A Linux host for the production deployment
|
||||||
|
- DNS records pointing the mail hostname to the server
|
||||||
|
- The `ep-roundcube-skin` repository available as a sibling directory if the custom skin is enabled
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create the real environment file from the template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` and set production values, especially:
|
||||||
|
|
||||||
|
- `MAIL_HOSTNAME` and `MAIL_DOMAIN`
|
||||||
|
- `RC_DB_PASSWORD`
|
||||||
|
- `RC_DB_ROOT_PASSWORD`
|
||||||
|
- `RC_DES_KEY`
|
||||||
|
- `STALWART_RECOVERY_ADMIN`
|
||||||
|
|
||||||
|
The real `.env` file is ignored by Git. Do not commit credentials or recovery settings.
|
||||||
|
|
||||||
|
## Generate a DES key
|
||||||
|
|
||||||
|
On Linux or macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/generate-des-key.sh
|
||||||
|
scripts/generate-des-key.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\generate-des-key.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Put the generated 24-character value in `RC_DES_KEY` in `.env`.
|
||||||
|
|
||||||
|
## Prepare Linux data directories
|
||||||
|
|
||||||
|
The Stalwart container runs as uid/gid `2000`. Create the bind-mounted directories and set the required ownership before starting the stack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/setup-data-dirs.sh
|
||||||
|
sudo ./scripts/setup-data-dirs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script uses `DATA_ROOT` from `.env` when it is exported in the shell; otherwise it defaults to `/opt/eternitymail`. To use the value from `.env` directly, run the setup command with it loaded or pass the path explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo DATA_ROOT=/opt/eternitymail ./scripts/setup-data-dirs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Roundcube skin
|
||||||
|
|
||||||
|
The default configuration expects the skin at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
../ep-roundcube-skin/eternity
|
||||||
|
```
|
||||||
|
|
||||||
|
relative to this repository. Override the location in `.env` with `ROUNDCUBE_SKIN_PATH` if needed. The skin directory is mounted read-only and selected with `ROUNDCUBE_SKIN_NAME`.
|
||||||
|
|
||||||
|
## Start and stop
|
||||||
|
|
||||||
|
Validate the rendered configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose config --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
View service status and logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs -f stalwart
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop the services without deleting persistent data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
Persistent data is stored under `DATA_ROOT` in the `stalwart`, `roundcube-db`, and `roundcube-config` directories.
|
||||||
|
|
||||||
|
## Network ports
|
||||||
|
|
||||||
|
The host bindings are configurable in `.env`:
|
||||||
|
|
||||||
|
- SMTP: `STALWART_SMTP_PORT` (default `25`)
|
||||||
|
- Submission: `STALWART_SUBMISSION_PORT` (default `587`)
|
||||||
|
- IMAPS: `STALWART_IMAPS_PORT` (default `993`)
|
||||||
|
- Stalwart administration: `STALWART_ADMIN_BIND` and `STALWART_ADMIN_PORT` (defaults `127.0.0.1:8081`)
|
||||||
|
- Roundcube: `ROUNDCUBE_BIND` and `ROUNDCUBE_PORT` (defaults `127.0.0.1:8091`)
|
||||||
|
|
||||||
|
The Stalwart and Roundcube containers communicate over the private `eternitymail` Docker network.
|
||||||
@@ -13,6 +13,9 @@ services:
|
|||||||
cap_add:
|
cap_add:
|
||||||
- NET_BIND_SERVICE
|
- NET_BIND_SERVICE
|
||||||
|
|
||||||
|
environment:
|
||||||
|
STALWART_RECOVERY_ADMIN: ${STALWART_RECOVERY_ADMIN}
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${STALWART_SMTP_PORT:-25}:25/tcp"
|
- "${STALWART_SMTP_PORT:-25}:25/tcp"
|
||||||
- "${STALWART_SUBMISSION_PORT:-587}:587/tcp"
|
- "${STALWART_SUBMISSION_PORT:-587}:587/tcp"
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates a stalwart-cli apply NDJSON plan (Domain + Account/User + aliases)
|
||||||
|
from a migration accounts.csv file.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$CsvPath,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Domain,
|
||||||
|
[Parameter(Mandatory = $true)][string]$OutFile
|
||||||
|
)
|
||||||
|
|
||||||
|
$rows = Import-Csv -Path $CsvPath
|
||||||
|
|
||||||
|
$missing = $rows | Where-Object { -not $_.new_password }
|
||||||
|
if ($missing) {
|
||||||
|
throw "accounts.csv has $($missing.Count) row(s) with no new_password set. Fill them in before generating the plan."
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
# Domain is upserted so the plan works whether or not the wizard already created it.
|
||||||
|
$domainOp = [ordered]@{
|
||||||
|
"@type" = "upsert"
|
||||||
|
object = "Domain"
|
||||||
|
matchOn = @("name")
|
||||||
|
value = @{ "dom" = @{ name = $Domain } }
|
||||||
|
}
|
||||||
|
$lines.Add(($domainOp | ConvertTo-Json -Compress -Depth 10))
|
||||||
|
|
||||||
|
$accounts = [ordered]@{}
|
||||||
|
foreach ($row in $rows) {
|
||||||
|
$localPart = $row.email.Split('@')[0]
|
||||||
|
$clientId = "acct-$localPart"
|
||||||
|
|
||||||
|
$aliases = @{}
|
||||||
|
if ($row.aliases) {
|
||||||
|
foreach ($aliasLocal in ($row.aliases -split ';' | Where-Object { $_ })) {
|
||||||
|
$aliases["alias-$aliasLocal"] = @{ name = $aliasLocal; domainId = "#dom" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$quotas = @{}
|
||||||
|
if ($row.quota_mb) {
|
||||||
|
$quotas["maxDiskQuota"] = [int64]$row.quota_mb * 1MB
|
||||||
|
}
|
||||||
|
|
||||||
|
$accounts[$clientId] = @{
|
||||||
|
"@type" = "User"
|
||||||
|
name = $localPart
|
||||||
|
domainId = "#dom"
|
||||||
|
credentials = @{ "pw" = @{ "@type" = "Password"; secret = $row.new_password } }
|
||||||
|
memberGroupIds = @{}
|
||||||
|
roles = @{ "@type" = "User" }
|
||||||
|
permissions = @{ "@type" = "Inherit" }
|
||||||
|
quotas = $quotas
|
||||||
|
aliases = $aliases
|
||||||
|
encryptionAtRest = @{ "@type" = "Disabled" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$accountOp = [ordered]@{
|
||||||
|
"@type" = "upsert"
|
||||||
|
object = "Account"
|
||||||
|
matchOn = @("name")
|
||||||
|
value = $accounts
|
||||||
|
}
|
||||||
|
$lines.Add(($accountOp | ConvertTo-Json -Compress -Depth 10))
|
||||||
|
|
||||||
|
$lines | Set-Content -Path $OutFile -Encoding utf8
|
||||||
|
Write-Host "Wrote $($lines.Count) operation(s) covering $($rows.Count) account(s) to $OutFile"
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Mailbox migration: docker-mailserver → Stalwart
|
||||||
|
|
||||||
|
Tools to move accounts, aliases, and mail from a `docker-mailserver` instance into
|
||||||
|
this Stalwart deployment.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Step | Linux (bash) | Windows (PowerShell) | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `export-docker-mailserver.sh` | *(run on the Linux docker-mailserver host)* | Dumps accounts + aliases from docker-mailserver's config files into `accounts.csv` |
|
||||||
|
| 2 | *(manual)* | *(manual)* | Fill in `new_password` (and `old_password` if not already known) in `accounts.csv` |
|
||||||
|
| 3 | `generate-account-plan.sh` | `New-AccountPlan.ps1` | Turns `accounts.csv` into a `stalwart-cli apply` NDJSON plan that creates the domain, accounts, and aliases in Stalwart |
|
||||||
|
| 4 | `stalwart-cli apply` | `stalwart-cli apply` | Applies the generated plan to Stalwart |
|
||||||
|
| 5 | `sync-mailboxes.sh` | `Sync-Mailboxes.ps1` | Runs `imapsync` (via Docker) for every account, copying all folders/messages from docker-mailserver to Stalwart |
|
||||||
|
| 6 | *(manual)* | *(manual)* | Verify, then repoint MX/DNS and decommission docker-mailserver |
|
||||||
|
|
||||||
|
Both old and new mail servers run Linux, so the bash scripts are the primary path;
|
||||||
|
the PowerShell scripts are equivalent for driving the migration from a Windows
|
||||||
|
workstation. Make the bash scripts executable once: `chmod +x migration/*.sh`.
|
||||||
|
`generate-account-plan.sh` requires `jq` (`apt install jq` / `dnf install jq`).
|
||||||
|
|
||||||
|
docker-mailserver stores passwords as one-way hashes (`config/postfix-accounts.cf`),
|
||||||
|
so they cannot be reused directly in Stalwart. Each account gets a **new** password
|
||||||
|
during migration; `old_password` is only needed temporarily so `imapsync` can log in
|
||||||
|
to the source server to copy mail.
|
||||||
|
|
||||||
|
## 1. Export accounts and aliases from docker-mailserver
|
||||||
|
|
||||||
|
Run on the docker-mailserver host (or `docker exec` into its container), pointing at
|
||||||
|
its config directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./export-docker-mailserver.sh /path/to/docker-mailserver/config > accounts.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces `accounts.csv` with columns `email,new_password,old_password,aliases,quota_mb`,
|
||||||
|
`new_password` and `old_password` left blank for you to fill in.
|
||||||
|
|
||||||
|
## 2. Fill in passwords
|
||||||
|
|
||||||
|
Edit `accounts.csv`:
|
||||||
|
- `new_password`: the password the account will have on Stalwart. Generate strong
|
||||||
|
random values; users can change them later via the WebUI.
|
||||||
|
- `old_password`: the account's **current, plaintext** docker-mailserver password,
|
||||||
|
needed only so `imapsync` can authenticate to the source during copy. If you don't
|
||||||
|
have it, reset it temporarily on docker-mailserver with
|
||||||
|
`setup email update <email> <temp-password>` and record that value here.
|
||||||
|
- `aliases`: optional, `;`-separated list of extra local-parts on the same domain
|
||||||
|
that should deliver to this mailbox (e.g. `info;sales`).
|
||||||
|
- `quota_mb`: optional mailbox quota in MB (blank = unlimited).
|
||||||
|
|
||||||
|
Never commit a filled-in `accounts.csv`; it contains plaintext passwords.
|
||||||
|
|
||||||
|
## 3. Generate the Stalwart account plan
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./generate-account-plan.sh -c accounts.csv -d eternityproject.fi -o accounts-plan.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./New-AccountPlan.ps1 -CsvPath accounts.csv -Domain eternityproject.fi -OutFile accounts-plan.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
This upserts the `Domain`, one `Account/User` per row (with a `Password` credential
|
||||||
|
set to `new_password` and any `aliases`), so re-running after edits is safe.
|
||||||
|
|
||||||
|
## 4. Apply the plan to Stalwart
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export STALWART_URL="https://mail.eternityproject.fi"
|
||||||
|
export STALWART_USER="admin"
|
||||||
|
export STALWART_PASSWORD="<admin password>"
|
||||||
|
|
||||||
|
stalwart-cli apply --file accounts-plan.ndjson --dry-run
|
||||||
|
stalwart-cli apply --file accounts-plan.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:STALWART_URL = "https://mail.eternityproject.fi"
|
||||||
|
$env:STALWART_USER = "admin"
|
||||||
|
$env:STALWART_PASSWORD = "<admin password>"
|
||||||
|
|
||||||
|
stalwart-cli apply --file accounts-plan.ndjson --dry-run
|
||||||
|
stalwart-cli apply --file accounts-plan.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Copy mail with imapsync
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./sync-mailboxes.sh -c accounts.csv -s mail.old-domain.example -t mail.eternityproject.fi -n
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./Sync-Mailboxes.ps1 `
|
||||||
|
-CsvPath accounts.csv `
|
||||||
|
-SourceHost mail.old-domain.example `
|
||||||
|
-DestHost mail.eternityproject.fi `
|
||||||
|
-DryRun
|
||||||
|
```
|
||||||
|
|
||||||
|
(`-n` / `-DryRun` runs imapsync in dry-run mode first; drop it to actually copy messages.)
|
||||||
|
|
||||||
|
Review the dry-run output, then re-run without `-DryRun` to actually copy messages.
|
||||||
|
The script is safe to re-run: `imapsync` skips messages that already exist at the
|
||||||
|
destination.
|
||||||
|
|
||||||
|
## 6. Cut over
|
||||||
|
|
||||||
|
- Spot-check folder counts/message counts on a few mailboxes in the Stalwart WebUI.
|
||||||
|
- Run `Sync-Mailboxes.ps1` once more shortly before cutover to catch mail received
|
||||||
|
during the migration window.
|
||||||
|
- Update MX records to point at `mail.eternityproject.fi` (see the top-level
|
||||||
|
[README.md](../README.md)).
|
||||||
|
- Once confirmed, decommission docker-mailserver and rotate the temporary
|
||||||
|
`old_password` values you set in step 2.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Runs imapsync (via the dockerized imapsync/imapsync image) for every account
|
||||||
|
in a migration accounts.csv, copying mail from docker-mailserver to Stalwart.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$CsvPath,
|
||||||
|
[Parameter(Mandatory = $true)][string]$SourceHost,
|
||||||
|
[Parameter(Mandatory = $true)][string]$DestHost,
|
||||||
|
[int]$SourcePort = 993,
|
||||||
|
[int]$DestPort = 993,
|
||||||
|
[switch]$DryRun
|
||||||
|
)
|
||||||
|
|
||||||
|
$rows = Import-Csv -Path $CsvPath
|
||||||
|
|
||||||
|
$missing = $rows | Where-Object { -not $_.old_password -or -not $_.new_password }
|
||||||
|
if ($missing) {
|
||||||
|
throw "accounts.csv has $($missing.Count) row(s) missing old_password or new_password. Fill them in before syncing."
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($row in $rows) {
|
||||||
|
Write-Host "==> Syncing $($row.email)"
|
||||||
|
|
||||||
|
$dockerArgs = @(
|
||||||
|
"run", "--rm", "imapsync/imapsync",
|
||||||
|
"--host1", $SourceHost, "--port1", $SourcePort, "--ssl1",
|
||||||
|
"--user1", $row.email, "--password1", $row.old_password,
|
||||||
|
"--host2", $DestHost, "--port2", $DestPort, "--ssl2",
|
||||||
|
"--user2", $row.email, "--password2", $row.new_password,
|
||||||
|
"--automap", "--syncinternaldates", "--skipcrossduplicates",
|
||||||
|
"--no-modulesversion"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
$dockerArgs += "--dry"
|
||||||
|
}
|
||||||
|
|
||||||
|
docker @dockerArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warning "imapsync exited with code $LASTEXITCODE for $($row.email)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
email,new_password,old_password,aliases,quota_mb
|
||||||
|
alice@eternityproject.fi,,,,
|
||||||
|
bob@eternityproject.fi,,,info;sales,2048
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Dumps accounts + aliases from a docker-mailserver config directory into a
|
||||||
|
# migration CSV skeleton (email,new_password,old_password,aliases,quota_mb).
|
||||||
|
# Passwords are left blank: docker-mailserver only stores irreversible hashes.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="${1:?Usage: $0 /path/to/docker-mailserver/config}"
|
||||||
|
ACCOUNTS_FILE="$CONFIG_DIR/postfix-accounts.cf"
|
||||||
|
ALIASES_FILE="$CONFIG_DIR/postfix-virtual.cf"
|
||||||
|
|
||||||
|
if [[ ! -f "$ACCOUNTS_FILE" ]]; then
|
||||||
|
echo "postfix-accounts.cf not found under $CONFIG_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "email,new_password,old_password,aliases,quota_mb"
|
||||||
|
|
||||||
|
while IFS='|' read -r email _hash; do
|
||||||
|
[[ -z "$email" || "$email" == \#* ]] && continue
|
||||||
|
|
||||||
|
aliases=""
|
||||||
|
if [[ -f "$ALIASES_FILE" ]]; then
|
||||||
|
# postfix-virtual.cf lines look like: alias@domain destination@domain
|
||||||
|
aliases=$(awk -v dest="$email" '$2 == dest { sub(/@.*/, "", $1); printf "%s;", $1 }' "$ALIASES_FILE")
|
||||||
|
aliases="${aliases%;}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$email,,,${aliases},"
|
||||||
|
done < "$ACCOUNTS_FILE"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generates a stalwart-cli apply NDJSON plan (Domain + Account/User + aliases)
|
||||||
|
# from a migration accounts.csv file. Requires jq.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() { echo "Usage: $0 -c accounts.csv -d domain.tld -o accounts-plan.ndjson" >&2; exit 1; }
|
||||||
|
|
||||||
|
while getopts "c:d:o:" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
c) CSV="$OPTARG" ;;
|
||||||
|
d) DOMAIN="$OPTARG" ;;
|
||||||
|
o) OUT="$OPTARG" ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ -z "${CSV:-}" || -z "${DOMAIN:-}" || -z "${OUT:-}" ]] && usage
|
||||||
|
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
|
||||||
|
|
||||||
|
: > "$OUT"
|
||||||
|
|
||||||
|
jq -nc --arg domain "$DOMAIN" \
|
||||||
|
'{"@type":"upsert","object":"Domain","matchOn":["name"],"value":{"dom":{"name":$domain}}}' >> "$OUT"
|
||||||
|
|
||||||
|
accounts_file=$(mktemp)
|
||||||
|
trap 'rm -f "$accounts_file"' EXIT
|
||||||
|
|
||||||
|
# Process substitution keeps the loop in the current shell, so a missing
|
||||||
|
# password can abort the whole script instead of just a pipeline subshell.
|
||||||
|
while IFS=',' read -r email new_password old_password aliases quota_mb; do
|
||||||
|
[[ -z "$email" ]] && continue
|
||||||
|
|
||||||
|
if [[ -z "$new_password" ]]; then
|
||||||
|
echo "Missing new_password for $email" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local_part="${email%%@*}"
|
||||||
|
|
||||||
|
aliases_json=$(jq -n --arg aliases "$aliases" '
|
||||||
|
($aliases | split(";") | map(select(length > 0))) as $names
|
||||||
|
| reduce $names[] as $n ({}; . + {("alias-" + $n): {name: $n, domainId: "#dom"}})')
|
||||||
|
|
||||||
|
quotas_json="{}"
|
||||||
|
if [[ -n "$quota_mb" ]]; then
|
||||||
|
quotas_json=$(jq -n --argjson mb "$quota_mb" '{"maxDiskQuota": ($mb * 1048576)}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -n --arg id "acct-$local_part" --arg name "$local_part" --arg secret "$new_password" \
|
||||||
|
--argjson aliases "$aliases_json" --argjson quotas "$quotas_json" '
|
||||||
|
{ ($id): {
|
||||||
|
"@type": "User",
|
||||||
|
name: $name,
|
||||||
|
domainId: "#dom",
|
||||||
|
credentials: { pw: { "@type": "Password", secret: $secret } },
|
||||||
|
memberGroupIds: {},
|
||||||
|
roles: { "@type": "User" },
|
||||||
|
permissions: { "@type": "Inherit" },
|
||||||
|
quotas: $quotas,
|
||||||
|
aliases: $aliases,
|
||||||
|
encryptionAtRest: { "@type": "Disabled" }
|
||||||
|
}
|
||||||
|
}' >> "$accounts_file"
|
||||||
|
done < <(tail -n +2 "$CSV")
|
||||||
|
|
||||||
|
accounts_value=$(jq -s 'add' "$accounts_file")
|
||||||
|
jq -nc --argjson value "$accounts_value" \
|
||||||
|
'{"@type":"upsert","object":"Account","matchOn":["name"],"value":$value}' >> "$OUT"
|
||||||
|
|
||||||
|
echo "Wrote plan to $OUT"
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runs imapsync (via the dockerized imapsync/imapsync image) for every account
|
||||||
|
# in a migration accounts.csv, copying mail from docker-mailserver to Stalwart.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SOURCE_PORT=993
|
||||||
|
DEST_PORT=993
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 -c accounts.csv -s source-host -t dest-host [-p source-port] [-q dest-port] [-n]" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while getopts "c:s:t:p:q:n" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
c) CSV="$OPTARG" ;;
|
||||||
|
s) SOURCE_HOST="$OPTARG" ;;
|
||||||
|
t) DEST_HOST="$OPTARG" ;;
|
||||||
|
p) SOURCE_PORT="$OPTARG" ;;
|
||||||
|
q) DEST_PORT="$OPTARG" ;;
|
||||||
|
n) DRY_RUN=true ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ -z "${CSV:-}" || -z "${SOURCE_HOST:-}" || -z "${DEST_HOST:-}" ]] && usage
|
||||||
|
|
||||||
|
while IFS=',' read -r email new_password old_password aliases quota_mb; do
|
||||||
|
[[ -z "$email" ]] && continue
|
||||||
|
|
||||||
|
if [[ -z "$old_password" || -z "$new_password" ]]; then
|
||||||
|
echo "Missing old_password or new_password for $email" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Syncing $email"
|
||||||
|
|
||||||
|
args=(run --rm imapsync/imapsync
|
||||||
|
--host1 "$SOURCE_HOST" --port1 "$SOURCE_PORT" --ssl1 --user1 "$email" --password1 "$old_password"
|
||||||
|
--host2 "$DEST_HOST" --port2 "$DEST_PORT" --ssl2 --user2 "$email" --password2 "$new_password"
|
||||||
|
--automap --syncinternaldates --skipcrossduplicates --no-modulesversion)
|
||||||
|
|
||||||
|
[[ "$DRY_RUN" == true ]] && args+=(--dry)
|
||||||
|
|
||||||
|
if ! docker "${args[@]}"; then
|
||||||
|
echo "warning: imapsync exited non-zero for $email" >&2
|
||||||
|
fi
|
||||||
|
done < <(tail -n +2 "$CSV")
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Utility Scripts
|
||||||
|
|
||||||
|
These scripts support the EternityMail Docker Compose deployment. Run them from the repository root.
|
||||||
|
|
||||||
|
## `generate-des-key.sh`
|
||||||
|
|
||||||
|
Generates a random 24-character alphanumeric value for Roundcube's `RC_DES_KEY` setting.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- Linux, macOS, or another Unix-like shell
|
||||||
|
- `/dev/urandom`
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/generate-des-key.sh
|
||||||
|
scripts/generate-des-key.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the printed value into `.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
RC_DES_KEY=<generated-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep this value stable after Roundcube has been deployed. Changing it can make existing encrypted session data unreadable.
|
||||||
|
|
||||||
|
## `generate-des-key.ps1`
|
||||||
|
|
||||||
|
Generates the same type of 24-character key using PowerShell.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- PowerShell 5.1 or PowerShell 7+
|
||||||
|
|
||||||
|
Usage from PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\generate-des-key.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the printed value into `RC_DES_KEY` in `.env`.
|
||||||
|
|
||||||
|
## `setup-data-dirs.sh`
|
||||||
|
|
||||||
|
Creates the host directories used by the Stalwart and Roundcube containers and assigns ownership of the Stalwart directories to uid/gid `2000`, which is the user used by the Stalwart image.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- Linux or another Unix-like host
|
||||||
|
- Permission to create and change ownership under `DATA_ROOT`
|
||||||
|
|
||||||
|
The script does not read `.env` automatically. Set `DATA_ROOT` explicitly when running it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/setup-data-dirs.sh
|
||||||
|
sudo DATA_ROOT=/opt/eternitymail ./scripts/setup-data-dirs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
When `DATA_ROOT` is omitted, the script uses `/opt/eternitymail`.
|
||||||
|
|
||||||
|
The created directories are:
|
||||||
|
|
||||||
|
- `$DATA_ROOT/stalwart/etc`
|
||||||
|
- `$DATA_ROOT/stalwart/data`
|
||||||
|
- `$DATA_ROOT/stalwart/certs`
|
||||||
|
- `$DATA_ROOT/roundcube-db`
|
||||||
|
- `$DATA_ROOT/roundcube-config`
|
||||||
|
|
||||||
|
Only `$DATA_ROOT/stalwart` is changed to uid/gid `2000:2000`; the MariaDB container manages its own data-directory permissions.
|
||||||
|
|
||||||
|
## Typical order
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
scripts/generate-des-key.sh
|
||||||
|
sudo DATA_ROOT=/opt/eternitymail ./scripts/setup-data-dirs.sh
|
||||||
|
docker compose config --quiet
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Creates the host data directories for stalwart/roundcube and sets ownership
|
||||||
|
# expected by each container (stalwart runs as uid/gid 2000).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DATA_ROOT="${DATA_ROOT:-/opt/eternitymail}"
|
||||||
|
|
||||||
|
mkdir -p \
|
||||||
|
"$DATA_ROOT/stalwart/etc" \
|
||||||
|
"$DATA_ROOT/stalwart/data" \
|
||||||
|
"$DATA_ROOT/stalwart/certs" \
|
||||||
|
"$DATA_ROOT/roundcube-db" \
|
||||||
|
"$DATA_ROOT/roundcube-config"
|
||||||
|
|
||||||
|
chown -R 2000:2000 "$DATA_ROOT/stalwart"
|
||||||
|
|
||||||
|
echo "Data directories ready under $DATA_ROOT"
|
||||||
Reference in New Issue
Block a user