Where should the approval boundary sit?
Put the approval boundary between a stored proposal and the component allowed to send it. A model-generated “approved” flag must never be enough to cross that boundary.
For this LangGraph email approval workflow, use an existing test mailbox and a recipient you control. Design separate read, draft, review, and send stages. The read stage uses Mailera MCP; the final executor uses REST so it can attach an Idempotency-Key.
| Component | Responsibility |
|---|---|
| LangGraph workflow | Coordinate reading, drafting, waiting, and handing off an approved operation |
| Persistent application storage | Preserve draft revisions, review decisions, and send-operation records |
| Mailera | Provide access to the connected mailbox through MCP and REST |
| Reviewer application | Authenticate reviewers, enforce mailbox authorization, and record decisions |
| Send executor | Recheck approval, submit the stored payload, and reconcile send status |
The review screen and its approval endpoint belong to your application. Keep them separate from Mailera's mailbox and API-key screens.

For the broader mailbox architecture, see email for AI agents using existing mailboxes. This workflow focuses on the narrower question of who can turn a particular draft into a send operation. The guide to building an email agent with approval boundaries covers the framework-neutral design behind that separation.
1. Give the drafting agent read-only MCP access
Start with a connected mailbox that can send. For Outlook or Microsoft 365, follow the Microsoft mailbox connection guide and use Sign in with Microsoft.
Before wiring up the agent, inspect the selected mailbox from your server:
curl --fail-with-body "https://mailera.app/api/public/v1/mailboxes/$MAILBOX_ID" \
-H "Authorization: Bearer $MAILERA_API_KEY"Check data.capabilities.send. A mailbox that can receive isn't necessarily ready for the final send stage.
Create the model-facing key under Settings → API key tab → Create key:
- Set a Descriptive name that identifies the drafting workflow.
- Restrict Mailbox scope to the intended group.
- Enable Read, without Send messages.
- Store the key in server-side secret storage.

Use the Mailera MCP endpoint, https://mailera.app/api/public/v1/mcp, with bearer authentication. Keep the credential in the connection layer, not in prompts or graph state.
For this workflow, allowlist only list_mailboxes, list_messages, and get_message. All three use the read scope. Don't expose every discovered tool to the model just because the connection can access it.
The MCP tool discovery specification defines tools/list for discovery and tools/call for invocation. Use each discovered tool's inputSchema to construct its arguments; don't assume REST parameter names are also MCP arguments.
Create a separate key with Send messages for the executor. Do not attach that key to the model-facing MCP connection. Mailera's send_email, reply_to_email, and get_send_status tools require the send scope, so they don't belong in this drafting toolset.
2. Persist the proposal before requesting review
Treat the proposed reply as application data, not as an already-authorized action. Store it before exposing a review task or allowing a resume request.
Keep these values together:
| Application record | What to preserve |
|---|---|
| Workflow identity | Stable thread identifier and owning user or tenant |
| Source | Mailbox identifier and source-message identifier |
| Proposed reply | Complete recipient lists, subject, and body |
| Revision | Draft revision and canonical payload hash |
| Review | Pending, approved, or rejected; reviewer identity and expiry |
| Send operation | Stable operation identifier, idempotency key, and send-attempt identifier, when available |
These are application-owned fields, not Mailera request fields. Keep the draft in your application until the executor is ready to submit it.
Make durability a requirement of the LangGraph implementation: the review must survive a worker restart, and the application must locate the same workflow afterward. Don't use a browser session or an in-memory dictionary as the only copy of the pending decision.
Keep the approval stage free of irreversible side effects before the pause. In particular, don't submit an email while preparing the review payload. Put submission behind the executor's authorization check instead.
Freeze the proposal presented for review. If the model runs again, treat its output as a new revision rather than silently replacing the version already on screen.
3. Bind external approval to the exact reply
An approval callback should identify a stored decision. It should not supply arbitrary replacement content for the executor to send.
Use this application-side sequence:
- Load the pending review from storage and display the sending mailbox, complete recipients, subject, and outgoing body.
- Authenticate the reviewer and check their authorization for that mailbox and workflow.
- Compare the displayed revision with the current stored revision.
- Record approval or rejection, including the reviewer identity and expiry.
- Atomically associate an approval with one durable send operation.
- Request continuation of the corresponding workflow through the application's server-side resume handler.
Don't trust a thread identifier from the browser on its own. Resolve it through an authorized review record. Reject expired decisions, mismatched workflow identities, stale revisions, and attempts to reuse a decision for another operation.
A payload hash can help detect changes. For a JSON-compatible application proposal, this Python helper produces a deterministic hash from its serialized content:
import hashlib
import json
def proposal_hash(proposal: dict) -> str:
encoded = json.dumps(
proposal,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()Include the mailbox, source message, recipients, subject, body, and revision in that proposal. Apply the same normalization rules before display, hashing, and storage. A hash detects a mismatch; it does not authenticate a reviewer or authorize a send.
Any edit requires new approval. That includes recipient changes and edits made by a human reviewer. Rejection ends the operation without sending. An edit returns it to review.

This flow uses an authenticated external application, not a reply email as authorization. Email-based human approval is a separate application pattern.
4. Send the approved version, not a regenerated reply
The executor should load the approved proposal from storage and recheck its revision, hash, authorization, and operation identity. Don't ask the model to “finalize” the message after approval: that creates a different payload.
Use POST /mailboxes/{id}/send for submission. It requires the send scope and accepts the Idempotency-Key header. Keep request construction inside the executor, following the Mailera developer API documentation.
Persist one idempotency key per approved send operation before making the request. Reuse that key when retrying the same operation; don't generate a fresh key for every HTTP attempt.
Mailera's idempotency window is 24 hours. Retain your own operation record beyond that window and require reconciliation before any later resubmission. See preventing duplicate sends with Idempotency-Key for the retry pattern.
Protect against concurrent approval callbacks with an atomic database transition or unique operation constraint. Both callbacks should resolve to the same stored operation, not create two executor jobs with different keys. Record that association durably so a worker crash doesn't discard the work after consuming the approval.
When a send-attempt identifier is available, inspect GET /sends/{id} using the executor’s send-scoped credential.
| Status | Application handling |
|---|---|
queued | Preserve the operation and continue status checks |
sending | Keep the operation in progress; don't create a replacement |
sent | Record the outcome without treating it as proof of inbox placement |
failed | Investigate before authorizing another submission |
unknown | Reconcile the existing attempt; don't assume nothing was sent |
A timeout after submission is an ambiguous outcome. If a send-attempt identifier is available, inspect that attempt. If the submission response is lost, recover using the stored operation and its stable idempotency key within the window. After the window, route unresolved attempts for reconciliation rather than automatically issuing a new send.
What commonly breaks, and how should you test it?
Run acceptance tests against your own mailbox and controlled recipient. The results below are requirements for the implementation, not reasons to relax the approval boundary when something fails.
| Symptom | Likely cause | Check |
|---|---|---|
| Pending review disappears after restart | Review or workflow state exists only in memory | Restart while waiting; recover the same proposal and review identity |
| Approval reaches the wrong task | Browser-supplied thread identity isn't bound to the review | Submit a mismatched identity; require rejection |
| Read tools fail | Wrong key, missing read scope, or wrong mailbox group | Check the read key and its mailbox restriction |
| Reading works but sending doesn't | Mailbox is receive-only | Inspect capabilities.send before starting the workflow |
| Executor cannot submit | Missing send permission or revoked key | Check the executor's credential, not the drafting key |
Test the decision paths separately:
- Approval: submit exactly the stored, approved version.
- Rejection: finish without a send operation.
- Edit and reapprove: invalidate the old decision and display the new revision.
- Stale, expired, or unauthorized approval: reject without submission.
- Duplicate and concurrent callbacks: resolve to one durable operation.
- Restart while waiting: recover the same review without approving it implicitly.
- Timeout after submission: reconcile the existing attempt instead of creating a fresh operation.
Also test a crash between recording approval and starting the executor. The durable operation must remain discoverable by the worker.
What safety checks belong around the agent?
Treat incoming email, quoted text, and attachments as untrusted data. Instructions inside a message must not grant permissions, approve a draft, change recipients outside your policy, or select a different executor credential.
Use a test email that asks the agent to skip review and send information elsewhere. Require the workflow to remain unable to send without application-authorized approval, regardless of what the draft says.
Human review isn't automatic protection against data leakage. Show reviewers the complete outgoing content and every recipient. The MCP tool safety guidance recommends human control over tool invocation and showing tool inputs before server calls.
The MCP security best practices also prohibit servers from accepting tokens that weren't explicitly issued for that MCP server.
Keep credentials out of prompts, persisted workflow state, screenshots, browser responses, and logs. Restrict access to stored message content and define its retention period. Review Mailera's security information alongside your application's own controls; email-agent connection safety covers the broader access questions.
What Mailera does—and doesn't—handle
Mailera connects mailboxes you already have; it doesn't create mailboxes or email addresses. Implement reviewer authentication, approval storage, and enforcement in your application.
Mailera does not provide bulk or marketing sending, and it does not guarantee inbox placement. Its 24-hour send-idempotency window is one part of retry handling, not an exactly-once guarantee for the complete workflow.
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








