Thanks for the tip, I used that script as a base to create separate scripts (filters, signatures, shares) and worked very good.mutface wrote: ↑Mon Jul 22, 2024 11:01 am By the way - I found this really useful script that will migrate emails, calendars, briefcases, share settings, filters, contacts, etc. between any version of Zimbra to any linux distribution. So for instance, I've been able to migrate really old Zimbra instances from Centos 6 to rocky Linux 8. From Zimbra 7 to Zimbra 8, etc.
https://github.com/hodfords/zimbra-migration
It runs on shell scripts and is quite good; it is also open source too.
Migrate FOSS 8.8.15 to fresh new Zimbra 10 Network Edition,
-
7224jobe
- Outstanding Member

- Posts: 304
- Joined: Sat Sep 13, 2014 1:55 am
- ZCS/ZD Version: 10.1.*.UBUNTU20_64 NETWORK
Re: Migrate FOSS 8.8.15 to fresh new Zimbra 10 Network Edition,
-
stcircumstance
- Posts: 2
- Joined: Thu Mar 05, 2020 12:16 am
Re: Migrate FOSS 8.8.15 to fresh new Zimbra 10 Network Edition,
Here is what I did.
# Migrating Zimbra 8.8.15 to 10.1 (FOSS) without losing anything
A field guide to moving a Zimbra install from 8.8.15 to 10.1 by building a **fresh
10.1 server and migrating content per-mailbox**, rather than an in-place upgrade.
This is the path you want when the old OS is also end-of-life, the box is
questionable, or you simply want a clean, supported foundation.
It works, and every mailbox, folder, contact, calendar, shared folder, filter and
(if you want) password comes across. But there are about a dozen non-obvious traps
that will cost you a day each if you meet them cold. They are collected at the end,
and flagged inline. Nothing here is exotic; it is all `zmprov`, `zmmailbox`,
`ldapsearch`/`ldapmodify` and DNS. Commands use `user@example.com` and
OLD (8.8.15) / NEW (10.1) for the two servers.
> Scope: single-server FOSS installs, LDAP auth (`zimbraAuthMech zimbra`). Multi-server
> or external-auth setups need adaptation. Test the whole thing on a throwaway box
> first; rehearse at least once against real data.
---
## The approach in one paragraph
Stand up a parallel 10.1 box. Recreate domains, COS and accounts from a **sanitized**
copy of the old LDAP (preserving each account's `zimbraId`). Copy each mailbox as a
`.tgz` over Zimbra's REST endpoints. Migrate password hashes directly in LDAP. Repair
the few things that do not survive a REST import (mountpoints, DKIM, certs). Then cut
over by pointing mail routing at the new box. Downtime is minimized by importing the
bulk days ahead and syncing only a small delta during a short freeze.
---
## Phase 1 — Inventory and dump from the OLD server (read-only)
As `zimbra` on OLD. None of this changes anything.
```bash
zmprov -l gad > domains.txt
zmprov -l gac > cos.txt
zmprov -l gaa | grep -vE '^(admin@|galsync[.@]|ham\.|spam\.|virus-quarantine\.)' > accounts.txt
# Per-account: a full reference dump, signatures, identities, data sources, filters.
mkdir -p accounts
while read a; do
s="${a//@/_at_}"
zmprov -l ga "$a" </dev/null | grep -v '^userPassword:' > "accounts/${s}.full"
zmprov gsig "$a" </dev/null > "accounts/${s}.signatures"
zmprov ga "$a" zimbraMailSieveScript </dev/null > "accounts/${s}.filters"
zmmailbox -z -m "$a" gaf </dev/null > "accounts/${s}.folders" # for mountpoints later
done < accounts.txt
```
> **Trap 1 — the dump is COS-merged.** `zmprov -l ga` returns ~490 attributes per
> account, most inherited from the COS. Replaying that verbatim writes every inherited
> default onto the account and **breaks COS inheritance**, and drags 8.x-era attributes
> onto 10.1 where some no longer exist. Diff each account against the COS default and
> carry only the real per-account overrides (roughly 20 attributes). Build the
> provisioning as an **allowlist**, never a denylist.
**Never replay** (the important exclusions):
- `userPassword` (migrated separately, see Phase 3), `zimbraAuthTokens`,
`zimbraCsrfTokenData` — live tokens; replaying them hands an old session onto the
clean box.
- `zimbraMailHost`, `zimbraMailTransport` — they name the OLD host; replayed, they aim
the account back at the old server.
- `objectClass`, `cn`, `uid`, `mail`, `zimbraId` as a *modify* — set at creation only
(but see Trap 2 — you DO set `zimbraId` at creation).
- `zimbraIsAdminAccount` — set by hand after review, never inherited.
- 8.x-only UI attrs (`zimbraFeatureTouchClientEnabled`, old skins, disabled-zimlet
lists). Unknown attributes error out on 10.1.
**Carry list** (roughly): `displayName`, `givenName`, `sn`, `zimbraCOSId`,
`zimbraAccountStatus` (value only, applied last), `zimbraMailStatus`, `zimbraMailQuota`
(as a real override only), the `zimbraPref*` you care about, and `zimbraMailSieveScript`
after review. Aliases carry via `zmprov aaa`, not as a `ca` argument.
---
## Phase 2 — Build the NEW server
- Install 10.1 FOSS on a supported OS. **Set the final hostname before installing** and
never rename Zimbra afterward. Answer the package prompts deliberately, in particular
`zimbra-dnscache = N` and `zimbra-imapd = N` unless you specifically want them.
- Recreate COS. The `default` COS UUID (`e00428a1-0c00-11d9-836a-000d93afea2a`) is
static across installs, so accounts can reference it unchanged; confirm with
`zmprov gc default zimbraId`. Apply only the reviewed COS deltas that matter
(`zimbraMailQuota`, `zimbraPrefTimeZoneId`, feature flags).
- Recreate domains. Note **alias domains** are a distinct type: create with
`zmprov cad <alias> <target>` (createAliasDomain), not `cd` then `md` —
`zimbraDomainType` is immutable and can only be set at creation.
---
## Phase 3 — Provision accounts (the load-bearing tricks)
### Preserve `zimbraId` at creation
```bash
zmprov ca user@example.com 'TempPass1!' \
zimbraId <ORIGINAL-UUID-FROM-DUMP> \
zimbraCOSId e00428a1-0c00-11d9-836a-000d93afea2a \
displayName "..." givenName "..." sn "..."
zmprov ma user@example.com zimbraAccountStatus active zimbraMailStatus enabled
zmprov aaa user@example.com alias@example.com
```
> **Trap 2 — preserve `zimbraId`, and do it at creation.** It is settable only when the
> account is created. Folder shares and mountpoints reference the owner by `zimbraId`,
> not by address. Preserve the original id and **grants ride in with the mailbox import
> for free**; skip it and every share breaks silently. Test it on one throwaway account
> first (`zmprov ga test@... zimbraId` should echo the UUID you set); Zimbra sometimes
> treats it as immutable, and if so you must plan on rebuilding shares by hand.
> **Trap 3 — create every account `active`, even the closed ones.** A `closed` account
> has no accessible mailbox, so the content import fails on it. Provision `active`,
> import, then apply the real `zimbraAccountStatus` as the very last step.
### Migrate password hashes (so nobody changes their password)
Optional — you can instead set fresh passwords and force a change. If you want to keep
existing passwords, migrate the hashes:
> **Trap 4 — `zmprov ga user userPassword` returns the literal string `VALUE-BLOCKED`,
> not the hash.** Extract with `ldapsearch` and write with `ldapmodify`; never with
> `zmprov`. Setting passwords from a VALUE-BLOCKED extraction locks out every user.
On OLD, get the LDAP creds and pull the hash (base64):
```bash
source /opt/zimbra/bin/zmshutil; zmsetvars # do NOT run under `set -u`; zmshutil uses $1
ldapsearch -x -LLL -o ldif-wrap=no -H "$ldap_url" -D "$zimbra_ldap_userdn" \
-w "$zimbra_ldap_password" -b '' \
"(zimbraMailDeliveryAddress=user@example.com)" userPassword
# -> userPassword:: <base64 of {SSHA512}...>
```
On NEW, find the account DN and write the same base64 verbatim:
```bash
source /opt/zimbra/bin/zmshutil; zmsetvars
DN=$(ldapsearch -x -LLL -H "$ldap_url" -D "$zimbra_ldap_userdn" -w "$zimbra_ldap_password" \
-b '' "(zimbraMailDeliveryAddress=user@example.com)" dn | sed -n 's/^dn: //p')
cat > /tmp/pw.ldif <<EOF
dn: $DN
changetype: modify
replace: userPassword
userPassword:: <base64-from-OLD>
EOF
ldapmodify -x -H "$ldap_url" -D "$zimbra_ldap_userdn" -w "$zimbra_ldap_password" -f /tmp/pw.ldif
```
`zmprov ma userPassword` may re-hash the value; `ldapmodify` writes the `{SSHA512}` bytes
verbatim, which is what you need. Shred the intermediate files afterward.
---
## Phase 4 — Migrate content over REST
Zimbra's per-mailbox export/import is HTTP, not a disk copy:
```bash
# Export on OLD:
zmmailbox -z -m user@example.com getRestURL '//?fmt=tgz' > user.tgz
# Import on NEW:
zmmailbox -z -m user@example.com postRestURL '//?fmt=tgz&resolve=skip' user.tgz
```
> **Trap 5 — import through mailboxd on :8443, not the nginx proxy on :443.**
> `postRestURL` uploads the archive, then waits for one response after mailboxd has
> processed the whole thing. Through nginx (443) the response is buffered, so the client
> sees >30s of silence and dies with `zclient.IO_ERROR (Read timed out)` on any large
> mailbox. `zmmailbox -t` does **not** help (it sets the SOAP timeout, not the REST
> socket timeout `socket_so_timeout`, default 30s). Point the import at mailboxd
> directly. In practice: set the domain's `zimbraPublicServiceHostname` to a name that
> resolves to NEW, `zimbraPublicServiceProtocol https`, `zimbraPublicServicePort 8443`
> for the duration of the import. (Export over 443 is fine — `getRestURL` streams bytes
> continuously, nothing to buffer.)
> **Trap 6 — raise message-size limits before importing large-attachment mail.** A fresh
> 10.1 `zimbraMailContentMaxSize` can be lower than the largest single message a mailbox
> holds, and oversized messages are dropped on import. Set
> `zimbraMtaMaxMessageSize` / `zimbraMailContentMaxSize` on NEW to at least the OLD
> server's MTA ceiling, then `zmmailboxdctl restart`.
**Minimize downtime with pre-seed + delta.** Import the bulk while OLD is still live
(no outage). At cutover, stop OLD's MTA, export only what arrived since the pre-seed
(`getRestURL '//?fmt=tgz&query=after:<date>'`) and import with `resolve=skip` (dedups).
The freeze is then minutes, not hours. Caveat: a `resolve=skip` delta adds new mail but
does not propagate read/unread, folder moves or deletions users made in the gap; minor
drift, acceptable for most.
### Verify by message count, not bytes
```bash
# ground truth from the export archive:
tar tzf user.tgz | grep -c '\.eml$'
# on NEW, count message-view folders:
zmmailbox -z -m user@example.com gaf </dev/null | awk '$2=="mess"||$2=="unkn"{s+=$4} END{print s}'
```
> **Trap 7 — count `mess` AND `unkn` view folders.** Quota-used differs across versions
> (dedup/blob handling), so bytes are useless for verification. Message counts are
> stable. But a folder whose default view was never set reports view `unkn` and still
> holds mail, so a `mess`-only count under-reports. Do not widen to all views — that
> pulls in Calendar/Contacts/Tasks/Briefcase items, which are not `.eml`.
---
## Phase 5 — Shares and mountpoints
Two halves. If `zimbraId` was preserved (Trap 2), the **grants** (folder ACLs) are inside
the mailbox tgz and restore themselves; a `zmmailbox ... mfg ...` replay returns
`mail.GRANT_EXISTS`, which is the success signal, not an error. The **mountpoints** (the
links in the grantee's mailbox) are the real work:
> **Trap 8 — mountpoints break because folder IDs are reassigned on import.** A mountpoint
> stores the owner's shared folder as a numeric **id**. A `.tgz` import reassigns ids for
> custom folders (standard folders keep fixed ids — Inbox 2, Calendar 10, etc.). So
> Calendar/Contacts shares survive, but every custom/Briefcase mountpoint that came in
> with the import points at the owner's OLD id, which on NEW is now nothing (folder shows
> crossed-out / unavailable) or a *different* folder (shows the wrong contents).
>
> Fix: recreate every mountpoint with `zmmailbox cm` **by owner path** — `cm` resolves the
> path to the current id at creation time. Delete the stale link first (this removes only
> the grantee's link; the owner's folder and data are untouched):
>
> ```bash
> zmmailbox -z -m grantee@example.com deleteFolder "/Local Name"
> zmmailbox -z -m grantee@example.com cm "/Local Name" owner@example.com "/Owners/Folder/Path"
> ```
>
> The owner's `gaf` output shows the mountpoint as `/Local Name (owner@domain:ID)`; look
> up that ID in the owner's `gaf` to get the current path, and create by path. Verify: the
> `(owner@domain:ID)` on the recreated mountpoint must equal the owner's *current* folder id.
> **Trap 9 — non-ASCII arguments need `LC_ALL=C.UTF-8`.** Zimbra's `.bash_profile` may
> force `LANG=C`/`LC_ALL=C`; Java then decodes non-ASCII command-line arguments as ASCII
> and stores them mangled. Folder names with a curly apostrophe come out as replacement
> characters. Prefix any `cm`/`mfg`/`zmprov -f` run that carries non-ASCII with
> `LC_ALL=C.UTF-8`. (Content import is immune — it reads the tgz as bytes; this only bites
> values passed as CLI arguments.)
Guest/public shares carry a password and cannot be reconstructed; recreate and
re-communicate by hand.
---
## Phase 6 — Filters, signatures, DKIM, certs
- **Filters (sieve):** an LDAP attribute, not mailbox content, so they do **not** ride in
with the tgz. Set `zimbraMailSieveScript` per account from your dump. For large
multi-line scripts, `LC_ALL=C.UTF-8 zmprov ma user@example.com zimbraMailSieveScript "$(cat script.sieve)"`
works (the whole file is one quoted argument; embedded quotes/newlines are literal data).
- **Signatures:** separate LDAP objects; recreate with `zmprov csig`, then repoint
`zimbraPrefDefaultSignatureId` (IDs are reassigned).
- **DKIM:** generate fresh keys on NEW per domain (`/opt/zimbra/libexec/zmdkimkeyutil -a -d
<domain>`) and publish the new selector TXT records. Leave the OLD selector published a
few days so in-flight mail still verifies.
- **Certificate:** issue a new cert for NEW covering the hostnames clients actually connect
to. `zmcertmgr` runs as `zimbra`, not root, on 10.1, and it wants the key + cert readable
by that user and the CA chain (intermediate + root) appended: `zmcertmgr verifycrt comm
key cert cachain` then `deploycrt comm cert cachain`, then `zmcontrol restart`.
---
## Phase 7 — Cutover
1. Stop OLD's MTA (`zmmtactl stop`); confirm `postqueue -p` is empty (drain if not). Mail
now queues at senders / your backup MX, nothing is lost.
2. Final delta export/import (Phase 4).
3. Point mail routing at NEW (DNS MX / A records, or your firewall's port-forward, whatever
your topology uses). Set each domain's `zimbraPublicServiceHostname` to the live client
name on port 443, and pin that name to NEW's own IP in NEW's `/etc/hosts` so the nightly
backup's `getRestURL` reaches itself (routers often don't hairpin the public IP).
4. Start NEW's MTA; apply real account statuses (close the closed ones); apply real quotas.
5. Verify outbound authentication with a tool like `check-auth@verifier.port25.com`, you
want **SPF pass, DKIM pass (new selector), DMARC pass**, and reverse-DNS/FCrDNS matching.
6. Keep OLD intact and powered (MTA down) for a reversion window (~5 days). Reversion =
NEW MTA down, export what arrived on NEW since cutover, import into OLD with
`resolve=skip`, routing back to OLD.
---
## What users lose (warn them)
REST export does not carry everything. Communicate these:
- **IMAP clients re-download every folder** (item IDs change, so UIDVALIDITY changes).
- **Mobile ActiveSync re-syncs from scratch** (partnerships don't migrate).
- **Dumpster/recover-deleted is not exported** — tell users to restore what they need first.
- **Saved searches** are not exported (recreate).
- **2FA and app-specific passwords** must be re-enrolled.
- **Only the latest Briefcase/document version** survives (versions aren't exported).
---
## The traps, in one list
1. The LDAP dump is COS-merged — sanitize to an allowlist; don't replay it whole.
2. Preserve `zimbraId` at account creation so shares/grants resolve.
3. Create accounts `active`; apply `closed`/real status only after import.
4. `zmprov` masks `userPassword` as `VALUE-BLOCKED`; migrate hashes via `ldapsearch` +
`ldapmodify`, not `zmprov`. (`set -u` also breaks sourcing `zmshutil`.)
5. Import via mailboxd `:8443`, not the nginx proxy `:443` (buffered response → 30s
socket timeout). `-t` doesn't fix it.
6. Raise `zimbraMailContentMaxSize` / `zimbraMtaMaxMessageSize` before importing
large-attachment mail, or messages get dropped.
7. Verify by counting `mess`+`unkn` view folders, not bytes.
8. Mountpoints reference folder IDs that get reassigned on import — recreate `cm` by owner
path (standard folders like Calendar keep fixed IDs and survive).
9. `LC_ALL=C.UTF-8` for any non-ASCII CLI argument, or Java mangles it.
10. Filters and signatures are LDAP objects — they don't ride in with the mailbox; restore
them separately.
11. DKIM keys and certs are fresh on the new box; publish new selectors, issue a new cert.
12. Pre-seed the bulk import while OLD is live, sync only a small delta during the freeze.
---
*Written up from a real 8.8.15 → 10.1.20 FOSS migration. Rehearse against real data before
the live run; the traps above are the ones that only surface with production mailboxes.*
# Migrating Zimbra 8.8.15 to 10.1 (FOSS) without losing anything
A field guide to moving a Zimbra install from 8.8.15 to 10.1 by building a **fresh
10.1 server and migrating content per-mailbox**, rather than an in-place upgrade.
This is the path you want when the old OS is also end-of-life, the box is
questionable, or you simply want a clean, supported foundation.
It works, and every mailbox, folder, contact, calendar, shared folder, filter and
(if you want) password comes across. But there are about a dozen non-obvious traps
that will cost you a day each if you meet them cold. They are collected at the end,
and flagged inline. Nothing here is exotic; it is all `zmprov`, `zmmailbox`,
`ldapsearch`/`ldapmodify` and DNS. Commands use `user@example.com` and
OLD (8.8.15) / NEW (10.1) for the two servers.
> Scope: single-server FOSS installs, LDAP auth (`zimbraAuthMech zimbra`). Multi-server
> or external-auth setups need adaptation. Test the whole thing on a throwaway box
> first; rehearse at least once against real data.
---
## The approach in one paragraph
Stand up a parallel 10.1 box. Recreate domains, COS and accounts from a **sanitized**
copy of the old LDAP (preserving each account's `zimbraId`). Copy each mailbox as a
`.tgz` over Zimbra's REST endpoints. Migrate password hashes directly in LDAP. Repair
the few things that do not survive a REST import (mountpoints, DKIM, certs). Then cut
over by pointing mail routing at the new box. Downtime is minimized by importing the
bulk days ahead and syncing only a small delta during a short freeze.
---
## Phase 1 — Inventory and dump from the OLD server (read-only)
As `zimbra` on OLD. None of this changes anything.
```bash
zmprov -l gad > domains.txt
zmprov -l gac > cos.txt
zmprov -l gaa | grep -vE '^(admin@|galsync[.@]|ham\.|spam\.|virus-quarantine\.)' > accounts.txt
# Per-account: a full reference dump, signatures, identities, data sources, filters.
mkdir -p accounts
while read a; do
s="${a//@/_at_}"
zmprov -l ga "$a" </dev/null | grep -v '^userPassword:' > "accounts/${s}.full"
zmprov gsig "$a" </dev/null > "accounts/${s}.signatures"
zmprov ga "$a" zimbraMailSieveScript </dev/null > "accounts/${s}.filters"
zmmailbox -z -m "$a" gaf </dev/null > "accounts/${s}.folders" # for mountpoints later
done < accounts.txt
```
> **Trap 1 — the dump is COS-merged.** `zmprov -l ga` returns ~490 attributes per
> account, most inherited from the COS. Replaying that verbatim writes every inherited
> default onto the account and **breaks COS inheritance**, and drags 8.x-era attributes
> onto 10.1 where some no longer exist. Diff each account against the COS default and
> carry only the real per-account overrides (roughly 20 attributes). Build the
> provisioning as an **allowlist**, never a denylist.
**Never replay** (the important exclusions):
- `userPassword` (migrated separately, see Phase 3), `zimbraAuthTokens`,
`zimbraCsrfTokenData` — live tokens; replaying them hands an old session onto the
clean box.
- `zimbraMailHost`, `zimbraMailTransport` — they name the OLD host; replayed, they aim
the account back at the old server.
- `objectClass`, `cn`, `uid`, `mail`, `zimbraId` as a *modify* — set at creation only
(but see Trap 2 — you DO set `zimbraId` at creation).
- `zimbraIsAdminAccount` — set by hand after review, never inherited.
- 8.x-only UI attrs (`zimbraFeatureTouchClientEnabled`, old skins, disabled-zimlet
lists). Unknown attributes error out on 10.1.
**Carry list** (roughly): `displayName`, `givenName`, `sn`, `zimbraCOSId`,
`zimbraAccountStatus` (value only, applied last), `zimbraMailStatus`, `zimbraMailQuota`
(as a real override only), the `zimbraPref*` you care about, and `zimbraMailSieveScript`
after review. Aliases carry via `zmprov aaa`, not as a `ca` argument.
---
## Phase 2 — Build the NEW server
- Install 10.1 FOSS on a supported OS. **Set the final hostname before installing** and
never rename Zimbra afterward. Answer the package prompts deliberately, in particular
`zimbra-dnscache = N` and `zimbra-imapd = N` unless you specifically want them.
- Recreate COS. The `default` COS UUID (`e00428a1-0c00-11d9-836a-000d93afea2a`) is
static across installs, so accounts can reference it unchanged; confirm with
`zmprov gc default zimbraId`. Apply only the reviewed COS deltas that matter
(`zimbraMailQuota`, `zimbraPrefTimeZoneId`, feature flags).
- Recreate domains. Note **alias domains** are a distinct type: create with
`zmprov cad <alias> <target>` (createAliasDomain), not `cd` then `md` —
`zimbraDomainType` is immutable and can only be set at creation.
---
## Phase 3 — Provision accounts (the load-bearing tricks)
### Preserve `zimbraId` at creation
```bash
zmprov ca user@example.com 'TempPass1!' \
zimbraId <ORIGINAL-UUID-FROM-DUMP> \
zimbraCOSId e00428a1-0c00-11d9-836a-000d93afea2a \
displayName "..." givenName "..." sn "..."
zmprov ma user@example.com zimbraAccountStatus active zimbraMailStatus enabled
zmprov aaa user@example.com alias@example.com
```
> **Trap 2 — preserve `zimbraId`, and do it at creation.** It is settable only when the
> account is created. Folder shares and mountpoints reference the owner by `zimbraId`,
> not by address. Preserve the original id and **grants ride in with the mailbox import
> for free**; skip it and every share breaks silently. Test it on one throwaway account
> first (`zmprov ga test@... zimbraId` should echo the UUID you set); Zimbra sometimes
> treats it as immutable, and if so you must plan on rebuilding shares by hand.
> **Trap 3 — create every account `active`, even the closed ones.** A `closed` account
> has no accessible mailbox, so the content import fails on it. Provision `active`,
> import, then apply the real `zimbraAccountStatus` as the very last step.
### Migrate password hashes (so nobody changes their password)
Optional — you can instead set fresh passwords and force a change. If you want to keep
existing passwords, migrate the hashes:
> **Trap 4 — `zmprov ga user userPassword` returns the literal string `VALUE-BLOCKED`,
> not the hash.** Extract with `ldapsearch` and write with `ldapmodify`; never with
> `zmprov`. Setting passwords from a VALUE-BLOCKED extraction locks out every user.
On OLD, get the LDAP creds and pull the hash (base64):
```bash
source /opt/zimbra/bin/zmshutil; zmsetvars # do NOT run under `set -u`; zmshutil uses $1
ldapsearch -x -LLL -o ldif-wrap=no -H "$ldap_url" -D "$zimbra_ldap_userdn" \
-w "$zimbra_ldap_password" -b '' \
"(zimbraMailDeliveryAddress=user@example.com)" userPassword
# -> userPassword:: <base64 of {SSHA512}...>
```
On NEW, find the account DN and write the same base64 verbatim:
```bash
source /opt/zimbra/bin/zmshutil; zmsetvars
DN=$(ldapsearch -x -LLL -H "$ldap_url" -D "$zimbra_ldap_userdn" -w "$zimbra_ldap_password" \
-b '' "(zimbraMailDeliveryAddress=user@example.com)" dn | sed -n 's/^dn: //p')
cat > /tmp/pw.ldif <<EOF
dn: $DN
changetype: modify
replace: userPassword
userPassword:: <base64-from-OLD>
EOF
ldapmodify -x -H "$ldap_url" -D "$zimbra_ldap_userdn" -w "$zimbra_ldap_password" -f /tmp/pw.ldif
```
`zmprov ma userPassword` may re-hash the value; `ldapmodify` writes the `{SSHA512}` bytes
verbatim, which is what you need. Shred the intermediate files afterward.
---
## Phase 4 — Migrate content over REST
Zimbra's per-mailbox export/import is HTTP, not a disk copy:
```bash
# Export on OLD:
zmmailbox -z -m user@example.com getRestURL '//?fmt=tgz' > user.tgz
# Import on NEW:
zmmailbox -z -m user@example.com postRestURL '//?fmt=tgz&resolve=skip' user.tgz
```
> **Trap 5 — import through mailboxd on :8443, not the nginx proxy on :443.**
> `postRestURL` uploads the archive, then waits for one response after mailboxd has
> processed the whole thing. Through nginx (443) the response is buffered, so the client
> sees >30s of silence and dies with `zclient.IO_ERROR (Read timed out)` on any large
> mailbox. `zmmailbox -t` does **not** help (it sets the SOAP timeout, not the REST
> socket timeout `socket_so_timeout`, default 30s). Point the import at mailboxd
> directly. In practice: set the domain's `zimbraPublicServiceHostname` to a name that
> resolves to NEW, `zimbraPublicServiceProtocol https`, `zimbraPublicServicePort 8443`
> for the duration of the import. (Export over 443 is fine — `getRestURL` streams bytes
> continuously, nothing to buffer.)
> **Trap 6 — raise message-size limits before importing large-attachment mail.** A fresh
> 10.1 `zimbraMailContentMaxSize` can be lower than the largest single message a mailbox
> holds, and oversized messages are dropped on import. Set
> `zimbraMtaMaxMessageSize` / `zimbraMailContentMaxSize` on NEW to at least the OLD
> server's MTA ceiling, then `zmmailboxdctl restart`.
**Minimize downtime with pre-seed + delta.** Import the bulk while OLD is still live
(no outage). At cutover, stop OLD's MTA, export only what arrived since the pre-seed
(`getRestURL '//?fmt=tgz&query=after:<date>'`) and import with `resolve=skip` (dedups).
The freeze is then minutes, not hours. Caveat: a `resolve=skip` delta adds new mail but
does not propagate read/unread, folder moves or deletions users made in the gap; minor
drift, acceptable for most.
### Verify by message count, not bytes
```bash
# ground truth from the export archive:
tar tzf user.tgz | grep -c '\.eml$'
# on NEW, count message-view folders:
zmmailbox -z -m user@example.com gaf </dev/null | awk '$2=="mess"||$2=="unkn"{s+=$4} END{print s}'
```
> **Trap 7 — count `mess` AND `unkn` view folders.** Quota-used differs across versions
> (dedup/blob handling), so bytes are useless for verification. Message counts are
> stable. But a folder whose default view was never set reports view `unkn` and still
> holds mail, so a `mess`-only count under-reports. Do not widen to all views — that
> pulls in Calendar/Contacts/Tasks/Briefcase items, which are not `.eml`.
---
## Phase 5 — Shares and mountpoints
Two halves. If `zimbraId` was preserved (Trap 2), the **grants** (folder ACLs) are inside
the mailbox tgz and restore themselves; a `zmmailbox ... mfg ...` replay returns
`mail.GRANT_EXISTS`, which is the success signal, not an error. The **mountpoints** (the
links in the grantee's mailbox) are the real work:
> **Trap 8 — mountpoints break because folder IDs are reassigned on import.** A mountpoint
> stores the owner's shared folder as a numeric **id**. A `.tgz` import reassigns ids for
> custom folders (standard folders keep fixed ids — Inbox 2, Calendar 10, etc.). So
> Calendar/Contacts shares survive, but every custom/Briefcase mountpoint that came in
> with the import points at the owner's OLD id, which on NEW is now nothing (folder shows
> crossed-out / unavailable) or a *different* folder (shows the wrong contents).
>
> Fix: recreate every mountpoint with `zmmailbox cm` **by owner path** — `cm` resolves the
> path to the current id at creation time. Delete the stale link first (this removes only
> the grantee's link; the owner's folder and data are untouched):
>
> ```bash
> zmmailbox -z -m grantee@example.com deleteFolder "/Local Name"
> zmmailbox -z -m grantee@example.com cm "/Local Name" owner@example.com "/Owners/Folder/Path"
> ```
>
> The owner's `gaf` output shows the mountpoint as `/Local Name (owner@domain:ID)`; look
> up that ID in the owner's `gaf` to get the current path, and create by path. Verify: the
> `(owner@domain:ID)` on the recreated mountpoint must equal the owner's *current* folder id.
> **Trap 9 — non-ASCII arguments need `LC_ALL=C.UTF-8`.** Zimbra's `.bash_profile` may
> force `LANG=C`/`LC_ALL=C`; Java then decodes non-ASCII command-line arguments as ASCII
> and stores them mangled. Folder names with a curly apostrophe come out as replacement
> characters. Prefix any `cm`/`mfg`/`zmprov -f` run that carries non-ASCII with
> `LC_ALL=C.UTF-8`. (Content import is immune — it reads the tgz as bytes; this only bites
> values passed as CLI arguments.)
Guest/public shares carry a password and cannot be reconstructed; recreate and
re-communicate by hand.
---
## Phase 6 — Filters, signatures, DKIM, certs
- **Filters (sieve):** an LDAP attribute, not mailbox content, so they do **not** ride in
with the tgz. Set `zimbraMailSieveScript` per account from your dump. For large
multi-line scripts, `LC_ALL=C.UTF-8 zmprov ma user@example.com zimbraMailSieveScript "$(cat script.sieve)"`
works (the whole file is one quoted argument; embedded quotes/newlines are literal data).
- **Signatures:** separate LDAP objects; recreate with `zmprov csig`, then repoint
`zimbraPrefDefaultSignatureId` (IDs are reassigned).
- **DKIM:** generate fresh keys on NEW per domain (`/opt/zimbra/libexec/zmdkimkeyutil -a -d
<domain>`) and publish the new selector TXT records. Leave the OLD selector published a
few days so in-flight mail still verifies.
- **Certificate:** issue a new cert for NEW covering the hostnames clients actually connect
to. `zmcertmgr` runs as `zimbra`, not root, on 10.1, and it wants the key + cert readable
by that user and the CA chain (intermediate + root) appended: `zmcertmgr verifycrt comm
key cert cachain` then `deploycrt comm cert cachain`, then `zmcontrol restart`.
---
## Phase 7 — Cutover
1. Stop OLD's MTA (`zmmtactl stop`); confirm `postqueue -p` is empty (drain if not). Mail
now queues at senders / your backup MX, nothing is lost.
2. Final delta export/import (Phase 4).
3. Point mail routing at NEW (DNS MX / A records, or your firewall's port-forward, whatever
your topology uses). Set each domain's `zimbraPublicServiceHostname` to the live client
name on port 443, and pin that name to NEW's own IP in NEW's `/etc/hosts` so the nightly
backup's `getRestURL` reaches itself (routers often don't hairpin the public IP).
4. Start NEW's MTA; apply real account statuses (close the closed ones); apply real quotas.
5. Verify outbound authentication with a tool like `check-auth@verifier.port25.com`, you
want **SPF pass, DKIM pass (new selector), DMARC pass**, and reverse-DNS/FCrDNS matching.
6. Keep OLD intact and powered (MTA down) for a reversion window (~5 days). Reversion =
NEW MTA down, export what arrived on NEW since cutover, import into OLD with
`resolve=skip`, routing back to OLD.
---
## What users lose (warn them)
REST export does not carry everything. Communicate these:
- **IMAP clients re-download every folder** (item IDs change, so UIDVALIDITY changes).
- **Mobile ActiveSync re-syncs from scratch** (partnerships don't migrate).
- **Dumpster/recover-deleted is not exported** — tell users to restore what they need first.
- **Saved searches** are not exported (recreate).
- **2FA and app-specific passwords** must be re-enrolled.
- **Only the latest Briefcase/document version** survives (versions aren't exported).
---
## The traps, in one list
1. The LDAP dump is COS-merged — sanitize to an allowlist; don't replay it whole.
2. Preserve `zimbraId` at account creation so shares/grants resolve.
3. Create accounts `active`; apply `closed`/real status only after import.
4. `zmprov` masks `userPassword` as `VALUE-BLOCKED`; migrate hashes via `ldapsearch` +
`ldapmodify`, not `zmprov`. (`set -u` also breaks sourcing `zmshutil`.)
5. Import via mailboxd `:8443`, not the nginx proxy `:443` (buffered response → 30s
socket timeout). `-t` doesn't fix it.
6. Raise `zimbraMailContentMaxSize` / `zimbraMtaMaxMessageSize` before importing
large-attachment mail, or messages get dropped.
7. Verify by counting `mess`+`unkn` view folders, not bytes.
8. Mountpoints reference folder IDs that get reassigned on import — recreate `cm` by owner
path (standard folders like Calendar keep fixed IDs and survive).
9. `LC_ALL=C.UTF-8` for any non-ASCII CLI argument, or Java mangles it.
10. Filters and signatures are LDAP objects — they don't ride in with the mailbox; restore
them separately.
11. DKIM keys and certs are fresh on the new box; publish new selectors, issue a new cert.
12. Pre-seed the bulk import while OLD is live, sync only a small delta during the freeze.
---
*Written up from a real 8.8.15 → 10.1.20 FOSS migration. Rehearse against real data before
the live run; the traps above are the ones that only surface with production mailboxes.*
- L. Mark Stone
- Ambassador

- Posts: 2940
- Joined: Wed Oct 09, 2013 11:35 am
- Location: Portland, Maine, US
- ZCS/ZD Version: 10.1.17 Network Edition
- Contact:
Re: Migrate FOSS 8.8.15 to fresh new Zimbra 10 Network Edition,
These notes are very helpful, but for less technical Zimbra admins, we developed Zimbra-to-Zimbra migration software. https://www.missioncriticalemail.com/zi ... made-easy/
Similar methodologies, but we use IMAPSYNC to avoid bringing over corrupted emails, and/or older emails that contain malware. Our GitHub site also offers some pre-migration scripts for doing things like cleaning up broken shares ahead of time.
Hope that helps,
Mark
Similar methodologies, but we use IMAPSYNC to avoid bringing over corrupted emails, and/or older emails that contain malware. Our GitHub site also offers some pre-migration scripts for doing things like cleaning up broken shares ahead of time.
Hope that helps,
Mark
___________________________________
L. Mark Stone
Mission Critical Email - Zimbra VAR/BSP/Training Partner https://www.missioncriticalemail.com/
AWS Certified Solutions Architect-Associate
L. Mark Stone
Mission Critical Email - Zimbra VAR/BSP/Training Partner https://www.missioncriticalemail.com/
AWS Certified Solutions Architect-Associate