Migrating from cPanel
The generic engine (Migration) copies mail from any IMAP server. This is what cPanel adds on top: the old server tells us who exists, how big they are, what forwards where, and what filters they have — so a migration starts from a list rather than a spreadsheet.
Most customers moving to this product are moving off cPanel, which is why it is the first connector after generic IMAP.
What the API token can and cannot do
Section titled “What the API token can and cannot do”This is the whole shape of the feature, so it is worth being blunt about it.
| Can | List mailboxes, their sizes and quotas; list domains; list forwarders, autoresponders and filters. |
| Cannot | Read a single message. Recover a mailbox password — cPanel stores them hashed. |
So a cPanel migration uses two credentials for two jobs: the API token, which discovers, and
whatever opens IMAP, which copies. They are stored together and sealed together but they are not interchangeable,
and a job with only a token will discover forty mailboxes and then fail all forty on auth.
Nothing here writes to the source. cPanel exposes password changes and forwarder deletion through the same API; not wiring those up is deliberate, and is what keeps “a failed migration cannot have damaged the customer’s old mail” true of this connector too.
Getting the credentials
Section titled “Getting the credentials”The API token
Section titled “The API token”Two kinds, and which one you can get depends on who you are.
- cPanel account token — the customer makes it themselves in cPanel → Manage API Tokens. Port 2083. Scoped to their one account, which is all a single migration needs.
- WHM token — the host or reseller makes it in WHM → Manage API Tokens. Port 2087. Reaches
every account on the box, so the migration also names which
accountto read. Leave the account blank and the connection test comes back listing what it found.
Use a token, never the account password. Tokens are revocable, and this one is going to sit in our database until the cutover finishes.
Opening the mailboxes
Section titled “Opening the mailboxes”In descending order of how much you will enjoy it:
- A Dovecot master user, if the old host’s root has configured one. One password reads every mailbox and the migration needs nothing else. cPanel does not ship one, so this only exists if somebody deliberately added it.
- Each mailbox’s own password, entered when mapping. Fine for five mailboxes, tedious for fifty.
- Reset the passwords on the source through cPanel and use the new ones. We do not do this for you — see Why we do not reset passwords.
The IMAP side is always 993 with TLS. The job’s source can carry another IMAP port and encryption, but the New migration page always sends 993/TLS for a cPanel job, so a host that serves IMAP only on 143 with STARTTLS cannot be set up from the admin UI.
Running one
Section titled “Running one”- New migration → cPanel / WHM. Hostname, port, the token and its username, and Mail server, if it is not
the same machine when IMAP is served from another host. Test proves the
token by listing the account; you do not need a mailbox address for that. Give one as well and the
test also tries IMAP: with a master user, or that mailbox’s password (asked for when there is no
master user, never stored), it opens the mailbox; with neither it only checks the server answers
(see
migration.md→ How a migration runs). - Add the domains on the Domains page for any that do not exist here yet. The migration will not create them (see below), and mailboxes on a missing domain are skipped.
- Read the source on the job’s Discover step. You get every mailbox with its size, quota and whether it is suspended, plus counts of forwarders, autoresponders and filters, plus any warnings.
- Create and map the missing mailboxes, optionally carrying each quota across (an old cPanel that only answers
list_popsreports no quotas, and each mailbox then takes the domain default). The quota is applied when the migration ends, not at creation (migration.md→ Quotas). A suspended source account is created as an ordinary active mailbox; switch on Suspended afterwards if needed. The generated passwords are shown once. Copy them then — there is no way to get them back. - Import the forwarders, autoresponders and filters.
- From here it is an ordinary migration: Migrating a domain and cutting over.
How each thing is translated
Section titled “How each thing is translated”Mailboxes
Section titled “Mailboxes”Email::list_pops_with_disk gives address, bytes used, quota and suspension. Old cPanel builds only
have list_pops, in which case sizes and quotas come back empty and you are told so.
Quotas: unlimited — and a quota of 0, which some cPanel versions write for the same thing — is never copied as a zero-byte allowance, nor as unlimited: the mailbox takes the domain default, like one whose source reports nothing. The cPanel account’s own row (the one with
no @) is not a mailbox and is skipped.
A migrated mailbox gets a new password, necessarily. Plan how you are handing those out before you start; it is the part of a cPanel migration that annoys users, not the mail copying.
Forwarders → aliases or forwarders
Section titled “Forwarders → aliases or forwarders”cPanel has one concept where this server has two, and the difference is whether the forwarding address is also a real mailbox:
| On cPanel | Here |
|---|---|
| A forwarder on an address that is a mailbox | A forwarder with keep a copy — mail is delivered here and sent on, exactly as cPanel does it |
| A forwarder on an address that is not a mailbox | An alias, with every destination for that address |
This is decided by looking the address up locally, not by guessing from the data, because getting it backwards either duplicates every message or loses it.
cPanel’s own field naming is confusing here (forward is the address that receives, dest is where
the copy goes). If a forwarder comes back with those reversed, it is corrected against the account’s
own domain list — the receiving side must be a domain the account holds — and you get a warning saying
so.
Autoresponders → Sieve vacation
Section titled “Autoresponders → Sieve vacation”Subject, body and interval come across; cPanel counts the interval in hours and Sieve in days, so it is converted and clamped to 1–30.
Two things to know:
- A responder whose window has already closed is imported switched off. Silently reactivating an out-of-date away message on a server the sender has just started using is not a helpful default.
- cPanel expands
%subject%,%from%and friends when it sends; Sieve does not. Those come across literally, and the discovery warning says how many responders use them — not which. Edit them after importing.
Filters → filter rules
Section titled “Filters → filter rules”cPanel’s filters are structured, so most of them translate. The rule is refuse rather than approximate: if any part of a filter cannot be expressed exactly, the whole filter is skipped and listed with the reason, rather than imported in a slightly different form. A filter that quietly misfiles mail for a month is worse than one you were told to recreate.
Translated:
| cPanel | Here |
|---|---|
contains, does not contain, equals, is not |
the same operators |
begins with / ends with |
a matches glob, with the literal escaped |
does not begin / does not end |
the negated glob |
| From/To/Cc/Sender/Reply-To | an address test |
| Subject and other headers | a header test |
| Body contains / does not contain | a body test |
is above / is below on size |
a size test |
| Deliver to folder | fileinto, with INBOX.Lists and mail/domain/user/.Lists both normalised to Lists |
| Redirect to an address | redirect |
| Discard | discard |
| Stop processing rules | the rule’s stop flag |
Refused, with the reason reported:
- Regex matching. Sieve’s
:matchesis glob, and:regexis an optional extension Dovecot does not enable. Translating one into the other would change what the filter matches. - Spam rules (
$spam_score,$spam_bar,$spam_status). cPanel scores with SpamAssassin; this server scores with Rspamd and writes different headers. A translated rule would compile cleanly and then never match anything. Recreate it againstX-Spam. - A mixture of “and” and “or” between rules. cPanel joins each rule to the next individually; our rules have one combinator for the whole filter.
- “Any header” as a single test, pipe to a program, and fail with a message — no equivalents.
A filter with more than 20 conditions or 10 actions, the limits of one rule here, is refused like the rest, and the note names the count. Cutting an “or” of 21 senders to fit would silently stop filing mail from the 21st. Until 2026-09-26 cPanel was the one translator that imported it truncated.
Where the admin reads these notes: the result under Import on the migration job’s discovery panel.
A refused filter is under n not done, as filter on <address> with its reason. A filter imported with a loss is
under its own list, n filters imported with something left out, as filter "<name>" on <address> with the
note. It is kept apart because that rule is running. The API returns these as skipped and imperfect in
MigrationImportResult. The discovery list itself only counts filters.
Filters cost one API call per mailbox plus one per filter — Email::list_filters to get the
names, then an Email::get_filter for each one, because cPanel’s list carries no rule detail. So the
cost is not two per mailbox but unbounded in the number of filters a mailbox happens to have, which
is why discovery reads filters for the first 200 mailboxes only and tells you when it stopped. A
mailbox with no filters costs the one call and no more.
Why domains are not created
Section titled “Why domains are not created”Pre-create makes mailboxes, never domains. Adding a domain rotates a DKIM key, publishes DNS records and consumes licence entitlement — it belongs on the Domains page, done once and deliberately, not as a side effect of a migration that was asked to copy some mail. Mailboxes on a domain this server does not have are skipped with a reason naming the domain.
Mailbox creation itself goes through the same licence check as everywhere else. A migration is not a way around the licence.
Why we do not reset passwords for you
Section titled “Why we do not reset passwords for you”cPanel’s API can set a mailbox password. Doing that would make a fifty-mailbox migration a single click: rotate every password, copy with the new ones, done.
We do not, because it breaks the promise the rest of the feature rests on — that nothing on the source is modified, so a migration can be re-run, abandoned or backed out of with no consequences. Rotating passwords locks out the customer’s users on a server they are still relying on, hours before the cutover, and cannot be undone if you then decide to postpone.
If you want it anyway, do it in cPanel deliberately, then enter the new passwords when mapping.
Errors specific to this connector
Section titled “Errors specific to this connector”| Code | Means |
|---|---|
source_auth (400) |
The API token was rejected, or a WHM token has no access to that account. cPanel reports a refusal at HTTP 200, so its error text is read, and it becomes source_auth only when it says so — permission, access denied, not authorised or privilege (WHM’s own metadata error is not tested for privilege; the cPanel result inside it is). |
source_tls (400) |
The control panel’s certificate did not verify. cPanel’s own 2083 is self-signed on plenty of boxes; re-create the migration with Accept an invalid TLS certificate if you recognise the host. |
source_unreachable (502) |
DNS, firewall or the wrong port. 2083 for a cPanel token, 2087 for WHM. |
source_rate_limited (429) |
The panel is throttling the API. Wait. |
source_error (502) |
Anything else, with the panel’s own words. An HTML login page at HTTP 200 means the wrong port, and says so. A bare status: 0 UAPI reply with no error text lands here too — nothing in it says the token was the problem, though it often is. |
The per-mailbox copy errors are the ordinary ones: When a migration fails.
Where it lives
Section titled “Where it lives”| Piece | Where |
|---|---|
| Schemas | packages/core/src/migration.ts (CpanelSourceConfig, CpanelSourceSecret) |
| HTTP client and envelopes | packages/migration/src/connectors/cpanel-api.ts |
| Filter translation (pure) | packages/migration/src/connectors/cpanel-filters.ts |
| The connector | packages/migration/src/connectors/cpanel.ts |
| Pre-create and import | apps/api/src/services/migration-import.ts |
| Routes | POST /migrations/:id/{discover,precreate,import} |
| UI | apps/admin/src/routes/migration.tsx (SourceDiscovery) |