API

Mail

Set up and operate a self-hosted mail server.

The Mail API drives Openship's self-hosted email feature: it installs an iRedMail stack on one of your servers over SSH, tracks the wizard's progress, then exposes an admin panel for the running server — domains, mailboxes, stats, backups, DNS health, and per-component control — plus a one-click webmail deploy. In the dashboard this is the Emails page.

Self-hosted only

Every route here is gated by localOnly and is never mounted in Openship Cloud — the module is dynamically imported only on self-hosted instances. On a cloud instance these paths return 404.

Every route is server-scoped

Most paths carry a :serverId (or take one in the body / ?serverId=). Openship checks that the server belongs to your organization on every call — a mail_server grants SSH-level reach into the box, so a serverId outside your org returns 404, indistinguishable from one that doesn't exist.

Start (or resume) setup

Kicks off the iRedMail install wizard against a server and streams progress as Server-Sent Events. The wizard is idempotent: re-POST with startStep to resume from where a previous run stopped (or was gated). Only one install may run per instance at a time.

POST /api/mail/setup

Prop

Type

config is an optional IRedMailConfig:

Prop

Type

curl -N -X POST "$OPENSHIP_URL/api/mail/setup" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serverId":"srv_123","domain":"example.com"}'

The stream emits step_start, log, and step_done events per step, plus these control events:

  • dns_records / dns_pending — the DKIM step publishes the records you must add, then holds. Add the records at your DNS provider and call POST /api/mail/setup/dns-ack, then re-POST /setup with the resumeStep from the event.
  • ptr_pending — after DNS is acknowledged, the wizard holds again for reverse-DNS (PTR). Set the PTR record at your VPS provider, call POST /api/mail/setup/ptr-ack, and resume.
  • complete — carries domain, webmailUrl, and adminUrl.
  • error — carries message and (when resumable) resumeStep.

The terminal verify_reachability step reads the target's TCP listener table and then connects from the API host to the public-DNS address on 25, 465, 587, and 993. Listener failures and off-box failures remain distinct in the result. A measured failure blocks completion and points at both the host firewall and the provider-level firewall/security group/security list/NSG; an unavailable probe is returned as a non-blocking warning rather than being invented as an outage.

GET /api/mail/health/:serverId reuses one public-port sweep for 60 seconds. Pass ?refreshReachability=1 after changing firewall rules to force an immediate recheck; concurrent requests are still coalesced into one bounded four-port probe.

Acknowledge DNS / PTR

Both flip a single flag in the on-server state file so the wizard can move past its gate on the next /setup POST. Both take the same body.

POST /api/mail/setup/dns-ack
POST /api/mail/setup/ptr-ack

Prop

Type

The same { serverId } body applies to POST /api/mail/scan, POST /api/mail/adopt, POST /api/mail/setup/cancel, and POST /api/mail/setup/reset.

Adopt an existing install

If Openship lost its record of a mail server (rebuilt desktop, wiped DB), scan the server, then adopt it — no reinstall, nothing changes on the box.

POST /api/mail/scan
POST /api/mail/adopt
# 1. Probe for iRedMail + its on-server state
curl -X POST "$OPENSHIP_URL/api/mail/scan" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serverId":"srv_123"}'

# 2. Re-adopt it (idempotent)
curl -X POST "$OPENSHIP_URL/api/mail/adopt" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serverId":"srv_123"}'

Rotate the postmaster password

POST /api/mail/credentials/postmaster

Prop

Type

Add a mail domain

POST /api/mail/admin/:serverId/domains

Prop

Type

PATCH /api/mail/admin/:serverId/domains/:domain accepts the same fields (all optional) plus active (boolean).

curl -X POST "$OPENSHIP_URL/api/mail/admin/srv_123/domains" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com","defaultQuotaMB":2048}'

Create a mailbox

POST /api/mail/admin/:serverId/mailboxes

Prop

Type

PATCH /api/mail/admin/:serverId/mailboxes/:email accepts name, password, quotaMB, and active (all optional). DELETE soft-deletes by default; add ?hard=true to purge the maildir.

curl -X POST "$OPENSHIP_URL/api/mail/admin/srv_123/mailboxes" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"localPart":"alice","domain":"example.com","password":"a-strong-password"}'

Configure backups

Plugs the mail server into the general backup system — pick a destination and what to capture. Requires an existing backup destination in your organization.

POST /api/mail/admin/:serverId/backup-policy

Prop

Type

Send a test email

POST /api/mail/admin/:serverId/test-email

Prop

Type

Deploy webmail

Installs the webmail app from the app catalog, pointed at this mail server's IMAP/SMTP, then queues a deployment and starts it. Returns { deploymentId, projectId } so the dashboard can redirect to /build/[deploymentId]. Re-deploying an existing webmail reuses its project (and its stored sessions).

The IMAP/SMTP backend is derived from the mail server — mail.<domain> on 993/465 — so there is nothing to configure here. To point a webmail at a mailbox host you run elsewhere, install the webmail app from /apps (or use POST /api/mail/webmail/deploy-external) instead.

POST /api/mail/webmail/deploy-project

Prop

Type

target is one of:

  • { "kind": "self", "serverId": "srv_123" } — self-hosted on one of your Openship servers.
  • { "kind": "cloud", "serverId": "srv_managed" } — on a selected managed Openship Cloud server.

Both targets need a real server ID. Use GET /api/system/servers/destinations to list accessible servers. For a Cloud server that is not connected to this installation yet, use POST /api/system/servers/managed/connect and pass its returned local serverId here. The dashboard uses the same connection flow. A missing Cloud destination is refused before creating a webmail project. Redeploying an existing managed webmail preserves its server binding; moving it to another server requires the normal project migration flow.

curl -X POST "$OPENSHIP_URL/api/mail/webmail/deploy-project" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mailServerId":"srv_123","hostname":"mail.example.com","target":{"kind":"self","serverId":"srv_123"}}'

Reconnecting an existing webmail

Forgetting and re-adopting a mail server can leave its catalog webmail running with a missing control-plane link. mail.getStatus({ serverId }) returns webmailRecoveryCandidates for accessible, unlinked catalog apps on that mail server which already route its mail.<domain> hostname. Reading status grants no routing rights.

Choose the existing app in Emails → Deploy webmail, then press Reconnect and deploy. With the SDK, pass its projectId as reconnectProjectId to mail.deployWebmail(input); the CLI accepts the same input through mail webmail deploy reconnect.json. Keep the app's existing server and mail hostname for this recovery. It reuses the project, service, generated credentials and persistent volumes, and restores the link before queuing deployment. The mail certificate record stays owned by the mail service.

An omitted selection returns WEBMAIL_RECONNECT_REQUIRED when a recoverable app exists. WEBMAIL_LINK_CHANGED means another operation changed the link; refresh status and review the current app before retrying. A project from another organization, another mail server's linked app, or a different destination cannot be adopted through this path.

Replacing a pre-catalog webmail

Openship 0.6.1 and earlier shipped webmail as a prebuilt bundle rather than an image. Those installs can't be redeployed — they have to be replaced by the catalog app. GET /api/mail/status reports one as webmail.legacy: true, and a deploy over it is refused with 409 and "code": "LEGACY_WEBMAIL" until you retry the same request with replaceLegacy: true.

The replace tears the old project down — container, routes, host state directory, project row — and then installs the webmail app at the same hostname. Mailboxes, mail data and every message live on the mail server and are untouched; the old webmail's sign-in sessions and per-user settings are not, so everyone signs in again. If the old install can't be removed, nothing is installed and the existing webmail keeps running.

Errors

503 — MAIL_STATUS_UNAVAILABLE

GET /api/mail/status could not read the server's saved setup state. Preserve any last-known details and retry the read; this response does not mean mail is stopped or uninstalled. If the saved state was read but the live engine check failed, the response instead keeps that state, omits engine, and includes observationError. Treat engine health as unknown.

404 — Not available

You're calling a cloud instance. The Mail API is self-hosted only; run it on an instance where CLOUD_MODE is off.

404 — Server not found

The :serverId doesn't belong to your organization (or doesn't exist). List your mail servers with GET /api/mail/servers to get valid IDs.

409 — Setup already running

Only one install runs per instance at a time, and per-server writes (reset, postmaster rotation) are refused while its setup is active. Wait for it to finish, or cancel it with POST /api/mail/setup/cancel.

409 — LEGACY_WEBMAIL

The webmail linked to this mail server predates the catalog app, so it can only be replaced. Retry the same deploy-project call with replaceLegacy: true once you've confirmed the operator accepts losing the old webmail's sessions and per-user settings.

502 — Scan / adopt failed

Openship couldn't reach the server over SSH, or the probe threw. Check the server is online and its SSH credentials are valid, then retry.

Operations

SDK and CLI

The remote SDK exposes client.mail; its methods below cover setup, health and administration through the same server permissions as the dashboard. Certificate renewal uses the existing edge pipeline.

const certificate = await client.mail.getCertificate("srv_mail");
await client.mail.updateCertificate("srv_mail", { autoRenew: true });
const domains = await client.mail.listDomains("srv_mail");
const mailboxes = await client.mail.listMailboxes("srv_mail", { domain: "example.com" });

client.mail.setup(input) returns the existing event stream. Observe its completion or DNS/PTR checkpoint before continuing. The CLI supplies mail setup, mail domain, mail mailbox, mail alias, mail relay, mail certificate, mail backup, mail inbound, mail component and mail webmail. Use --schema for JSON configuration commands. Mail is a remote, self-hosted feature.

OperationSDKREST API
List all setup steps and the total count.mail.getSteps()
Remote client
GET /api/mail/steps
mail_server:read · Self-hosted
Current setup progress for a server (rendered from its on-server state file).mail.getStatus(input?)
Remote client
GET /api/mail/status
mail_server:read · Self-hosted
List every server Openship has provisioned (or is provisioning) mail on.mail.listServers()
Remote client
GET /api/mail/servers
mail_server:list · Self-hosted
Probe a server for an existing iRedMail install + its on-server state (read-only).mail.scan(input)
Remote client
POST /api/mail/scan
mail_server:write · Self-hosted
Re-adopt a scanned server whose orchestrator state was lost.mail.adopt(input)
Remote client
POST /api/mail/adopt
mail_server:write · Self-hosted
Cancel the running setup.mail.cancelSetup()
Remote client
POST /api/mail/setup/cancel
mail_server:write · Self-hosted
Mark DNS (DKIM) records as configured, releasing the DNS gate.mail.acknowledgeDns(input)
Remote client
POST /api/mail/setup/dns-ack
mail_server:write · Self-hosted
Mark reverse-DNS (PTR) as configured, releasing the PTR gate.mail.acknowledgePtr(input)
Remote client
POST /api/mail/setup/ptr-ack
mail_server:write · Self-hosted
Wipe the on-server state file (does not touch iRedMail).mail.resetSetup(input)
Remote client
POST /api/mail/setup/reset
mail_server:admin · Self-hosted
Rotate the postmaster account password.mail.setPostmasterPassword(input)
Remote client
POST /api/mail/credentials/postmaster
mail_server:write · Self-hosted
List deploy-target options (self-hosted servers / cloud).mail.getWebmailTargets(input)
Remote client
GET /api/mail/webmail/targets
mail_server:read · Self-hosted
Install or redeploy webmail; reconnectProjectId explicitly recovers an existing app after its mail-server link was lost.mail.deployWebmail(input)
Remote client
POST /api/mail/webmail/deploy-project
mail_server:write · Self-hosted
Deploy webmail for an external mail server.mail.deployExternalWebmail(input)
Remote client
POST /api/mail/webmail/deploy-external
mail_server:write · Self-hosted
Stop managing a server (drops the DB row only; leaves the stack + state file intact).mail.forget(id)
Remote client
DELETE /api/mail/servers/:serverId
mail_server:admin · Self-hosted
Mail daemons, outbound delivery, and cached public-port reachability (TCP 25/465/587/993).mail.getHealth(id, input?)
Remote client
GET /api/mail/health/:serverId
mail_server:read · Self-hosted
Read the mail certificate, SMTP/IMAP TLS checks and automatic renewal settings.mail.getCertificate(id)
Remote client
GET /api/mail/admin/:serverId/certificate
mail_server:read · Self-hosted
Recheck the certificate on disk and served by SMTP/IMAP without issuing a certificate or sending email.mail.checkCertificate(id)
Remote client
POST /api/mail/admin/:serverId/certificate/check
mail_server:write · Self-hosted
Renew a due mail certificate through Openship Edge and reload Postfix/Dovecot, reusing a still-valid certificate and repairing its service configuration.mail.renewCertificate(id)
Remote client
POST /api/mail/admin/:serverId/certificate/renew
mail_server:admin · Self-hosted
Set autoRenew to enable or disable automatic mail certificate renewal. Monitoring continues when renewal is disabled.mail.updateCertificate(id, input)
Remote client
PATCH /api/mail/admin/:serverId/certificate
mail_server:admin · Self-hosted
List mail domains on the server.mail.listDomains(id)
Remote client
GET /api/mail/admin/:serverId/domains
mail_server:list · Self-hosted
Add a mail domain.mail.createDomain(id, input)
Remote client
POST /api/mail/admin/:serverId/domains
mail_server:write · Self-hosted
List additional domains whose DNS is still pending.mail.pendingDomainDns(id)
Remote client
GET /api/mail/admin/:serverId/domains-dns/pending
mail_server:read · Self-hosted
List mailboxes in a domain.mail.listMailboxes(id, input)
Remote client
GET /api/mail/admin/:serverId/mailboxes
mail_server:list · Self-hosted
Create a mailbox.mail.createMailbox(id, input)
Remote client
POST /api/mail/admin/:serverId/mailboxes
mail_server:write · Self-hosted
Rotate the platform mailbox credential.mail.rotatePlatformMailbox(id)
Remote client
POST /api/mail/admin/:serverId/platform-mailbox/rotate
mail_server:admin · Self-hosted
List mail aliases on a server.mail.listAliases(id, input)
Remote client
GET /api/mail/admin/:serverId/aliases
mail_server:list · Self-hosted
Create a mail alias.mail.createAlias(id, input)
Remote client
POST /api/mail/admin/:serverId/aliases
mail_server:write · Self-hosted
Aggregate counts (domains, mailboxes, storage).mail.getStats(id)
Remote client
GET /api/mail/admin/:serverId/stats
mail_server:read · Self-hosted
The mail server's backup policy (or null).mail.getBackupPolicy(id)
Remote client
GET /api/mail/admin/:serverId/backup-policy
mail_server:read · Self-hosted
Create or update the backup policy.mail.saveBackupPolicy(id, input)
Remote client
POST /api/mail/admin/:serverId/backup-policy
mail_server:admin · Self-hosted
Recent backup runs for the mail server.mail.listBackupRuns(id)
Remote client
GET /api/mail/admin/:serverId/backup-runs
mail_server:read · Self-hosted
Live DNS health scan (MX/SPF/DKIM/DMARC/PTR).mail.scanDns(id, input?)
Remote client
GET /api/mail/admin/:serverId/dns-scan
mail_server:read · Self-hosted
Read outbound relay configuration.mail.getRelay(id)
Remote client
GET /api/mail/admin/:serverId/relay
mail_server:read · Self-hosted
Configure an outbound relay.mail.configureRelay(id, input)
Remote client
POST /api/mail/admin/:serverId/relay
mail_server:admin · Self-hosted
Remove outbound relay configuration.mail.removeRelay(id)
Remote client
DELETE /api/mail/admin/:serverId/relay
mail_server:admin · Self-hosted
Send a test email from the server.mail.sendTestEmail(id, input)
Remote client
POST /api/mail/admin/:serverId/test-email
mail_server:write · Self-hosted
Restart every mail component.mail.restartComponents(id)
Remote client
POST /api/mail/admin/:serverId/components/restart-all
mail_server:admin · Self-hosted
List inbound mail rules.mail.listInboundRules(id)
Remote client
GET /api/mail/admin/:serverId/inbound-rules
mail_server:read · Self-hosted
Create an inbound mail rule.mail.createInboundRule(id, input)
Remote client
POST /api/mail/admin/:serverId/inbound-rules
mail_server:write · Self-hosted
Evaluate a message against inbound mail rules.mail.testInboundRules(id)
Remote client
POST /api/mail/admin/:serverId/inbound-rules/test
mail_server:write · Self-hosted
Get one domain's config.mail.getDomain(parentId, id)
Remote client
GET /api/mail/admin/:serverId/domains/:domain
mail_server:read · Self-hosted
Update a domain (quotas, limits, active flag).mail.updateDomain(parentId, id, input)
Remote client
PATCH /api/mail/admin/:serverId/domains/:domain
mail_server:write · Self-hosted
Delete a domain (add ?cascade=true to remove its mailboxes too).mail.removeDomain(parentId, id, input?)
Remote client
DELETE /api/mail/admin/:serverId/domains/:domain
mail_server:admin · Self-hosted
Count mailboxes/aliases that depend on a domain.mail.domainDependents(parentId, id)
Remote client
GET /api/mail/admin/:serverId/domains/:domain/dependents
mail_server:read · Self-hosted
DNS records + publish state for an additional domain.mail.getDomainDns(parentId, id)
Remote client
GET /api/mail/admin/:serverId/domains/:domain/dns
mail_server:read · Self-hosted
Confirm a domain's DNS records are published.mail.acknowledgeDomainDns(parentId, id)
Remote client
POST /api/mail/admin/:serverId/domains/:domain/dns/acknowledge
mail_server:write · Self-hosted
Preview the mail domain's DNS records.mail.planDomainDns(parentId, id)
Remote client
GET /api/mail/admin/:serverId/domains/:domain/dns/plan
mail_server:read · Self-hosted
Apply the mail domain's DNS plan through a configured provider.mail.applyDomainDns(parentId, id)
Remote client
POST /api/mail/admin/:serverId/domains/:domain/dns/apply
mail_server:write · Self-hosted
Get one mailbox.mail.getMailbox(parentId, id)
Remote client
GET /api/mail/admin/:serverId/mailboxes/:email
mail_server:read · Self-hosted
Update a mailbox (name, password, quota, active).mail.updateMailbox(parentId, id, input)
Remote client
PATCH /api/mail/admin/:serverId/mailboxes/:email
mail_server:write · Self-hosted
Delete a mailbox (add ?hard=true to purge its maildir; default is a soft delete).mail.removeMailbox(parentId, id, input?)
Remote client
DELETE /api/mail/admin/:serverId/mailboxes/:email
mail_server:admin · Self-hosted
Update a mail alias.mail.updateAlias(parentId, id, input)
Remote client
PATCH /api/mail/admin/:serverId/aliases/:id
mail_server:write · Self-hosted
Remove a mail alias.mail.removeAlias(parentId, id)
Remote client
DELETE /api/mail/admin/:serverId/aliases/:id
mail_server:admin · Self-hosted
Update an inbound mail rule.mail.updateInboundRule(parentId, id, input)
Remote client
PATCH /api/mail/admin/:serverId/inbound-rules/:ruleId
mail_server:write · Self-hosted
Remove an inbound mail rule.mail.removeInboundRule(parentId, id)
Remote client
DELETE /api/mail/admin/:serverId/inbound-rules/:ruleId
mail_server:write · Self-hosted
Tail a component's logs.mail.componentLogs(parentId, id, input?)
Remote client
GET /api/mail/admin/:serverId/components/:key/logs
mail_server:read · Self-hosted
Run an action (start/stop/restart/…) on one component.mail.componentAction(parentId, id, input)
Remote client
POST /api/mail/admin/:serverId/components/:key/:action
mail_server:admin · Self-hosted
Start or resume the install wizard — returns an SSE stream.mail.setup(value, options?)
Remote client
POST /api/mail/setup
mail_server:write · Self-hosted

On this page