Ad

Our DNA is written in Swift
Jump

Post

Many moons ago I had the idea that I would like for an agentic system to be able to access my e-mail servers. That came to me when I automated collecting incoming invoices for my company with a make.com workflow. But that didn’t amount to much until OpenClaw hit the world’s stage.

The second key project besides SwiftMail was SwiftMCP. Those two together were the first link I had between ChatGPT and my e-mail server. SwiftMCP exposes an OpenAPI interface which you could connect to custom GPTs to query and read your emails. And of course as local MCP server you could connect to it with MCP from local AI client apps as well.

And then OpenClaw entered my life and – almost everything changed. Finally I could pursue my vision from two years ago of having a company of AI Agents within 5 years. All of the sudden I have agents working for me and some of them could do amazing work if only they could read my e-mail.

But OpenClaw doesn’t do MCP. The problem with LLM tools in general is that an agentic session needs to add a lot of schema information into the context. With every server you have functions for which you need many a tool’s JSON schema and keep that in context so that the LLM can choose to emit tool calls to them in the right format.

LLMs do have trained knowledge how to work with scripts and CLIs though. That’s why Peter Steinberger’s main body of work in 2025 revolved around building CLIs for most things. And this trend culminated in the Agent Skills standard. The concept being that the agent gets a short description of the skill into memory and if he needs it he can read up on it in SKILL.md. And most skills also come with scripts that the agent then can call. And since good scripts or CLIs have a --help option, LLMs can easily figure out how to call them for the intended purpose.

This works so well, that I myself made a ton of skills for myself, about half of which I put up on GitHub. Python is the de factor leader in terms of portability for skills.

So I needed/wanted to have a CLI for e-mail. The problem for that though is that CLIs are stateless: you run them, they produce a result and then they terminate. For one-shot email queries this might be ok, but usually you have to do multiple things in a work flow: list new emails, download some, flag them as seen or junk, trash them, or archive them. With a classic CLI you would have to connect, login, do the operation, logout and disconnect every time.

That felt silly to me.

Also I wanted to make use of IMAP IDLE where you get a notification whenever something changes in your Inbox. So I wanted to have a process that you kick off and then remains resident, keeping open connections to multiple email servers. So how can you have both things at the same time?

The answer is this: the background process keeping the connections is a daemon. And the CLI connects to it locally and fast.

This is what I am giving you today. Post has a simple post command that communicates via a local bonjour+tcp MCP transport to postd running on the same machine. The daemon is the one where you configure the server connections. The CLI is the one that lets you interact with the servers.

🏤Post – Secure.Agentic.Post.Office

Post stores server configurations in ~/.post.json. You can include credentials directly in the config file or store them securely in a private macOS Keychain.

Option 1: Credentials in Config File

Simple but less secure. Good for development or non-sensitive accounts.

{
  "servers": {
    "personal": {
      "credentials": {
        "host": "imap.gmail.com",
        "port": 993,
        "username": "you@gmail.com",
        "password": "your-app-password"
      }
    }
  }
}

Option 2: Credentials in Keychain (Recommended)

Post automatically creates and manages a private keychain at ~/.post.keychain-db. The keychain is encrypted with a passphrase derived from your Mac’s hardware UUID, so it’s locked to your specific machine.

{
  "servers": {
    "personal": {
      "command": "imap.gmail.com:993"
    }
  }
}

Then add the credentials to the keychain:

# Post will prompt for username and password
post keychain add personal --host imap.gmail.com --port 993

Your credentials are now stored securely. Post will retrieve them automatically when needed.

Basic Operations

For the CLI to do anything the daemon needs to be running. Default mode is for it to run in background, with --foreground you can have it in foreground as well.

# Daemon Commands
postd start
postd status
postd stop
postd reload

Emails are identified by the combination of server, mailbox and UID. Please note that UID might become invalid and if you move an email to a different mailbox it also gets a new UID. Some of the commands default to --mailbox INBOX.

List Unseen Emails

# List newest 10 messages in INBOX
post list --server personal --limit 10

Fetch an Email as Markdown

Post converts HTML emails to clean, readable markdown.

# Fetch UID 12345 and display as markdown
post fetch 12345 --server personal

The output includes headers, body text converted from HTML, and a list of attachments. With the --json option you get beautify JSON for all commands.

Downloading Attachments

# Download first attachment from UID 12345 to current directory
post attachment --server personal --uid 12345

# Or specify which attachment you want and a different output directory
post attachment --filename invoice.pdf --server personal --uid 12345 --out ~/Downloads

Post will save each attachment with its original filename.

Making Drafts

With post Agents can also draft emails – but not send them. You can simply create a rich text email from a markdown file:

# Create a markdown file
cat > email.md << 'EOF'
# Project Update

Hi team,

Here's the **weekly update**:

- Feature A: *completed*
- Feature B: in progress
- Feature C: planned for next sprint

Check the [documentation](https://example.com) for details.

Thanks!
EOF

# Draft the email
post draft --to colleague@example.com \
  --subject "Weekly Update" \
  --body email.md

# Or use inline markdown
post draft --to colleague@example.com \
  --subject "Quick note" \
  --body "Thanks for the **great** work on the project!"

Post will use the markdown for the text/plain header and generate a nice text/html version.

Enabling IDLE for Real-Time Email Processing

IDLE is an IMAP extension that keeps a connection open and notifies you instantly when new mail arrives. This is perfect for automation.

Configure IDLE in .post.json

Add idle: true and specify a handler script:

{
  "servers": {
    "personal": {
      "command": "imap.gmail.com:993",
      "idle": true,
      "idleMailbox": "INBOX",
      "command": "python3 /Users/you/process-email.py"
    }
  }
}

Create a Simple Handler Script

Your handler receives email data as JSON on stdin:

#!/usr/bin/env python3
import json
import sys

# Read email notification
email = json.load(sys.stdin)

subject = email['headers'].get('subject', 'No subject')
sender = email['headers'].get('from', 'Unknown')

print(f"New email from {sender}: {subject}")

# Exit 0 = success, 1 = error, 2 = intentionally skipped
sys.exit(0)

The daemon will watch all configured IDLE mailboxes and call your handler script whenever new mail arrives. The script receives full email details (headers, markdown body, attachments) as structured JSON.

Handler JSON Format

Here’s what your script receives:

{
  "uid": 12345,
  "date": "2026-02-27T10:30:00Z",
  "from": ["sender@example.com"],
  "to": ["you@gmail.com"],
  "subject": "Your email subject",
  "headers": {
    "from": "Sender Name <sender@example.com>",
    "subject": "Your email subject",
    "list-id": "<newsletter.example.com>",
    ...
  },
  "markdown": "# Email body\n\nConverted to markdown...",
  "attachments": [
    {
      "filename": "document.pdf",
      "contentType": "application/pdf",
      "size": 102400
    }
  ]
}

Your handler can then decide what to do: archive it, forward it, extract data, trigger a workflow, etc.

Multiple Scope Support

You can easily have multiple different agents in OpenClaw, some might be sandboxed, some are not. And you don’t want every agent to be able to access all configured email servers. That’s the reason why Postd has the (optional) ability to set up multiple API keys that can only access certain servers.

Setting Up Two Scoped API Tokens in Post

This walkthrough creates two different API keys, each limited to specific mail server IDs, and enables strict token enforcement in postd.

1. Configure your servers

Define at least two server IDs in ~/.post.json:

{
  "servers": {
    "work": {},
    "personal": {}
  }
}

Set credentials as usual (keychain or inline credentials).

2. Create two scoped tokens

Create one token for work only:

post api-key create --servers work

Create one token for personal only:

post api-key create --servers personal

Each command prints a UUID token. Save both.

You can inspect what exists with:

post api-key list

3. Start/restart daemon so scopes are loaded

postd now preloads API-key scopes at startup. Restart it after key changes:

postd restart
# or:
postd stop && postd start

If keychain prompts appear, authorize postd once (prefer Always Allow). Contrary to the private keychain (which is protected by a very long password), the tokens are stored in your login keychain. This way you know if an unknown process tries to access them. Because of this, you have to approve postd once for every token you set up.

Now you can add the POST_API_KEY specific to each agent in their .env. Having an API key in the environment hides the commands for configuration in the CLI. So if you want to make changes you’d have to unset the key. But note, if there’s at least one token configured, then all commands accessing the daemon need you to specify the token.

  • If at least one API key exists, MCP access requires a token.
  • No token: request fails (API key is required.).
  • Unknown token: request fails (Invalid API key.).
  • Valid token, wrong server: request fails (not authorized for server).

You can rotate tokens by just deleting and recreating them.
post api-key delete --token <TOKEN>
post api-key create --servers work,personal

Working on a Mailroom Skill

Large corporations have a mail room, where all new post is being delivered, before it gets sorted and routed through the company. With Post you can now have the same.

I am working on a mail-room skill that already does a lot of sorting for me. With prompt injections being the worry of the day, some initial sorting can be done programmatically. Most newsletter and notification emails have certain header fields or sender addresses by which they can be recognized.

What It Does

The mail-room skill automatically processes incoming emails in real-time as they arrive. Think of it as a smart assistant sitting in your inbox, sorting mail before you even see it.

Programmatic Sorting (Rule-Based)

The first line of defense uses simple pattern matching – no AI needed:

Newsletters & Mailing Lists are identified by standard email headers like List-ID or List-Unsubscribe. These get filed away to a separate archive, keeping your inbox clean.

Service Notifications from known companies (GitHub, Amazon, Apple, xAI, LinkedIn, etc.) are recognized by their sender addresses and automatically archived to a notifications folder.

This rule-based sorting is fast, deterministic, and immune to prompt injection attacks – it’s just checking headers and email addresses against a whitelist.

Secure AI-Powered Categorization

For emails that don’t match the simple rules, the skill uses AI to handle the trickier cases. But I am not having my OpenClaw handle this. Instead I have a simple agent that I built with the OpenAI Agents SDK do the categorization. This agent has neither knowledge nor access to any of my personal information. I only has a tool with which it can request a markdown representation of a file attachment.

The Goal: Inbox Zero

After processing:

  • Newsletters → Archived, removed from inbox
  • Service notifications → Archived, removed from inbox
  • Spam → Moved to Junk folder
  • Personal & business mail → Stays in inbox (unread, for your attention)

All archived emails are saved as readable markdown files, organized by sender, with full metadata preserved. Nothing is permanently deleted – everything can be recovered if needed.

The goal is simple: only the emails that need your attention stay in your inbox. Everything else is automatically sorted, archived, and out of your way.

Now about these newsletters and notifications: having them as markdown on my hard drive allows me to have another process that picks up interesting items and compiles a personalized news briefing based on what really interests me.

Conclusion

Besides its utility, Post has quickly become my go-to project for furthering development on SwiftMail, SwiftMCP and SwiftText. The latter I haven’t even mentioned until now: It’s a collection of functions to get markdown from PDFs, HTML files or DOCX files, and more.

In the combination of these threes and working on real-life application of agentically handling my email proved to be an extremely fertile ground for what to improve and which functions are useful. I haven’t even touched on search for emails. I plan on putting it on homebrew together with a skill on clawhub as well, soon.

I could write a book about this all, but for now, this article must suffice. I’m happy to talk with you about your usage scenarios, get your bug reports or pull requests on GitHub.


Categories: Administrative

Comments are closed.