Manual pages

shardpostctl-migrate(1)

Copy an account from another IMAP server, keeping its identities.

SHARDPOSTCTL-MIGRATE(1) ShardPost Manual SHARDPOSTCTL-MIGRATE(1)

NAME

shardpostctl-migrate - copy an account from another IMAP server, keeping its identities

SYNOPSIS

shardpostctl migrate -source-addr host:port -password-file file [-master-user name] [-sieve-addr host:port] [options] account
shardpostctl migrate [options] -source-user name -source-namespace prefix [-target-prefix name/] [-readers account,...] '~public'

DESCRIPTION

Reads an account from another IMAP server and writes it into ShardPost through the import API, keeping what clients care about: UIDVALIDITY, the UIDs with their gaps, UIDNEXT, flags and keywords, INTERNALDATE and SAVEDATE, and the MODSEQ of every message. Clients that were synchronized before the move therefore only fetch what is new, instead of downloading the whole account again. IMAP APPEND cannot do this, which is why imapsync and similar tools always produce new UIDs.

Every run is repeatable. The migrator reads the source, compares it with the target's inventory and sends only the differences, so you can run it during normal operation until the last delta is small. At the end it checks each mailbox against the source it read and reports it as verified.

Mailboxes that the source no longer has are removed from the target (-no-prune keeps them; INBOX always stays). Sieve scripts are copied when -sieve-addr is given, except jmap-vacation: a ShardPost source lists it, but it is the server's copy of the JMAP vacation response, and the target refuses it.

While a run is in progress, the account is locked: IMAP, POP3 and LMTP refuse it. Role: import.

The full-text index

Before it copies messages the migrator tells the target how many messages the source's mailboxes hold (STATUS MESSAGES of each); a fresh layout 3 full-text index (see shardpostctl-fts(1)) is sized for them. After every batch it asks the target to check the index: when its objects outgrew fts.shard_target_keys, the target reshards it under the import session, since a fts_layout job would wait for the import to end. The target works up to fts.import_reshard_sync_seconds (60) on the request and then goes on in the background; the migrator asks every five seconds until the reshard ended. Batches sent while it runs are answered 409 and are sent again afterwards. A reshard that fails is a warning; the import goes on and the fts_due worker reshards later. A layout 2 index is never resharded. Run the migrator with shardpostctl -timeout 10m so that the synchronous part of a reshard fits into one request; the default request timeout of two minutes suffices for it, but not for large batches.

Public folders

The second form imports a namespace of the source other than the personal one — a public namespace of Dovecot, as a rule — into the public folders. The account is ~public (quote it: a shell expands the tilde), -source-user is the source account whose view is read, and -source-namespace is the prefix of the namespace there. The folders land below -target-prefix; the namespace's own mailbox, if it has one (Zabbix of the namespace Zabbix/), takes the name of the prefix. The identities are kept exactly as for an account.

What differs from an account:

  • Rights are not copied. A public namespace of Dovecot takes its rights from the configuration and from groups, which have no counterpart here. Grant them with shardpostctl-acl(1); an imported folder without an entry is visible to nobody.
  • \Seen is private per account. The messages carry the \Seen of the source user, and that is what every account without a state of its own starts from. For each account named with -readers the migrator logs in at the source as that account and records where its \Seen differs, and its subscriptions of these folders. A folder the account does not see at the source is left alone. The source user itself needs no entry for \Seen, only for its subscriptions.
  • Pruning stays below the target prefix. Public folders outside it are never removed, the folder the prefix itself names stays, and without a prefix nothing is pruned.
  • No Sieve scripts. They belong to accounts.

While such a run is in progress the public namespace is locked: a client gets NO [UNAVAILABLE] for LIST and for every public folder, a session that has a public folder selected is closed within a few seconds, and a delivery that files into a public folder is deferred. The first copy of a large namespace therefore belongs into a quiet hour.

Import into folders nobody uses here yet. A later run makes the target what the source is: a reader's \Seen becomes exactly the source's, so what that account read here in the meantime is unread again, and the reader marks of an earlier run have moved HIGHESTMODSEQ past the source, so a client that synchronized such a folder here between two runs can miss flag changes of the second. Grant the rights after the last run, or keep clients away until then.

OPTIONS

Source

-source-addr host:port
Source IMAP server. Required.
-source-tls implicit|starttls|none
How to secure the connection. Default: implicit.
-source-server-name name
Certificate name to expect. Default: the host of -source-addr.
-source-ca file
CA for the source and for ManageSieve. Default: the system roots.
-source-proxy
Send a PROXY v1 header to the source, for load balancers that require it.
-source-timeout duration
Timeout per source command. Default: 2m.

Login

-source-user name
Login name at the source. Default: the account. Required for ~public.
-master-user name
Master user of the source. Without it, the migrator logs in as the user.
-user-format format
How the master login is composed. Default: {user}*{master}.
-password-file file
File holding the password of the (master) user. Required.

Sieve

-sieve-addr host:port
ManageSieve of the source. Without it, Sieve is not copied.
-sieve-tls implicit|starttls|none
Default: starttls.
-sieve-server-name name
Certificate name to expect for ManageSieve.
-sieve-proxy
Send a PROXY v1 header to ManageSieve.

Public folders

-source-namespace prefix
Import this namespace of the source instead of the personal one, for example Mailinglists/. The prefix is the one the source reports in NAMESPACE.
-target-prefix name/
Put this in front of the imported names. Empty, or a mailbox name ending in a slash.
-readers account,...
Accounts whose own \Seen and subscriptions are carried over. Only with the account ~public. Each of them is logged in at the source under its account name, with the same master login; an account that is called something else there cannot be a reader. A folder the account can list but not open is skipped with a warning. A client with a scope needs the readers in it, not only ~public.

Run

-session id
Resume this import session instead of opening a new one.
-no-prune
Keep target mailboxes the source no longer has.
-no-acl
Do not copy mailbox ACLs. A namespace import never copies them.
-reimport
Replace target mailboxes whose UIDVALIDITY no longer matches the source. Their messages are imported again.
-pop3-uidl-format format
The source's pop3_uidl_format (Dovecot syntax), so POP3 clients keep their UIDLs. Without it, POP3 reports the message GUID.
-batch-messages n
Messages per import request. Default: 200.
-batch-bytes n
Bytes per import request; a batch ends at whichever limit comes first, this one or -batch-messages. Default: 33554432 (32 MiB). The server has limits of its own (admin.import.max_batch_bytes, admin.import.max_batch_messages) and refuses a larger batch.
--json
Print the report as JSON.

ENVIRONMENT

Every flag of the source and of ManageSieve reads its default from the environment, which is how the import sidecar is configured: the deployment sets the variables once and the command is called without flags. A flag on the command line wins.

SHARDPOST_MIGRATE_SOURCE_ADDR
Default of -source-addr.
SHARDPOST_MIGRATE_SOURCE_TLS
Default of -source-tls.
SHARDPOST_MIGRATE_SOURCE_SERVER_NAME
Default of -source-server-name.
SHARDPOST_MIGRATE_SOURCE_CA
Default of -source-ca.
SHARDPOST_MIGRATE_SOURCE_PROXY
Default of -source-proxy.
SHARDPOST_MIGRATE_MASTER_USER
Default of -master-user.
SHARDPOST_MIGRATE_USER_FORMAT
Default of -user-format.
SHARDPOST_MIGRATE_PASSWORD_FILE
Default of -password-file.
SHARDPOST_MIGRATE_SIEVE_ADDR
Default of -sieve-addr.
SHARDPOST_MIGRATE_SIEVE_TLS
Default of -sieve-tls.
SHARDPOST_MIGRATE_SIEVE_SERVER_NAME
Default of -sieve-server-name.
SHARDPOST_MIGRATE_SIEVE_PROXY
Default of -sieve-proxy.
SHARDPOST_MIGRATE_POP3_UIDL_FORMAT
Default of -pop3-uidl-format.

The two proxy variables are read as the literal string true; anything else counts as off.

The connection to this server is configured as for every other command, see shardpostctl(1).

EXAMPLES

A first copy while the account still lives on the old server. Repeat it until a run reports few changes:

shardpostctl -timeout 10m migrate \

-source-addr mail.example.org:993 \
-master-user admin -password-file /run/secrets/master \
-sieve-addr mail.example.org:4190 \
user@example.org

In the cluster the sidecar already carries the settings:

kubectl -n shardpost exec deploy/shardpost-server -c import -- \

shardpostctl -timeout 10m migrate user@example.org

A long run, detached, with its log on the work volume:

kubectl -n shardpost exec deploy/shardpost-server -c import -- sh -c \

'nohup shardpostctl -timeout 10m migrate --json user@example.org \
>/work/user.json 2>/work/user.log &'

Keep POP3 UIDLs stable for a Dovecot 2.3 source:

shardpostctl migrate -pop3-uidl-format '%08Xu%08Xv' \

-source-addr mail.example.org:993 \
-master-user admin -password-file /run/secrets/master \
user@example.org

The public namespace Mailinglists/ of a Dovecot server, as lists@example.org sees it, into Public/Mailinglists/..., with the reading state of a second account; then the rights, which the import does not copy:

shardpostctl -timeout 10m migrate \

-source-user lists@example.org \
-source-namespace Mailinglists/ -target-prefix Mailinglists/ \
-readers lists@example.org,user@example.org '~public' shardpostctl acl set '~public' Mailinglists/Dovecot \
user@example.org lrswipkxte

A namespace that is a single mailbox (Zabbix/ with the mailbox Zabbix) becomes the folder the prefix names:

shardpostctl migrate -source-user lists@example.org \

-source-namespace Zabbix/ -target-prefix Zabbix/ '~public'

The cutover, once the copy is close: hold the account, kick its sessions, run the last delta, then move it.

nauthilus-directorctl users hold set user@example.org --duration 15m \

--reason mailbox-move nauthilus-directorctl users kick user@example.org --reason mailbox-move shardpostctl migrate user@example.org # all mailboxes verified nauthilus-directorctl users move user@example.org \
--to-shard shardpost --strategy kick_existing nauthilus-directorctl users hold clear user@example.org --reason done

EXIT STATUS

0
Everything was copied and verified.
1
Parts failed. Run the command again; it continues where it left off.
64
Usage error, for example a missing -source-addr or -password-file, or options of a namespace import that do not fit together.
69
The run was aborted.

SEE ALSO

shardpostctl(1), shardpostctl-export(1), shardpostctl-acl(1), shardpostctl-account(1)

The import semantics and the migration procedure are described in docs/admin-api.md.

2026-10-02 ShardPost 0.45.0

Rendered from docs/man/man1/shardpostctl-migrate.1 in the ShardPost repository.