# Mail
URL: https://openship.io/docs/api/mail.md

Set up and operate a self-hosted mail server.

import { TypeTable } from 'fumadocs-ui/components/type-table';

The Mail API drives Openship's self-hosted email feature: it installs an [iRedMail](https://www.iredmail.org)
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.

<Callout title="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`.
</Callout>

<Callout title="Every route is server-scoped" type="info">
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.
</Callout>

## Start (or resume) setup

Kicks off the iRedMail install wizard against a server and streams progress as
[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/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
```

<TypeTable
  type={{
    serverId: { type: 'string', description: 'The Openship server to install onto.', required: true },
    domain: { type: 'string', description: 'The mail domain, e.g. example.com. Mail is served from mail.<domain>.', required: true },
    startStep: { type: 'number', description: 'Step to start/resume from (clamped to 1..total).', default: '1' },
    config: { type: 'IRedMailConfig', description: 'Optional install config (see below).' },
  }}
/>

`config` is an optional `IRedMailConfig`:

<TypeTable
  type={{
    adminPassword: { type: 'string', description: 'Password for the postmaster account. Generated if absent.' },
    storageBackend: { type: "'mariadb' | 'postgresql'", description: 'iRedMail storage backend.', default: "'postgresql'" },
    prefillSecrets: { type: 'Record<string, string>', description: 'Previously-generated secrets to reuse on a retry, keyed as iRedMail expects (e.g. VMAIL_DB_BIND_PASSWD).' },
  }}
/>

```bash
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
```

<TypeTable
  type={{
    serverId: { type: 'string', description: 'The server whose setup state to update.', required: true },
  }}
/>

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
```

```bash
# 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
```

<TypeTable
  type={{
    serverId: { type: 'string', description: 'The mail server.', required: true },
    password: { type: 'string', description: 'New password (minimum 12 characters).', required: true },
  }}
/>

## Add a mail domain

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

<TypeTable
  type={{
    domain: { type: 'string', description: 'The domain to add, e.g. example.com.', required: true },
    description: { type: 'string', description: 'Free-text label for the domain.' },
    maxMailboxes: { type: 'number', description: 'Cap on mailboxes for this domain.' },
    maxAliases: { type: 'number', description: 'Cap on aliases for this domain.' },
    defaultQuotaMB: { type: 'number', description: 'Default per-mailbox quota (MB).' },
  }}
/>

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

```bash
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
```

<TypeTable
  type={{
    localPart: { type: 'string', description: 'The part before the @, e.g. alice.', required: true },
    domain: { type: 'string', description: 'An existing mail domain on this server.', required: true },
    password: { type: 'string', description: 'Initial mailbox password.', required: true },
    name: { type: 'string', description: 'Display name.' },
    quotaMB: { type: 'number', description: 'Mailbox quota (MB).' },
  }}
/>

`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.

```bash
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](/docs/api/backups) — pick a destination and what to
capture. Requires an existing backup destination in your organization.

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

<TypeTable
  type={{
    destinationId: { type: 'string', description: 'An existing backup destination in your org.', required: true },
    messageData: { type: 'boolean', description: "Include mailbox message data (maildirs).", default: 'false' },
    keys: { type: 'boolean', description: 'Include keys/secrets (DKIM keys, config).', default: 'true' },
    cronExpression: { type: 'string | null', description: 'Schedule; null for manual only.' },
    retainCount: { type: 'number | null', description: 'Keep the newest N runs.' },
    retainDays: { type: 'number | null', description: 'Keep runs younger than N days.' },
  }}
/>

## Send a test email

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

<TypeTable
  type={{
    to: { type: 'string', description: 'Recipient address.', required: true },
    fromDomain: { type: 'string', description: 'Sender domain (defaults to the primary install domain).' },
  }}
/>

## 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
```

<TypeTable
  type={{
    mailServerId: { type: 'string', description: 'The mail server this webmail fronts.', required: true },
    hostname: { type: 'string', description: 'Fully-qualified hostname to serve webmail at.', required: true },
    target: { type: 'object', description: 'Where to deploy — see below.', required: true },
    reconnectProjectId: {
      type: 'string',
      description: 'Explicitly reconnect an existing catalog webmail when the mail registration lost its link. Choose an ID from status.webmailRecoveryCandidates.',
    },
    replaceLegacy: {
      type: 'boolean',
      description: 'Consent to replace a pre-catalog webmail. Only needed after a 409 LEGACY_WEBMAIL.',
    },
  }}
/>

`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.

```bash
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

<Callout title="503 — MAIL_STATUS_UNAVAILABLE" type="warn">
`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.
</Callout>

<Callout title="404 — Not available" type="error">
You're calling a cloud instance. The Mail API is self-hosted only; run it on an instance where `CLOUD_MODE`
is off.
</Callout>

<Callout title="404 — Server not found" type="error">
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.
</Callout>

<Callout title="409 — Setup already running" type="warn">
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`.
</Callout>

<Callout title="409 — LEGACY_WEBMAIL" type="warn">
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.
</Callout>

<Callout title="502 — Scan / adopt failed" type="warn">
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.
</Callout>

## 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.

```ts
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.

{/* api-operations:start */}

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

{/* api-operations:end */}
