feat(proton-mail): Hermes skill — IMAP/SMTP tools via Bridge
Full Proton Mail Bridge Hermes skill with 6 tools: - proton_mail_bridge_status — check daemon health - proton_mail_list — list inbox/folder messages - proton_mail_read — read full message by UID (body+headers) - proton_mail_search — search by subject/from/body/all - proton_mail_send — send email with CC/BCC support - proton_mail_reply — reply preserving In-Reply-To/References Implementation: pure Python stdlib (imaplib + smtplib + email), no external dependencies. 22 unit tests with mocked IMAP/SMTP. Follows architecture from ARCHITECTURE.md (section 3). Per-tool auth via PROTONMAIL_ACCOUNT + PROTONMAIL_BRIDGE_PASSWORD env vars. Bridge runs on 127.0.0.1:1143 (IMAP TLS) / 127.0.0.1:1025 (SMTP STARTTLS).
This commit is contained in:
parent
f103d5f44f
commit
f8b9991207
6 changed files with 2130 additions and 0 deletions
341
skills/proton-mail/SKILL.md
Normal file
341
skills/proton-mail/SKILL.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
---
|
||||
name: proton-mail
|
||||
description: "Proton Mail via Bridge — read, send, search, and reply to emails using the local Proton Mail Bridge daemon (IMAP 127.0.0.1:1143 / SMTP 127.0.0.1:1025)."
|
||||
version: 1.0.0
|
||||
author: Trentuna / B.A. Baracus
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [proton, email, imap, smtp, bridge, productivity]
|
||||
category: productivity
|
||||
related_skills: [hermes-agent]
|
||||
tools:
|
||||
- proton_mail_bridge_status
|
||||
- proton_mail_list
|
||||
- proton_mail_read
|
||||
- proton_mail_search
|
||||
- proton_mail_send
|
||||
- proton_mail_reply
|
||||
requires_env:
|
||||
- PROTONMAIL_ACCOUNT
|
||||
- PROTONMAIL_BRIDGE_PASSWORD
|
||||
optional_env:
|
||||
- PROTONMAIL_IMAP_HOST
|
||||
- PROTONMAIL_IMAP_PORT
|
||||
- PROTONMAIL_SMTP_HOST
|
||||
- PROTONMAIL_SMTP_PORT
|
||||
---
|
||||
|
||||
# Proton Mail Bridge — Hermes Skill
|
||||
|
||||
Give any Hermes agent native access to Proton Mail via the official [Proton Mail Bridge](https://proton.me/mail/bridge).
|
||||
|
||||
The Bridge runs as a local daemon, handles all OpenPGP encryption/decryption transparently, and exposes standard IMAP (read) and SMTP (send) ports on localhost. This skill wraps those ports as Hermes tools.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌──────────────┐ IMAP 127.0.0.1:1143 (TLS) ┌─────────────────┐
|
||||
│ Hermes │ ───────────────────────────────► │ Proton Bridge │
|
||||
│ Agent │ │ (local daemon) │
|
||||
│ (this │ ◄─────────────────────────────── │ │
|
||||
│ skill) │ SMTP 127.0.0.1:1025 (STARTTLS) │ decrypts PGP │
|
||||
└──────────────┘ └────────┬────────┘
|
||||
│
|
||||
▼
|
||||
Proton Servers
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Proton Mail Bridge** installed and running:
|
||||
- Download: https://proton.me/mail/bridge
|
||||
- Linux: `protonmail-bridge --cli`
|
||||
- macOS: `brew install --cask proton-mail-bridge`
|
||||
2. **Proton Mail account** (Free or paid)
|
||||
3. **Bridge credentials** — Bridge generates a local app password (NOT your Proton password). Get it from Bridge → Settings → Account → Mailbox configuration → Show password.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `PROTONMAIL_ACCOUNT` | Yes | — | Your Proton email address (e.g. `user@proton.me`) |
|
||||
| `PROTONMAIL_BRIDGE_PASSWORD` | Yes | — | Bridge-generated app password |
|
||||
| `PROTONMAIL_IMAP_HOST` | No | `127.0.0.1` | Bridge IMAP hostname |
|
||||
| `PROTONMAIL_IMAP_PORT` | No | `1143` | Bridge IMAP port |
|
||||
| `PROTONMAIL_SMTP_HOST` | No | `127.0.0.1` | Bridge SMTP hostname |
|
||||
| `PROTONMAIL_SMTP_PORT` | No | `1025` | Bridge SMTP port |
|
||||
|
||||
## Tool Reference
|
||||
|
||||
### `proton_mail_bridge_status`
|
||||
|
||||
Check that the Proton Bridge daemon is running, reachable, and authenticated.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "proton_mail_bridge_status",
|
||||
"description": "Check Proton Mail Bridge status — running, authenticated, connected.",
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `{"status": "running", "imap": "127.0.0.1:1143", "smtp": "127.0.0.1:1025", "authenticated": true}`
|
||||
- `{"status": "stopped", "unreachable": ["IMAP 127.0.0.1:1143"]}`
|
||||
- `{"status": "unconfigured", "error": "PROTONMAIL_ACCOUNT environment variable is not set"}`
|
||||
|
||||
---
|
||||
|
||||
### `proton_mail_list`
|
||||
|
||||
List recent messages in a mailbox folder. Returns headers only (no full body).
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "proton_mail_list",
|
||||
"description": "List recent email messages in a folder.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folder": {"type": "string", "description": "Mailbox folder (INBOX, Sent, Drafts, etc.)", "default": "INBOX"},
|
||||
"limit": {"type": "integer", "description": "Max messages to return (1-100)", "default": 20}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": [
|
||||
{"uid": 42, "subject": "Meeting tomorrow", "from": "alice@example.com",
|
||||
"to": "you@proton.me", "date": "Tue, 4 Jun 2024 14:00:00 +0000"}
|
||||
],
|
||||
"folder": "INBOX",
|
||||
"total": 137
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `proton_mail_read`
|
||||
|
||||
Read a full email by UID — subject, all headers, and body text.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "proton_mail_read",
|
||||
"description": "Read a full email message including body content.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uid": {"type": "integer", "description": "UID of the message to read"},
|
||||
"folder": {"type": "string", "description": "Mailbox folder", "default": "INBOX"}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"uid": 42,
|
||||
"subject": "Meeting tomorrow",
|
||||
"from": "alice@example.com",
|
||||
"to": "you@proton.me",
|
||||
"date": "2024-06-04T14:00:00+00:00",
|
||||
"body": "Hi, let's meet at 3pm tomorrow.\n\nBest,\nAlice",
|
||||
"message_id": "<msg42@proton.me>",
|
||||
"flags": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `proton_mail_search`
|
||||
|
||||
Search emails across a mailbox by subject, sender, body, or all fields.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "proton_mail_search",
|
||||
"description": "Search email messages by query in a specific field.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query text (min 2 characters)"},
|
||||
"field": {"type": "string", "description": "Field to search (subject, from, body, or all)", "enum": ["subject", "from", "body", "all"], "default": "all"},
|
||||
"folder": {"type": "string", "description": "Mailbox folder", "default": "INBOX"},
|
||||
"limit": {"type": "integer", "description": "Max results (1-100)", "default": 20}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:** Same shape as `proton_mail_list` plus `"query": "Meeting"`.
|
||||
|
||||
---
|
||||
|
||||
### `proton_mail_send`
|
||||
|
||||
Send a new email via Bridge SMTP.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "proton_mail_send",
|
||||
"description": "Send a new email message.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {"type": "string", "description": "Recipient email(s), comma-separated"},
|
||||
"cc": {"type": "string", "description": "CC recipient(s), comma-separated"},
|
||||
"bcc": {"type": "string", "description": "BCC recipient(s), comma-separated"},
|
||||
"subject": {"type": "string", "description": "Email subject"},
|
||||
"body": {"type": "string", "description": "Email body text (plain text)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message_id": "<hermes-...@proton.me>",
|
||||
"to": "bob@example.com",
|
||||
"subject": "Hello"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `proton_mail_reply`
|
||||
|
||||
Reply to an existing email, preserving thread context (In-Reply-To and References headers).
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "proton_mail_reply",
|
||||
"description": "Reply to an existing email, preserving thread headers.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uid": {"type": "integer", "description": "UID of the email to reply to"},
|
||||
"body": {"type": "string", "description": "Reply body text"},
|
||||
"folder": {"type": "string", "description": "Mailbox folder", "default": "INBOX"}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message_id": "<hermes-reply-...@proton.me>",
|
||||
"in_reply_to": "<original-msg-id@proton.me>",
|
||||
"to": "alice@example.com",
|
||||
"subject": "Re: Original Subject"
|
||||
}
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Proton Mail Bridge
|
||||
|
||||
```bash
|
||||
# Linux (headless)
|
||||
protonmail-bridge --cli
|
||||
# Follow interactive setup — login with Proton credentials
|
||||
|
||||
# macOS
|
||||
brew install --wait proton-mail-bridge
|
||||
```
|
||||
|
||||
### 2. Get Bridge password
|
||||
|
||||
In Bridge: Settings → your account → Mailbox configuration → Show password.
|
||||
This is a **Bridge-generated local password**, not your Proton password.
|
||||
|
||||
### 3. Set environment variables
|
||||
|
||||
```bash
|
||||
export PROTONMAIL_ACCOUNT="your-email@proton.me"
|
||||
export PROTONMAIL_BRIDGE_PASSWORD="bridge-generated-password"
|
||||
```
|
||||
|
||||
Or add to your Hermes profile's `.env` at `~/.hermes/profiles/<name>/.env`:
|
||||
|
||||
```env
|
||||
PROTONMAIL_ACCOUNT=your-email@proton.me
|
||||
PROTONMAIL_BRIDGE_PASSWORD=bridge-generated-password
|
||||
```
|
||||
|
||||
### 4. Verify
|
||||
|
||||
Call `proton_mail_bridge_status` — you should see `"status": "running"` and `"authenticated": true`.
|
||||
|
||||
## Example Workflows
|
||||
|
||||
**Quick inbox check:**
|
||||
```
|
||||
proton_mail_list({"folder": "INBOX", "limit": 5})
|
||||
```
|
||||
|
||||
**Read and reply:**
|
||||
```
|
||||
1. proton_mail_list({"limit": 10})
|
||||
2. proton_mail_read({"uid": 42})
|
||||
3. proton_mail_reply({"uid": 42, "body": "Thanks, got it!"})
|
||||
```
|
||||
|
||||
**Search and respond:**
|
||||
```
|
||||
1. proton_mail_search({"query": "invoice", "field": "subject"})
|
||||
2. proton_mail_read({"uid": result.uid})
|
||||
3. proton_mail_send({"to": result.from, "subject": "Re: invoice", "body": "..."})
|
||||
```
|
||||
|
||||
**Send with CC:**
|
||||
```
|
||||
proton_mail_send({
|
||||
"to": "team@example.com",
|
||||
"cc": "manager@example.com",
|
||||
"subject": "Status Update",
|
||||
"body": "All good here."
|
||||
})
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
The skill is implemented in pure Python using standard library modules (`imaplib`, `smtplib`, `email`). No external dependencies.
|
||||
|
||||
Reference implementation at `<skill-dir>/references/tools.py`.
|
||||
|
||||
### Security
|
||||
|
||||
- **Connections are localhost-only** — Bridge listens on `127.0.0.1` only
|
||||
- **TLS on IMAP** — `IMAP4_SSL` connects to port 1143
|
||||
- **STARTTLS on SMTP** — explicit TLS negotiation on port 1025
|
||||
- **Bridge password is NOT your Proton password** — defense-in-depth via Bridge's separate auth
|
||||
- **Credential injection prevented** — `_sanitize_search_term()` strips control characters and IMAP-special chars from user input
|
||||
- **No secrets in tool calls** — credentials come from environment, never from tool arguments
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **Plain text body only** — HTML rendering is not available; HTML emails return the raw HTML source
|
||||
- **No attachment handling yet** — `proton_mail_read` returns body text only. Attachments are present in the MIME structure but not extracted separately
|
||||
- **Localhost-only** — the Bridge must run on the same machine as Hermes
|
||||
- **Bridge required** — the skill doesn't work without the Bridge daemon; it can't log into Proton API directly
|
||||
- **Single account** — one Bridge instance serves one account; multi-account requires multiple Bridge instances
|
||||
|
||||
## References
|
||||
|
||||
- [Proton Mail Bridge](https://proton.me/mail/bridge) — official download and docs
|
||||
- [openclaw-protonmail-skill](https://github.com/rvacyber/openclaw-protonmail-skill) — OpenClaw analogue (TypeScript)
|
||||
- [emersion/hydroxide](https://github.com/emersion/hydroxide) — third-party Bridge alternative for headless servers
|
||||
- [Hermes-Proton Architecture](../ARCHITECTURE.md) — full architecture document
|
||||
Loading…
Add table
Add a link
Reference in a new issue