# Take messages out of Junk, back to where they were filed

Removes the Junk label from a batch of messages — the "Not spam" verb, and POST /messages/untrash's counterpart for the other state label. Where each one lands is its restoresTo: whatever it carries besides Junk if anything, otherwise the filing recorded when a label write left it carrying only Junk, otherwise Inbox. An origin label that has since been deleted is matched by name if a label of that name exists again. Reports as an update on /changes (the rows were never hidden). This does NOT train the spam filter; pair it with POST /messages/learn as before. `threadIds` takes a whole conversation out: it expands, server-side and in the same transaction, to the members that are IN Junk. A conversation with nothing junked expands to nothing and the call is a no-op.

## `POST /api/v1/mailboxes/{mailboxId}/messages/unjunk`

### Path parameters

- `mailboxId` (string, required) — Mailbox ULID identifier

### Request body (optional)

`application/json`

- `ids` (string[], min 1 items, max 200 items) — Array of message IDs to take out of Junk
- `threadIds` (string[], min 1 items, max 50 items) — Conversation IDs whose JUNKED members to take out of Junk, expanded server-side. Refused with thread_too_large past 1000 expanded messages.

### Responses

**200** — Batch unjunk outcomes per message

`application/json` → BatchUnjunkResult

- `results` (BatchUnjunkEntry[], required) — List of unjunk outcomes for each requested message
  - `id` (string, required) — Message ULID
  - `status` ("unjunked" | "not_junked" | "not_found", required) — Unjunk outcome for this message. not_junked: the message exists and is readable but does not carry the Junk label, so there is nothing to undo — deliberately distinct from not_found, which means no such live message (an EXPUNGED id is not_found here; use POST /messages/restore for that tier).
  - `message` (MessageMeta & object)
    - `auth` (MessageAuth, required) — Stored SMTP authentication and original inbound junk verdicts. Missing verdicts are null; the object is null when none were recorded (including APPEND and local Sent copies). Junk does not follow later label changes.
      - `arc` (string | null, required) — ARC verdict, or null
      - `dkim` (string | null, required) — DKIM verdict, or null
      - `dmarc` (string | null, required) — DMARC verdict, or null
      - `junk` (boolean | null, required) — The inbound spam verdict (X-OpenEmail-Junk), or null when none was stamped
      - `spf` (string | null, required) — SPF verdict (pass/fail/softfail/none/...), or null
    - `bcc` (array | null, required) — Parsed Bcc header addresses for composed drafts, or null
    - `blobGen` (string, required) — Storage generation identifier
    - `blobHash` (string, required) — SHA-256 hash of raw MIME content in storage
    - `cc` (array | null, required) — Parsed Cc header addresses, or null if unparseable
    - `deliveryMeta` (object | null, required) — Inbound delivery audit metadata
    - `deliveryState` ("queued" | "relayed" | "delivered" | "failed" | "mixed", required) — What became of this message after it was SENT, for a message this mailbox sent: 'queued' (handed to the relay), 'relayed' (the receiving mail system accepted it — never 'read'), 'delivered' (committed to a local mailbox), 'failed', or 'mixed' (some recipients failed, others did not). Null for received mail, and for sent mail whose delivery record has aged out — render null as no indicator, never as a state. Per-recipient detail and the SMTP reply behind a failure are at /messages/{messageId}/delivery.
    - `envelopeFrom` (string, required) — SMTP envelope sender address
    - `envelopeTo` (string, required) — SMTP envelope recipient address
    - `flags` (string[], required) — System flags (seen, answered, flagged, draft, deleted)
    - `from` (array | null, required) — Parsed From header addresses, or null if unparseable
    - `hasAttachment` (boolean | null, required) — True if message has file attachments, null if not yet determined
    - `id` (string, required) — Unique message identifier (ULID)
    - `inReplyTo` (string | null, required) — In-Reply-To header Message-ID, or null
    - `keywords` (string[], required) — Custom JMAP keywords (e.g. $forwarded)
    - `labels` (MessageLabel[], required) — Labels attached to this message with per-label UIDs
      - `deleted` (boolean, required) — This membership's IMAP \Deleted mark (set with `flagLabel` on PATCH, acted on by POST /labels/{label}/expunge). Always present; false when unmarked. Per label, so the same message can be marked in one folder and clean in another
      - `modseq` (integer <int64>, required) — RFC 7162 MODSEQ change sequence number
      - `name` (string, required) — Label name
      - `uid` (integer <int64>, required) — Message UID inside this label
      - `uidValidity` (integer <int64>, required) — UID validity epoch of the label
    - `messageIdHeader` (string | null, required) — Message-ID header value, or null
    - `receivedAt` (integer <int64>, required) — Timestamp when message was received by server in epoch seconds
    - `referencesIds` (string[], required) — List of Message-IDs from References header
    - `sentAt` (integer | null, required) — Sent timestamp from Date header in epoch seconds, or null
    - `size` (integer <int64>, required) — Total message size in bytes
    - `snippet` (string | null, required) — Short plain-text preview of message body
    - `subject` (string | null, required) — Message subject line, or null
    - `threadId` (string | null, required) — Conversation thread identifier (ULID), or null
    - `to` (array | null, required) — Parsed To header addresses, or null if unparseable
    - `modseq` (integer <int64>) — Per-label MODSEQ (present in label UID listings)
    - `restoresTo` (array | null) — Where a recovery would file this message, as label names. Present only when the message is recoverable: one carrying the Trash label (POST /messages/untrash), one carrying the Junk label with a recorded origin (POST /messages/unjunk), or one on the expunged tier (POST /messages/restore). A message in both Trash and Junk's records answers for Trash, the verb a client offers it first. Null otherwise, meaning the message is filed rather than recoverable. This is the effective destination including the Inbox fallback, not a raw snapshot, so a client can name it on a Put back or Not spam control without reimplementing the rule.
    - `uid` (integer <int64>) — Per-label UID (present in label UID listings)

**400** — validation_failed, target_required (neither ids nor threadIds given), or thread_too_large

`application/json` → ErrorOrValidation

**401** — unauthorized: the bearer credential is missing, malformed, expired, or revoked. Carries a `WWW-Authenticate: Bearer` challenge

`application/json` → ErrorBody

**404** — Mailbox not found or not accessible to this principal

`application/json` → ErrorBody

**413** — too_large: JSON request body exceeds 1 MiB or 500,000 values (object keys included), checked before parsing

`application/json` → ErrorBody

**503** — temporary_failure: the platform's storage could not answer just now; retry after the Retry-After window. Any operation can answer this. A write that got it may have been applied, so retry it with the same idempotency key where it takes one, or read the state back first (see Transient failures in the API description)

`application/json` → ErrorBody

### Example request

```bash
curl -X POST 'https://api.open.email/api/v1/mailboxes/{mailboxId}/messages/unjunk' \
  -H 'Authorization: Bearer oek_…' \
  -H 'Content-Type: application/json' \
  -d '{}'
```
