# Send as a person from a mailbox

> Send as a person through a connected Gmail, Microsoft 365 or SMTP mailbox, with copies in its Sent folder; OAuth consent, bring-your-own clients, reconnects.

Source: https://docs.minket.com/mailboxes/
Updated: 2026-10-29

A connected mailbox lets a project send as a person: the mail leaves through that person's own Gmail, Outlook or Microsoft 365 account, or any mailbox with SMTP, and a copy lands in its Sent folder, so replies thread in their own client. Domains stay the way to send as a product; mailboxes are for one-to-one mail.

## What each provider does

| Provider | Connects with | Sends with | Sent copy |
|---|---|---|---|
| Gmail | OAuth consent (`gmail.send` only) | Gmail API, the platform's MIME as is | Gmail files it in Sent |
| Microsoft 365, Outlook.com | OAuth consent (`Mail.Send`, `offline_access`) | Microsoft Graph `sendMail` with the MIME | Graph saves it in Sent Items |
| Anything else: Yahoo, iCloud, Zoho, a company server | SMTP host, port and password (an app password for Yahoo, iCloud and Gmail) | SMTP over TLS or STARTTLS | IMAP `APPEND` to the Sent folder, when IMAP is given |

Both OAuth providers also get `openid email`, only to learn which account consented. Graph takes at most about 3 MB of message, Gmail 25 MB; SMTP servers set their own limit.

## Connect Gmail or Microsoft

`POST /v1/mailboxes` with a live full-access key starts consent and answers with a URL that stays valid for 10 minutes:

```sh
curl https://api.minket.com/v1/mailboxes \
  -H "Authorization: Bearer $MINKET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "gmail", "return_url": "https://app.acme.example/settings/mail" }'
```

```json
{
  "object": "oauth",
  "url": "https://api.minket.com/oauth/google/start?state=…",
  "expires_at": "2026-10-27T18:10:00.000Z"
}
```

Send the person's browser to `url`, the API's start link: it ties the flow to that browser and opens the provider's consent screen. After they consent, the provider sends the browser to the API's callback (`/oauth/google` or `/oauth/microsoft`), which connects the mailbox and sends the browser on to `return_url` with the outcome in its query:

- `?mailbox=<id>&mailbox_status=connected` when it worked;
- `?mailbox_error=<reason>` when it did not: `access_denied` (consent declined), `expired` (more than 10 minutes), `scope_missing` (sending was unticked on Google's screen), `address_missing`, `consent_failed` or `limit_reached`.

`return_url` must be on the console or on the origin of the project's own OAuth redirect URI (below); anything else is refused with `422`, so consent can never end on someone else's site. Without it, the flow comes back to the console's mailboxes page.

The flow is the authorization code flow with PKCE. Its state is single use, bound to the project and to whoever started it, and stored only as an HMAC under the project's signing secret; a state that was used, forged, or meant for the other provider gets a plain error page and no redirect.

The flow is also bound to one browser: the first browser to open the start link gets a cookie (HttpOnly, Secure, SameSite=Lax, sent only to `/oauth` on the API host) whose hash the state keeps, and the callback connects the mailbox only in that browser. Anywhere else it shows a page saying to start again from that browser, and so does the start link opened in a second browser, so a consent screen passed to someone else connects nothing. Open the start link in the browser of the person who will consent; don't send it to anyone.

In the console, Mailboxes, Connect mailbox does the same and comes back to the page.

## Connect an SMTP mailbox

An SMTP mailbox is checked with a test login before anything is saved, and so is its IMAP login when one is given:

```sh
curl https://api.minket.com/v1/mailboxes \
  -H "Authorization: Bearer $MINKET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "smtp",
    "address": "support@acme.example",
    "display_name": "Acme support",
    "smtp": { "host": "smtp.mail.yahoo.com", "port": 465, "secure": "tls",
              "username": "support@acme.example", "password": "app-password" },
    "imap": { "host": "imap.mail.yahoo.com", "port": 993,
              "username": "support@acme.example", "password": "app-password",
              "sent_folder": "Sent" }
  }'
```

- `secure` is `tls` (TLS from the first byte, usually 465) or `starttls` (usually 587), the default. A server that does not offer STARTTLS is refused before the password is sent: the platform never logs in over plaintext. IMAP uses TLS on 993 and STARTTLS on 143.
- Hosts must be public. Private, loopback, link-local and metadata addresses are refused by name, and every address the host resolves to is checked again before connecting; the connection then goes to that checked address, so a second DNS answer cannot redirect it.
- `sent_folder` defaults to `Sent`. When the server has no folder by that name, the one it marks as Sent (RFC 6154) is used; with neither, the mailbox is refused.
- A refused login answers `422` on the field to fix (`smtp.password`, `imap.host` and so on).

The Sent copy is appended after the server accepts the email, with its blind copies, marked as read. It is best effort: if IMAP is down the email still counts as sent, and the mailbox's `last_error` says the copy was not saved.

## Send through a mailbox

```ts
await minket.emails.send({
  from: "Sam Rivera <sam@example.com>",
  to: "jane@example.net",
  subject: "Following up on our call",
  text: "Hi Jane, here are the notes from today.",
  via: { mailbox: "019a2b3c-4d5e-7f60-8a9b-0c1d2e3f4a5b" },
});
```

- `from` must be the mailbox's address; a display name is fine.
- The mailbox must belong to the key's project (another project's is `404`) and be `connected` (else `409 mailbox_unavailable`).
- Mailboxes send transactional mail only: `stream: "marketing"` is refused, and so is a key restricted to one domain.
- No domain is needed: the mailbox's provider authenticates the sender, so the domain gate does not apply. Suppressions, the kill switch, the daily cap, idempotency, scheduling and templates all work as for domain mail. Test keys simulate as usual and never reach the mailbox.

The send goes through the same [ledger](https://docs.minket.com/delivery/#never-sent-twice) as SES mail, so an answer lost after the request left is never sent again. A mailbox provider sends no delivery events, so the timeline holds `email.sent` and nothing after it; bounces come back to the mailbox as mail. `GET /v1/emails/{id}` shows `via` and `provider` (`gmail`, `microsoft` or `smtp`).

## Reconnect

When a grant is revoked, the person's password changes, or the OAuth client a mailbox used is removed, the next send through it fails with `mailbox_unavailable`, the mailbox moves to `needs_reconnect` with the reason in `last_error`, and an alert opens on the Alerts page. Connect it again the same way (consent with the same Google or Microsoft account, or the same SMTP address with new settings): the existing mailbox is updated in place, keeps its id and resolves its alert. Access tokens are refreshed a minute before they expire; Microsoft's rotated refresh tokens are stored as they arrive.

`DELETE /v1/mailboxes/{id}` removes a mailbox and, for Gmail, revokes its grant. `GET /v1/mailboxes` and `GET /v1/mailboxes/{id}` read them with any full-access key, test keys included; connecting and removing need a live key.

## Bring your own OAuth client

By default mailboxes connect through the platform's Google and Microsoft clients. A project can register its own instead, in the console under Settings, OAuth clients, so people consent on the calling app's own verified screen (a Google OAuth client you already have, for example). One client per provider; saving a second replaces the first.

- **Client id and secret** from Google Cloud or Microsoft Entra. The secret is encrypted and never shown again.
- **Redirect URI**, registered with the provider exactly as stored. Left empty, it is the API's callback, `https://api.minket.com/oauth/google` (or `/oauth/microsoft`). It can also be a page of your app on your own domain; that page redirects the browser to the API's callback with its query string unchanged, and `return_url` may then use your app's origin. Redirect, don't call the callback from your server: it needs the cookie the start link set in the browser.
- **Tenant** (Microsoft): a tenant id or domain, `organizations` or `consumers`; `common` by default.

Removing a client disconnects the mailboxes connected through it, since a grant belongs to the client that issued it; they reconnect through the platform's client or a new one.

## Security

Tokens, refresh tokens, client secrets and SMTP and IMAP passwords are encrypted at rest, bound to their row, and never returned by any API or written to any log. Audit rows record who connected, reconnected or removed a mailbox and which host it uses, never a secret. Mailboxes per project and the time each call may take are in [Limits and retention](https://docs.minket.com/limits/).
