Skip to content

Tutorials

Give Claude Code a secure email inbox with MCP

Connect Claude Code to a Postfleet inbox over Streamable HTTP, keep the bearer key out of the repository, and allow only the read tools the workflow needs.

Connect Claude Code to Postfleet with a project-scoped HTTP MCP server, pass the Postfleet key through an environment variable, and bind that key to one mailbox with send disabled. Deny every unused Postfleet tool in Claude Code as a second guard. The resulting workflow can list, wait for, and read cleaned email without gaining a path to send mail or provision infrastructure.

This tutorial builds a narrow verification inbox. Claude Code waits for one expected message, reads it, and returns the code. The same setup works for test results, support intake, or any workflow that only needs to receive email.

What you need#

Before editing the project, create these Postfleet resources in the dashboard:

  • One mailbox for the workflow.
  • One MCP key bound to that mailbox.
  • can_read enabled on the key.
  • can_send disabled on the key.

Copy the mailbox ID and key once. The key begins with pf_. Do not use an account-wide provisioning key for the day-to-day Claude Code connection.

You also need Claude Code installed and authenticated. Anthropic's MCP support uses Streamable HTTP for remote servers. SSE remains available for older servers but is deprecated, so use the HTTP endpoint for Postfleet.

Add Postfleet to .mcp.json#

Set the key in the shell that starts Claude Code:

export POSTFLEET_API_KEY="pf_your_mailbox_bound_key"

Create .mcp.json in the project root:

{
  "mcpServers": {
    "postfleet": {
      "type": "http",
      "url": "https://api.postfleet.ai/api/mcp",
      "headers": {
        "Authorization": "Bearer ${POSTFLEET_API_KEY}"
      }
    }
  }
}

Claude Code expands environment variables in HTTP headers. The configuration can be shared with a team without putting the bearer token in version control. If POSTFLEET_API_KEY is missing, Claude Code rejects the configuration instead of connecting with an empty value.

Project scope is useful when everyone working in one repository needs the same server definition. Claude Code asks each user to approve a project-scoped MCP server after they trust the workspace. The approval cannot be silently granted by files in a newly cloned repository.

If Postfleet should be available across all of your projects, use user scope instead:

claude mcp add-json --scope user postfleet \
  '{"type":"http","url":"https://api.postfleet.ai/api/mcp","headers":{"Authorization":"Bearer ${POSTFLEET_API_KEY}"}}'

The user-scoped entry lives in ~/.claude.json and is not shared through the repository. Pick one scope. Defining the same server name at several scopes makes troubleshooting harder because Claude Code uses the highest-precedence definition rather than merging them.

Verify the connection#

From the project directory, run:

claude mcp list
claude mcp get postfleet

Then start Claude Code and open:

/mcp

Approve the project server if prompted. A healthy connection lists the Postfleet tools, including list_inbox, read_email, and wait_for_email.

A 401 response usually means the bearer header is missing or the key is invalid. A 403 key_scope response means the key exists but the requested tool, mailbox, or capability falls outside its scope. Fix the key binding rather than replacing it with an account-wide key.

Deny unused tools in Claude Code#

The Postfleet key is the server-side authorization boundary. Claude Code permissions add a client-side boundary and keep unused tools out of ordinary operation.

Add .claude/settings.json:

{
  "permissions": {
    "allow": [
      "mcp__postfleet__list_inbox",
      "mcp__postfleet__read_email",
      "mcp__postfleet__wait_for_email"
    ],
    "deny": [
      "mcp__postfleet__create_mailbox",
      "mcp__postfleet__send_email",
      "mcp__postfleet__reply_email",
      "mcp__postfleet__create_draft",
      "mcp__postfleet__list_drafts",
      "mcp__postfleet__send_draft"
    ]
  }
}

MCP tool names follow mcp__<server>__<tool>, so the configured server name postfleet becomes the middle segment. Claude Code evaluates deny rules before allow rules. Exact denials remove provisioning, sending, and draft tools from use, while the three inbox tools can run without an interactive approval each time.

Keep can_send=false on the Postfleet key even with these rules. A user can change a local Claude Code setting. They cannot make a read-only key pass Postfleet's server authorization. Two independent checks also protect you from a typo in either configuration.

If every read should require confirmation during initial testing, omit the allow array. Claude Code will prompt according to its normal permission mode. Add the allow entries only after the mailbox and workflow are correct.

Wait for a verification email#

Start Claude Code with the environment variable available, then give it a concrete request:

Use Postfleet to wait up to 90 seconds for a message in mailbox
b_8c2f... whose sender contains "accounts.example.com" and whose subject
contains "verification". Read the matching message and return only the
six-digit verification code. Do not call any send or draft tool.

Claude Code should call wait_for_email with:

{
  "mailbox_id": "b_8c2f...",
  "from_contains": "accounts.example.com",
  "subject_contains": "verification",
  "timeout_seconds": 90
}

The sender and subject filters are case-insensitive substring matches. The timeout can be 1 to 120 seconds and defaults to 60. If no matching message arrives, the tool returns:

{ "timed_out": true, "waited_seconds": 90 }

That is a normal result, not a reason to invent a code or read an unrelated message. The workflow can report the timeout, retry within its own finite budget, or ask the upstream system to resend.

When a message matches, wait_for_email returns the full cleaned message, including the sanitization report, classification, and any configured extraction. read_email returns the same safe detail shape when Claude starts from a message ID found by list_inbox. Neither tool returns the pre-sanitization raw body.

The sanitization report names what was removed and from which channel — hidden HTML, zero-width runs, quoted history, attachment text. Which gate closes each channel explains what those entries mean and which comprehension status a message ends up with.

The full tool arguments and response behavior are in Postfleet's MCP documentation.

Do not trust the expected sender filter#

from_contains narrows a search. It does not authenticate the sender or make the body an instruction source. Display names and visible addresses can be misleading, and a compromised sender can still deliver hostile content.

Treat the message as data even when it contains the code you requested. Ask Claude to extract a narrow value, not to follow steps written in the message. The server cleans hidden content and screens the body before it reaches read_email, but no prompt-injection filter catches every attack.

The read-only key limits the consequence of a missed attack. An email can try to tell Claude to send credentials elsewhere, yet the key cannot authorize send_email, reply_email, or draft writes. The Claude Code deny rules remove those tools from the client as well.

Email prompt injection explains the source-to-sink threat model behind this setup. Postfleet's security record publishes the current screening results and limitations.

Keep the mailbox ID out of model choice#

Put the mailbox ID in project configuration or the task input. Do not give an account-wide key to Claude and ask it to discover which mailbox looks relevant. A guessed ID should not become an authorization decision.

Mailbox binding gives the server a simple rule: this key can read this mailbox and no other. A request for a message outside that mailbox returns the same not-found response as a nonexistent ID, so the client cannot enumerate other mailboxes through object identifiers.

This also makes logs easier to interpret. Every tool call belongs to one workflow and one inbox. If several unrelated jobs share an account-wide key, a prompt mistake can cross a boundary that the server was never asked to enforce.

Add sending as a separate change#

Receiving email does not require send permission. Leave it off until a real workflow needs an external reply.

When you add sending:

  1. Issue a separate send-capable key bound to the same mailbox or to a dedicated sender mailbox.
  2. Add only the specific send or draft tool the workflow needs.
  3. Configure recipient allowlists in the Postfleet dashboard.
  4. Turn on human approval for messages that can affect a customer, vendor, or account.
  5. Test both the direct-send and pending-approval response shapes.

With approval enabled, send_email, reply_email, and send_draft return a queued result:

{
  "draft_id": "d_123...",
  "status": "pending_approval"
}

This is a successful handoff to a human, not a failed send. Claude must not retry it. An operator approves or rejects the draft in the dashboard, where the data-plane key cannot act on its own proposal.

Postfleet's direct MCP send tools do not currently accept a caller-defined client_id. Use the REST send endpoint when your application needs a stable outbound idempotency key under its own control. For higher-risk agent mail, the draft and approval path is usually the more useful boundary.

Common setup failures#

The server is pending approval#

Project-scoped .mcp.json entries require workspace trust and explicit approval. Start Claude Code in the project, accept the workspace trust dialog, then check /mcp.

The environment variable is missing#

Start Claude Code from a shell where POSTFLEET_API_KEY is exported. Do not replace the variable with a literal key in a committed file.

Read calls return 403#

Confirm that can_read is enabled and that the key is bound to the mailbox ID in the request. Do not broaden the key before checking the mismatch.

A wait always times out#

Remove one filter at a time and inspect list_inbox. Sender strings may include a display name, and the subject may have a reply prefix. Keep the timeout below 120 seconds and make sure the client and deployment permit a request to stay open that long.

A send tool is unavailable#

That is expected in the receive-only configuration. Both Claude Code and Postfleet deny it. Treat the missing capability as proof that the boundary is working.

Production checklist#

Before using the connection in an unattended workflow, verify:

  • .mcp.json contains only the endpoint and an environment-variable reference.
  • No bearer key appears in the repository, prompt, transcript, or fixture.
  • The Postfleet key is bound to one mailbox.
  • can_send is false for a receive-only worker.
  • Claude Code denies every known Postfleet write tool.
  • The prompt supplies one mailbox ID and narrow message filters.
  • A timeout produces an explicit no-result path.
  • The workflow never treats email content as policy or authorization.
  • Logs keep message bodies and verification codes out unless an operator needs them.
  • Key revocation, rotation, and workspace offboarding have an owner.

The configuration is small enough to inspect in one sitting. That is a useful property for an inbox connected to an agent: the credential, mailbox, and allowed tools should tell the same story.

Sources#

Continue reading

Put a trust boundary in front of the inbox.

Create a mailbox, issue the narrowest key the workflow needs, and inspect the cleaned message before your agent acts.