EmailEngine stores a refresh token for every OAuth mailbox it serves. When you move to another email API, the expensive part is rarely the code. It is asking thousands of users to connect their mailbox again, and losing the ones who never do. Whether you have to comes down to two things you already control: the refresh tokens and the OAuth app that issued them.
A refresh token belongs to an app.
Microsoft documents that refresh tokens are bound to a combination of user and client (Refresh tokens in the Microsoft identity platform). Google’s token endpoint takes the client id and client secret with every refresh (Refresh an access token). A token issued to your app is of no use to anyone else’s.
So a move that keeps users signed in needs two things:
- The same OAuth app. Your Google or Microsoft client id and secret move with the tokens and become a workspace OAuth app in workwae. A Microsoft app keeps its cloud, so GCC High and DoD grants keep working.
- The stored grant. The refresh token, the granted scopes and, while it is still valid, the current access token. An expired access token is left behind; the first refresh replaces it.
A provider that connects mailboxes only through its own app cannot do this: no token
from your app refreshes against theirs, so every user consents again. To connect new
mailboxes through your app after the move, add workwae’s callback,
https://api.workwae.com/connect/callback/microsoft (or /google),
to its redirect URIs (Connect flows).
Why the import reads Redis.
Going through EmailEngine’s API would sign every account in again, so our team reads
EmailEngine 2.x’s Redis directly and only reads it. EmailEngine encrypts stored
credentials with its EENGINE_SECRET. You share that secret and Redis read
access through a one-time encrypted link, and we delete both after the import. A secret
that cannot decrypt the credentials stops the run before anything is written. Nothing in
EmailEngine changes, so it keeps serving your app until you switch
(Migrate from EmailEngine).
What moves.
- OAuth2 accounts in API mode become Gmail or Microsoft Graph accounts with their refresh token and granted scopes.
- OAuth2 accounts in IMAP mode become IMAP accounts that sign in with XOAUTH2 on the provider’s servers.
- Password accounts become IMAP accounts; EmailEngine’s
copybecomesappendSent. - Gmail service account apps become Google domain-wide delegation connections with the same key and scopes, and Outlook application apps become Microsoft app-only connections for the same directory (Organization connections). They arrive unverified until a test call checks them.
- Shared mailboxes opened through another account’s grant move when
that grant has
Mail.ReadWrite.Shared. - Stored templates move too: public templates become shared templates, and account templates follow their account.
Calendar and contacts sync turn on only when the grant already covers them, since a grant
asked for mail alone cannot serve them. Each imported account keeps its EmailEngine
account id as its externalRef, which is how you map your own records.
What stays behind.
Message ids are new, so anything you store by EmailEngine message id needs new keys from each account’s first sync. These settings are not carried over, and the plan and report name them per account:
- Folder
pathand folder overrides - Webhooks and
notifyFrom; add endpoints in workwae and verify deliveries with the SDK’sparseWebhook - Proxy and TLS options
- Locale and time zone
A dry run, then apply.
The first pass writes nothing. It lists, per account, what imports, what needs a reconnect and what stays behind, and we go through it with you:
# The plan we review with you (example)
Accounts 40 found
37 import with their existing grants
3 need a reconnect through hosted connect
OAuth apps 2 become workspace OAuth apps
Templates 6 import
# Nothing is written until you approve the plan
You can move everything at once or named mailboxes first. On apply, each account is
written in its own transaction and announced with the same account.connected
event a normal sign-in emits, and a JSON report lists every outcome. A rerun skips
accounts and templates already imported, matched by EmailEngine id or by address and
provider. Accounts over your plan’s limit are reported, and the run goes on.
Your webhook handler can link each account to your own record as the events arrive:
import { parseWebhook } from "@workwae/sdk";
const body = await request.text();
const event = await parseWebhook(secret, request.headers, body);
if (event.known && event.type === "account.connected" && event.data.externalRef) {
// Imported accounts carry their EmailEngine account id
await mailboxes.link(event.data.externalRef, event.accountId);
}
// Or look one up later
const { items } = await workwae.accounts.list({ externalRef: emailEngineAccountId });
Each account is then checked on its first sync, which ends with
account.initialized. A grant revoked since EmailEngine last used it fails
that check: the account moves to reauth_required and you receive
account.reauth_required.
What needs a reconnect.
The dry run flags these, skips them, and leaves them for a sign-in afterward:
- Accounts whose OAuth app is gone or uses EmailEngine’s old settings-based configuration
- Credentials from EmailEngine’s authentication server
- IMAP logins with integrator-supplied access tokens
- Shared mailboxes opened with their own grant, and grants that only send
- Service account apps without a key file, and Mail.ru accounts
For those, and for any imported account that later needs a fresh grant, a hosted connect
session does the sign-in. Passing accountId re-authorizes an existing account
instead of creating another:
// A flagged mailbox: connect it fresh, keyed to its EmailEngine id
const fresh = await workwae.connectSessions.create({
redirectUri: "https://app.example.com/mail/connected",
providers: ["google", "microsoft", "imap"],
loginHint: "ada@example.com",
externalRef: emailEngineAccountId,
});
// An imported account in reauth_required: sign in again through your app
const again = await workwae.connectSessions.create({
accountId,
redirectUri: "https://app.example.com/mail/connected",
providers: ["microsoft"],
oauthAppIds: { microsoft: oauthAppId },
});
// Redirect the browser to fresh.url or again.url When to stay on EmailEngine.
workwae is hosted only, on AWS in the United States. Some teams are better off where they are:
- You must self-host. EmailEngine runs on your servers with your Redis, and you choose where it runs and processes mailbox data (EmailEngine security). workwae has no self-hosted edition.
- Mailbox data must stay in your environment. EmailEngine keeps metadata in your Redis and fetches bodies on demand. workwae indexes messages as they sync, on its own infrastructure in the United States.
- You need a compliance report from your vendor today. workwae’s SOC 2 is planned, not started. EmailEngine has none of its own either; it inherits your environment’s.
- You want a published price. EmailEngine lists $1,450 or €1,200 per year for unlimited mailboxes (emailengine.app); workwae sets pricing with each early-access team.
- You need an MCP server now. EmailEngine has one in beta; workwae’s is coming.
If you want calendars, contacts and meetings, a webhook delivery log with replays, or no Redis and workers to run, the EmailEngine alternative page sets the two side by side, and how workwae compares covers hosted APIs. The quickstart shows the SDK your migrated accounts answer through.