---
title: "Responses API email tools with human-approved replies"
description: "Build Responses API email tools around Mailera reads, with server-side mailbox authorization, human-approved replies, and idempotent send retries."
url: https://mailera.app/en/blog/responses-api-email-tools
published: 2026-10-25
updated: 2026-10-25
---

# Responses API email tools with human-approved replies

Connect an existing mailbox through Mailera, keep reads narrowly scoped, and put a server-side approval check between a reply proposal and an actual send.

An email function tool is an interface the model can request; it is not permission to access a mailbox. Your application decides which requests to execute and which data to return.

Mailera supplies the mailbox connection and email operations your server calls. For the broader architecture, see [email for AI agents using existing mailboxes](https://mailera.app/en/blog/email-for-ai-agents). Here, the important boundary is between reading a message, proposing a reply, and authorizing its delivery.

## Should you use function tools or remote MCP?

Start with application-executed function tools. You can validate each request, restrict mailbox access, and keep the send credential outside the model's tool executor.

OpenAI defines function tools using JSON Schema, while application-side code executes the requested operation. The [OpenAI function-calling guide](https://developers.openai.com/api/docs/guides/function-calling) describes that division of responsibility.

| Decision | Function tools | Remote MCP |
| --- | --- | --- |
| Execution | Your application dispatches calls to Mailera REST endpoints | OpenAI invokes tools exposed by the remote server |
| Schema ownership | You define your wrapper's interface | The MCP server publishes its tools |
| Credentials | Your server holds the Mailera key | The remote connection needs its own authentication setup |
| Approval | Your application enforces the send boundary | Configure tool-call approval and check that it matches your application policy |

Mailera's [MCP endpoint and setup information](https://mailera.app/en/mcp) describe `https://mailera.app/api/public/v1/mcp`. Its tools include `list_mailboxes`, `get_mailbox_status`, `list_messages`, `search_messages`, `get_message`, `get_latest_otp` (read scope), and `send_email`, `reply_to_email`, `get_send_status` (send scope), plus `move_message`, `rescue_spam` (move scope) and `sync_mailbox` (sync scope).

OpenAI supports remote MCP servers using Streamable HTTP or HTTP/SSE. Its [remote MCP guide](https://developers.openai.com/api/docs/guides/tools-connectors-mcp) documents `allowed_tools`, explicit approvals, and the `mcp_list_tools` discovery output.

Before choosing that route, check authentication, transport compatibility, discovery, filtering, and approval behavior together. A configuration copied from Claude, Cursor, or VS Code setup docs is not a substitute for those checks. The [existing-mailbox MCP server guide](https://mailera.app/en/blog/email-mcp-server-existing-mailboxes) explains that integration path in more detail.

## What should you prepare first?

1. Connect a mailbox you control. Open **Mailboxes → Add mailbox**, then follow the provider setup. For Outlook or Microsoft 365, use [Mailera's Microsoft OAuth connection](https://mailera.app/en/providers/microsoft), rather than an app password. Confirm the mailbox's status on **Mailboxes**.
2. Open **Settings → API key tab → Create key**. Choose a descriptive name, restrict **Mailbox scope** to the group containing your test mailbox, select **Read**, and set **Expiration**.
3. Keep the OpenAI credential and Mailera read key on your server. Create a separate Mailera key with **Send messages** for the approved-send executor. Do not give that key to the read-tool process.

![Mailboxes page showing the connection status and last sync of a redacted test mailbox.](https://mailera.app/media/8d7f667e969054ac/responses-api-email-tools-with-human-approved-replies-en-1200.jpg)
_Check the mailbox connection before debugging the Responses API integration._

The key appears once under **Key created — copy it now**. The same dialog also has a **Copy MCP config** option if you later want to connect this key through MCP instead. Store it in your server's secret storage, not a prompt, browser bundle, or screenshot. The [Mailera developer overview](https://mailera.app/en/developers) provides the REST integration entry point.

![Mailera API-key settings with a restricted mailbox group and Read permission.](https://mailera.app/media/44f579be902e2448/responses-api-email-tools-with-human-approved-replies-read-en-1200.jpg)
_Use a restricted read key for the tool dispatcher and a separate key for approved sends._

With `$MAILERA_API_KEY` set to your read key, inspect the mailbox list:

```bash
curl --fail-with-body \
  'https://mailera.app/api/public/v1/mailboxes?limit=1' \
  -H "Authorization: Bearer $MAILERA_API_KEY"
```

The response contains a `data` array. Use the mailbox's `id`, `status`, and `capabilities` when setting up your application. A successful read does not establish that the connection can send; check `capabilities.send` separately.

## How do you keep email tools narrowly scoped?

Use application-defined names that make the wrapper's purpose clear. These names are your interfaces, not native Mailera MCP tool names.

| Application wrapper | Mailera REST operation | Scope | Application restriction |
| --- | --- | --- | --- |
| `email_list_mailboxes` | `GET /mailboxes` | `read` | Return only mailboxes authorized for the current user |
| `email_mailbox_status` | `GET /mailboxes/{id}/status` | `read` | Bind the mailbox server-side before returning status |
| `email_list_inbox` | `GET /mailboxes/{id}/messages` | `read` | Bind the mailbox server-side; fix the folder and cap results |
| `email_search_messages` | `GET /messages/search` | `read` | Set the `mailbox` filter server-side to an authorized mailbox and cap results |
| `email_read_message` | `GET /mailboxes/{id}/messages/{msgId}` | `read` | Check message ownership and cap content |
| Approved-send executor | `POST /mailboxes/{id}/send` | `send` | Require approval for the exact request payload |

`GET /messages/search` supports cross-mailbox search and is the REST counterpart to the MCP `search_messages` tool. The `email_list_inbox` wrapper instead uses the single-mailbox listing operation. For this walkthrough, keep search restricted to the authorized mailbox rather than exposing cross-mailbox access.

For a single-mailbox task, leave mailbox selection out of the model's arguments entirely. Resolve the mailbox from the authenticated user's application session.

For example, this application-owned input schema permits only a message identifier:

```ts
export const readMessageInput = {
  type: "object",
  properties: {
    message_id: { type: "string", minLength: 1 }
  },
  required: ["message_id"],
  additionalProperties: false
} as const;
```

Use it when defining your function interface, following OpenAI's function-schema requirements. Independently validate the parsed arguments on your server; schema guidance to the model is not an authorization check.

```ts
export function parseReadArguments(raw: string): string {
  const value: unknown = JSON.parse(raw);
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error("Expected an argument object");
  }
  const args = value as Record<string, unknown>;
  if (Object.keys(args).length !== 1 ||
      typeof args.message_id !== "string" ||
      args.message_id.trim().length === 0) {
    throw new Error("Expected only a nonempty message_id");
  }
  return args.message_id;
}
```

The REST read itself uses the authorized mailbox and selected message:

```bash
curl --fail-with-body \
  "https://mailera.app/api/public/v1/mailboxes/$MAILBOX_ID/messages/$MESSAGE_ID" \
  -H "Authorization: Bearer $MAILERA_API_KEY"
```

Before returning content to the model, verify that `data.mailbox_id` matches the authorized mailbox and `data.id` matches the requested message. Project only the fields needed for the task, such as `subject`, `from`, and a bounded portion of `body.text`. Do not forward the entire response by default.

## How should your application run the tool loop?

Treat each model response as a set of requests to inspect, not a command to execute wholesale. OpenAI's function-calling flow can continue through multiple calls, so a dispatcher must handle more than the first requested operation.

1. Submit the user's task with only the read-tool definitions.
2. Inspect the returned tool calls and reject names outside your allowlist.
3. Parse and validate each call's arguments. Resolve mailbox authorization from application state before making the Mailera request.
4. Execute the read and turn its response into a bounded, serialized tool result. Track each dispatched read and its result in your application's own records; do not rely on execution completion order.
5. Return the tool results to the model and continue the function-calling flow within your application's execution budget.
6. Stop when the model returns its answer or your application's execution budget is exhausted.

Set budgets for calls, returned content, and elapsed work. If the model produces no tool calls, handle that as a normal answer rather than attempting another mailbox read.

For a tool failure, return a short application-generated error result without credentials or raw provider diagnostics. Do not represent a failed read as an empty inbox. When reads finish out of order, update the corresponding read records in your application's task state rather than assigning results by completion order.

Store a proposed reply in your application's state. It remains a proposal until a person approves it; generating text does not require giving the model send access.

![Responses API read requests pass through application validation; replies reach the send executor only after human approval.](https://mailera.app/media/b932469db78dc0f3/responses-api-email-tools-with-human-approved-replies-en-1200.jpg)
_A reply proposal is not a send authorization. Keep the approval check outside the model._

## How do you approve and send without duplicates?

Make the review screen show the sender mailbox, every recipient, subject, complete body, and reply target. Avoid a generic approval button that authorizes whatever the model produces next. For the broader approval architecture, see [building an email agent with approval boundaries](https://mailera.app/en/blog/email-agent-tutorial-human-approval).

Use this application-side sequence:

1. Build and persist the exact proposed send payload, including its reply target.
2. Show that immutable version to the reviewer.
3. Record the authenticated reviewer's approval against that version or a digest of its complete payload.
4. Have the send executor load the stored payload and approval. Recheck mailbox authorization and reject any mismatch.
5. Claim the approved operation atomically before dispatch so competing workers cannot independently submit it.

Any edit requires new approval, including a recipient change or a different sending mailbox. Never accept an `approved` flag supplied by the model as proof of human consent. This approval workflow belongs to your application, not to a native Mailera approval mechanism.

Mailera supports sending, replying to, or forwarding a message through `POST /mailboxes/{id}/send`, using the `send` scope. Build the request according to the [Mailera API documentation](https://mailera.app/en/docs), then bind approval to that exact request.

Persist an `Idempotency-Key` before the first submission. Reuse it for retries of the same operation with the same payload. Mailera's configured idempotency window is 24 hours; keep your own operation record so a retried job outside that window doesn't get treated as a brand-new send. See [preventing duplicate sends with Idempotency-Key](https://mailera.app/en/blog/email-api-idempotency-key).

Record the send-attempt identifier and inspect `GET /sends/{id}` with the send credential. Keep `queued`, `sending`, `sent`, `failed`, and `unknown` distinct. In particular, an `unknown` outcome calls for reconciliation, not a new key and another send.

## What commonly breaks?

| Symptom | What to inspect |
| --- | --- |
| Arguments fail validation | Reject malformed JSON, unexpected fields, and invalid values before dispatch |
| A result appears attached to the wrong read in application records | Track each dispatched read and its result in your application's own records; do not match by completion order |
| Your application repeats completed reads | Check its task records for completed, pending, and failed reads before dispatching more work |
| Only one requested read runs | Iterate over all returned calls within your execution budget |
| Mailera rejects a read | Check **Read** permission, mailbox-group restrictions, and whether the key was revoked |
| Messages look stale | Check mailbox status and synchronization timestamps |
| Reads work but sending does not | Check the separate send key and `capabilities.send`; the connection may be receive-only |
| Requests are rate-limited | Use bounded retries with backoff; do not continuously replay the whole task |
| Remote MCP tools do not appear | Diagnose authentication, transport, discovery, and filtering separately from REST wrappers |

Mailbox freshness depends on provider connectivity and synchronization. Background checks vary with mailbox activity, and idle mailboxes can sleep. Do not design the workflow around an absolute real-time arrival assumption.

## How do you keep incoming email from becoming instructions?

Treat subjects, bodies, attachments, and quoted replies as untrusted data. A message asking the agent to reveal a key, change its tools, or forward unrelated correspondence is still message content, not authorization.

Keep credentials outside model context. Send only the content needed for the user's task and redact sensitive content from logs. The read dispatcher should expose no import, sharing, management, or OTP tools.

Before enabling sending, test that malicious message content cannot cause a send, change recipients after approval, or read another user's mailbox. Also test simultaneous workers retrying the same approved operation. These are application tests, not behaviors to enforce only through a system prompt.

For the access review, use [Mailera's security information](https://mailera.app/en/security) alongside the guide to [connecting an AI assistant to email safely](https://mailera.app/en/blog/is-it-safe-to-connect-claude-to-gmail). The [MCP tools specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) also recommends confirmation for sensitive operations.

## What does Mailera not do here?

Mailera connects mailboxes you already have; it does not create new mailboxes or addresses. It does not provide bulk or marketing sending, and a `sent` status is not a guarantee of inbox placement.

Your application remains responsible for deciding who may read each mailbox, binding approval to the exact reply, and preventing its workers from dispatching an operation twice.

**Let AI agents use your mailbox via MCP** — Add Mailera's MCP server to Claude, Cursor or VS Code so agents can read, categorize and reply to email messages. [Learn how to use MCP](https://mailera.app/en/docs#mcp)
