Part of the PurelyManage series.

The migration system in PurelyManage wraps imapsync and adds a job queue, a UI, and a few quality-of-life features on top. This post covers the job system from the user side: how jobs are created, run, monitored, and managed.

The imapsync installation and the async job architecture under the hood are covered in the next post.

Server Configuration

Before running any migration you need to add IMAP server definitions. A server definition stores only the connection details: host, port, and whether to use SSL. Credentials are entered per-job, not per-server, so you can reuse the same server config across many migrations without re-entering connection details each time.

Add servers once in the Migration page under the Servers tab. The panel includes a connection test button that performs a live IMAP LOGIN check against the server to confirm it is reachable before you run jobs against it.

Single Job

A single migration job takes:

  • Source server (from the configured list)
  • Source email and password
  • Destination server
  • Destination email and password
  • Job name
  • Dry run toggle (on by default)

Submitting the form encrypts both passwords immediately and inserts a job row. The job enters the queue and starts as soon as a slot is available.

Dry Run

Every job can be run in dry run mode, which passes the --dry flag to imapsync. A dry run connects to both IMAP servers, calculates what would be transferred, and reports the mailbox sizes and message counts without copying anything. It is the right first step before a real migration: confirm the connection works, check the volume, and catch any obvious issues.

Dry run is the default for new jobs and bulk imports. After a dry run completes you can click “Run Real” on that job to launch the actual migration as a new job, reusing the same credentials.

Bulk CSV Import

For migrating many accounts at once, the bulk import accepts a CSV file with one row per mailbox. The expected columns are:

sourceServerId,sourceEmail,sourcePassword,destServerId,destEmail,destPassword

A CSV template with the correct headers can be downloaded from the bulk import modal. After uploading, the panel shows a row count preview before you confirm. All jobs in a bulk import share a batch tag, which you can use to filter the jobs list to a specific migration batch.

Bulk imports default to dry run. The typical workflow is: import CSV as dry run, review the results, then use “Bulk Rerun” to launch all of them as real jobs once you are satisfied.

The Concurrency Queue

imapsync is not lightweight. Each job opens IMAP connections on both sides, downloads messages, and uploads them. Running too many in parallel stresses both the source server and the destination server.

The queue limits concurrent jobs to two at a time:

const MAX_CONCURRENT = 2

function processQueue() {
  while (runningJobs.size < MAX_CONCURRENT && jobQueue.length > 0) {
    const nextId = jobQueue.shift()!
    runJob(nextId)
  }
}

Jobs are queued in FIFO order. When a running job finishes (success, failure, or cancel), processQueue() fires automatically and the next queued job starts. You can submit dozens of jobs at once and they will work through the queue two at a time without any manual intervention.

Job Operations

Cancel

Cancelling a running job sends SIGTERM to the imapsync process and marks the job as cancelled. Cancelling a queued job removes it from the in-memory queue before it starts. Either way, the job row stays in the database for reference.

Hard delete (?hard=true) removes the job row entirely from the database. Use this for cleanup after a batch completes and you no longer need the history.

Rerun

Any completed, failed, cancelled, or interrupted job can be rerun. Rerunning creates a new job row copying the credentials from the original. You can override the dry run flag at rerun time: use “Run Real” to promote a completed dry run to a real migration, or use “Dry Run” to retest a failed job before retrying.

Bulk rerun applies to a selected set of jobs: select multiple rows, pick an action from the bulk action bar, and all selected jobs are requeued as new jobs.

Edit

A job that is not running can be edited: source email, source password, destination email, and destination password can all be updated. Editing re-encrypts any changed password fields. Unchanged password fields stay as-is. The edit form shows a placeholder (unchanged) for existing passwords and only sends a new value if you type one.

Editing a running job is blocked. Wait for it to finish or cancel it first.

Monitoring Jobs

The jobs list auto-refreshes every 5 seconds. Status badges update in place: queued, running, done, failed, cancelled, interrupted.

Opening a running job shows the live imapsync output. The log viewer polls GET /migration/jobs/:id/tail?offset=N every 1.5 seconds, requesting only the new chunk since the last poll:

GET /migration/jobs/:id/tail?offset=1024
→ { status: "running", chunk: "...", newOffset: 2048 }

This avoids re-fetching the full log on every poll. The offset tracks how much of the log the frontend has already seen.

Completed job logs can be downloaded as a plain text file. The download streams the full imapsync stdout/stderr from the database as a .log file, useful for keeping a record of what was transferred.

Email Notifications

If you are not watching the jobs list, PurelyManage emails you when a job finishes or fails. The notification is suppressed if you have polled the jobs list within the last 60 seconds, on the assumption that you are actively watching. SMTP must be configured in the backend .env for notifications to work.


The next post covers the imapsync setup and the async architecture that makes jobs survive UI disconnects.