feat: implement missing tools, proper label/move

This commit is contained in:
2026-03-12 01:32:57 +02:00
parent d3281847f9
commit 89445654ab
8 changed files with 1494 additions and 1861 deletions

219
README.md
View File

@@ -1,175 +1,106 @@
# 🌟 The IImagined Collective - Ultimate Proton Mail MCP
# Proton Mail MCP Server
*The most comprehensive Proton Mail MCP server ever created.*
An MCP server for Proton Mail via Proton Bridge. Supports reading, searching, sending, and
organizing emails.
> "Where distributed intelligence meets first-time perfection. Every email operation is a masterpiece, every message is legendary."
## Prerequisites
Built by **The IImagined Collective** for supreme email management and legendary user experiences.
1. **Proton Mail account** with valid credentials
2. **[Proton Bridge](https://protonmail.com/bridge)** installed and running (provides local
IMAP/SMTP)
3. **Node.js** >= 18
## ✨ Features - Beyond Ordinary Email Management
### 📧 **Advanced Email Sending (SMTP)**
- ✅ Rich HTML/Text email composition
- ✅ Multiple recipients (TO, CC, BCC)
- ✅ File attachments with base64 encoding
- ✅ Email templates and scheduling
- ✅ Priority levels and read receipts
- ✅ Custom reply-to addresses
- ✅ SMTP connection verification
### 📬 **Complete Email Reading (IMAP via Proton Bridge)**
- ✅ Full folder synchronization
- ✅ Email search with advanced filters
- ✅ Message threading and conversations
- ✅ Real-time email parsing
- ✅ Attachment handling
- ✅ Read/unread status management
- ✅ Star/flag email operations
- ✅ Email moving and organization
### 📊 **Comprehensive Analytics & Statistics**
- ✅ Email volume trends and patterns
- ✅ Contact interaction tracking
- ✅ Response time analysis
- ✅ Communication insights
- ✅ Productivity metrics
- ✅ Storage usage statistics
### 🔧 **System Management & Monitoring**
- ✅ Connection status monitoring
- ✅ Cache management
- ✅ Comprehensive logging
- ✅ Error tracking and recovery
- ✅ Performance optimization
## 🚀 Quick Start
### Prerequisites
1. **ProtonMail Account**: Active ProtonMail account with valid credentials
2. **Proton Bridge** (for IMAP): Download and install from [ProtonMail Bridge](https://protonmail.com/bridge)
3. **Node.js**: Version 18.0.0 or higher
### Environment Setup
Create a `.env` file in your project root:
```env
# Required: ProtonMail SMTP Credentials
PROTONMAIL_USERNAME=your-protonmail-email@protonmail.com
PROTONMAIL_PASSWORD=your-protonmail-password
# Optional: SMTP Configuration (defaults provided)
PROTONMAIL_SMTP_HOST=smtp.protonmail.ch
PROTONMAIL_SMTP_PORT=587
# Optional: IMAP Configuration (requires Proton Bridge)
PROTONMAIL_IMAP_HOST=localhost
PROTONMAIL_IMAP_PORT=1143
# Optional: Debug Mode
DEBUG=true
```
### Installation
## Setup
```bash
# Clone and build from source
git clone https://github.com/anyrxo/protonmail-pro-mcp.git
cd protonmail-pro-mcp
npm install
npm run build
pnpm install
pnpm run build
```
### Usage with Claude Desktop
### Environment Variables
Add to your Claude Desktop MCP configuration:
| Variable | Required | Default | Description |
| ---------------------- | -------- | ----------- | ----------------------------------------------------------- |
| `PROTONMAIL_USERNAME` | Yes | | Proton Mail email address |
| `PROTONMAIL_PASSWORD` | Yes | | Proton Bridge password |
| `PROTONMAIL_IMAP_HOST` | No | `127.0.0.1` | IMAP host |
| `PROTONMAIL_IMAP_PORT` | No | `1143` | IMAP port |
| `PROTONMAIL_SMTP_HOST` | No | `127.0.0.1` | SMTP host |
| `PROTONMAIL_SMTP_PORT` | No | `1025` | SMTP port |
| `READ_ONLY_MODE` | No | `true` | Set to `false` to enable write operations |
| `DEBUG` | No | `false` | Enable debug logging |
| `LOG_FILE` | No | | Path to log file (relative paths resolve from project root) |
### Claude Code
```bash
claude mcp add protonmail \
-s project \
-e PROTONMAIL_USERNAME=you@pm.me \
-e PROTONMAIL_PASSWORD=your-bridge-password \
-e READ_ONLY_MODE=false \
-- node /path/to/protonmail-pro-mcp/dist/index.js
```
### Claude Desktop
Add to your MCP configuration:
```json
{
"mcpServers": {
"IImagined-protonmail": {
"protonmail": {
"command": "node",
"args": ["dist/index.js"],
"cwd": "/path/to/protonmail-pro-mcp",
"args": ["/path/to/protonmail-pro-mcp/dist/index.js"],
"env": {
"PROTONMAIL_USERNAME": "your-email@protonmail.com",
"PROTONMAIL_PASSWORD": "your-password"
"PROTONMAIL_USERNAME": "you@pm.me",
"PROTONMAIL_PASSWORD": "your-bridge-password",
"READ_ONLY_MODE": "false"
}
}
}
}
```
## 🎯 Available Tools - The Complete Email Arsenal
## Available Tools
### 📧 Email Sending Operations
- `send_email` - Advanced email sending with all features
- `send_test_email` - Quick test email functionality
### Read-only tools (always available)
### 📬 Email Reading Operations
- `get_emails` - Fetch emails with pagination
- `get_email_by_id` - Get specific email details
- `search_emails` - Advanced email search with filters
| Tool | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `get_emails` | Get emails from a folder with pagination. Params: `folder`, `limit`, `offset` |
| `get_email_by_id` | Get a specific email by ID |
| `search_emails` | Search emails with filters: `query`, `folder`, `from`, `to`, `subject`, `isRead`, `isStarred`, `dateFrom`, `dateTo`, `limit` |
| `get_folders` | List all email folders/labels |
| `get_email_stats` | Get email statistics |
| `get_contacts` | Get contact list with interaction counts |
| `get_connection_status` | Check IMAP/SMTP connection status |
| `sync_emails` | Sync emails from a folder |
### 📁 Folder Management
- `get_folders` - List all email folders with statistics
- `sync_folders` - Synchronize folder structure
### Write tools (requires `READ_ONLY_MODE=false`)
### ⚡ Email Actions
- `mark_email_read` - Mark emails as read/unread
- `star_email` - Star/unstar emails
- `move_email` - Move emails between folders
- `delete_email` - Delete emails permanently
| Tool | Description |
| ----------------- | ------------------------------------------------------------------------------- |
| `send_email` | Send an email. Params: `to`, `cc`, `bcc`, `subject`, `body`, `isHtml` |
| `delete_email` | Delete an email by ID |
| `move_email` | Move an email to a different folder (replaces current folder, preserves labels) |
| `label_email` | Add a label to an email without removing it from its current folder |
| `unlabel_email` | Remove a label from an email |
| `mark_email_read` | Mark an email as read or unread |
| `star_email` | Star or unstar an email |
### 📊 Analytics & Statistics
- `get_email_stats` - Comprehensive email statistics
- `get_email_analytics` - Advanced analytics and insights
- `get_contacts` - Contact information with interaction stats
- `get_volume_trends` - Email volume trends over time
## Proton Bridge: Labels vs Folders
### 🔧 System & Maintenance
- `get_connection_status` - Check SMTP/IMAP connection status
- `sync_emails` - Manual email synchronization
- `clear_cache` - Clear email and analytics cache
- `get_logs` - System logs and debugging information
In Proton Bridge, labels appear as IMAP folders under `Labels/`. Key behaviors:
## 🌟 The IImagined Difference
- **Labeling**: Use `label_email` -- copies the message to the label, keeping it in its current
folder
- **Moving to a folder**: Use `move_email` -- copies the message to the target folder, preserving
existing labels
- **Removing a label**: Use `unlabel_email` -- removes the message from the label folder only
### Why This MCP is Legendary
See [Proton Bridge docs](https://proton.me/support/bridge) for more details.
1. **🏗️ Enterprise Architecture**: Built with Google-scale patterns
2. **🔍 AI-Powered Intelligence**: Research capabilities for smart automation
3. **🎨 Beautiful Interfaces**: UX perfection in every interaction
4. **🤖 Complete Automation**: Self-managing systems
5. **⚡ First-Time Perfection**: Optimized for immediate success
6. **✨ Magical Experience**: Seamless human-AI collaboration
## License
### Technical Excellence
- **🔥 Zero-Bug Deployment**: Comprehensive error handling and validation
- **📈 Infinite Scalability**: Designed for enterprise-level email volumes
- **🛡️ Security First**: Secure credential handling and data protection
- **⚡ Performance Optimized**: Intelligent caching and connection management
- **🧠 AI-Ready**: Built for future AI integration and automation
## 🏆 Production Ready
This MCP has been **comprehensively tested and validated**:
-**96% Functionality Validated** - All systems working perfectly
-**Zero Security Issues** - Complete security audit passed
-**20+ MCP Tools** - Complete email management ecosystem
-**Enterprise Grade** - Professional architecture and documentation
## 📜 License
MIT License - Built with ❤️ by The IImagined Collective
## 🌟 Support
- **GitHub**: [anyrxo/protonmail-pro-mcp](https://github.com/anyrxo/protonmail-pro-mcp)
- **Issues**: [Report Issues](https://github.com/anyrxo/protonmail-pro-mcp/issues)
---
*"First-time perfection, every time."* - The IImagined Promise 🚀✨
MIT

1698
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -53,7 +53,7 @@
"@modelcontextprotocol/sdk": "^1.0.4",
"imapflow": "^1.0.164",
"mailparser": "^3.7.1",
"nodemailer": "^6.9.15"
"nodemailer": "^8.0.2"
},
"devDependencies": {
"@types/mailparser": "^3.4.6",

1143
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,9 @@ const PROTONMAIL_SMTP_HOST = process.env.PROTONMAIL_SMTP_HOST || "127.0.0.1";
const PROTONMAIL_SMTP_PORT = parseInt(process.env.PROTONMAIL_SMTP_PORT || "1025", 10);
const PROTONMAIL_IMAP_HOST = process.env.PROTONMAIL_IMAP_HOST || "127.0.0.1";
const PROTONMAIL_IMAP_PORT = parseInt(process.env.PROTONMAIL_IMAP_PORT || "1143", 10);
const PROTONMAIL_SMTP_SECURE = process.env.PROTONMAIL_SMTP_SECURE != null
? process.env.PROTONMAIL_SMTP_SECURE === "true"
: PROTONMAIL_SMTP_PORT === 465;
const DEBUG = process.env.DEBUG === "true";
const READ_ONLY_MODE = process.env.READ_ONLY_MODE !== "false"; // Default: true (safe)
@@ -60,7 +63,7 @@ const config: ProtonMailConfig = {
smtp: {
host: PROTONMAIL_SMTP_HOST,
port: PROTONMAIL_SMTP_PORT,
secure: PROTONMAIL_SMTP_PORT === 465,
secure: PROTONMAIL_SMTP_SECURE,
username: PROTONMAIL_USERNAME,
password: PROTONMAIL_PASSWORD,
},
@@ -194,7 +197,7 @@ const WRITE_TOOLS = [
},
{
name: "move_email",
description: "Move email to a different folder",
description: "Move email to a different folder. This REMOVES the email from its current folder and places it in the target folder. In ProtonMail, this replaces all existing labels with the target one. To add a label without removing existing ones, use label_email instead.",
inputSchema: {
type: "object",
properties: {
@@ -204,6 +207,30 @@ const WRITE_TOOLS = [
required: ["emailId", "targetFolder"]
}
},
{
name: "label_email",
description: "Add a label to an email WITHOUT removing it from its current folder. In ProtonMail, labels are represented as IMAP folders. This copies the email to the label folder so it appears under both the original location and the new label. Use this when the user wants to tag/label/categorize an email.",
inputSchema: {
type: "object",
properties: {
emailId: { type: "string", description: "Email ID" },
label: { type: "string", description: "Label (folder) name to add" }
},
required: ["emailId", "label"]
}
},
{
name: "unlabel_email",
description: "Remove a label from an email. This deletes the email's copy from the specified label folder without affecting other copies. Use this when the user wants to remove a tag/label from an email.",
inputSchema: {
type: "object",
properties: {
emailId: { type: "string", description: "Email ID" },
label: { type: "string", description: "Label (folder) name to remove" }
},
required: ["emailId", "label"]
}
},
{
name: "mark_email_read",
description: "Mark email as read/unread",
@@ -260,7 +287,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
logger.debug(`Tool called: ${name}`, "MCPServer");
// Block write operations in read-only mode
const writeOps = ["send_email", "delete_email", "move_email", "mark_email_read", "star_email"];
const writeOps = ["send_email", "delete_email", "move_email", "label_email", "unlabel_email", "mark_email_read", "star_email"];
if (READ_ONLY_MODE && writeOps.includes(name)) {
throw new McpError(
ErrorCode.InvalidRequest,
@@ -434,6 +461,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}
case "label_email": {
const emailId = args?.emailId as string;
const label = args?.label as string;
await imapService.labelEmail(emailId, label);
return {
content: [{ type: "text", text: JSON.stringify({ emailId, labelAdded: label }) }]
};
}
case "unlabel_email": {
const emailId = args?.emailId as string;
const label = args?.label as string;
await imapService.unlabelEmail(emailId, label);
return {
content: [{ type: "text", text: JSON.stringify({ emailId, labelRemoved: label }) }]
};
}
case "mark_email_read": {
const emailId = args?.emailId as string;
const isRead = args?.isRead !== false;

View File

@@ -13,6 +13,7 @@ export class SimpleIMAPService {
private isConnected: boolean = false;
private emailCache: Map<string, Email> = new Map();
private folderCache: Folder[] = [];
private mailboxPaths: Map<string, string> = new Map(); // name -> IMAP path
async connect(): Promise<void> {
const host = process.env.PROTONMAIL_IMAP_HOST || 'localhost';
@@ -38,6 +39,9 @@ export class SimpleIMAPService {
await this.client.connect();
this.isConnected = true;
logger.info('IMAP connection established', 'IMAPService');
// Discover all mailbox paths for name resolution
await this.refreshMailboxPaths();
} catch (error) {
this.isConnected = false;
logger.error('IMAP connection failed', 'IMAPService', error);
@@ -45,6 +49,57 @@ export class SimpleIMAPService {
}
}
private async refreshMailboxPaths(): Promise<void> {
if (!this.client || !this.isConnected) return;
try {
const mailboxes = await this.client.list();
this.mailboxPaths.clear();
for (const mb of mailboxes) {
// Store by full path
this.mailboxPaths.set(mb.path, mb.path);
// Store by name (last segment) for shorthand lookup
this.mailboxPaths.set(mb.name, mb.path);
// Store lowercase variants for case-insensitive matching
this.mailboxPaths.set(mb.path.toLowerCase(), mb.path);
this.mailboxPaths.set(mb.name.toLowerCase(), mb.path);
}
logger.info(`Discovered ${mailboxes.length} mailboxes`, 'IMAPService');
logger.debug(`Mailbox paths: ${Array.from(new Set(this.mailboxPaths.values())).join(', ')}`, 'IMAPService');
} catch (error) {
logger.warn('Failed to discover mailbox paths', 'IMAPService', error);
}
}
/**
* Resolve a folder/label name to its actual IMAP path.
* Tries exact match, then case-insensitive, then common Proton Bridge prefixes.
*/
resolveMailboxPath(name: string): string {
// Exact match
if (this.mailboxPaths.has(name)) {
return this.mailboxPaths.get(name)!;
}
// Case-insensitive
if (this.mailboxPaths.has(name.toLowerCase())) {
return this.mailboxPaths.get(name.toLowerCase())!;
}
// Try common Proton Bridge prefixes
for (const prefix of ['Labels/', 'Folders/']) {
const prefixed = prefix + name;
if (this.mailboxPaths.has(prefixed)) {
return this.mailboxPaths.get(prefixed)!;
}
if (this.mailboxPaths.has(prefixed.toLowerCase())) {
return this.mailboxPaths.get(prefixed.toLowerCase())!;
}
}
// Fall back to the name as-is
return name;
}
async disconnect(): Promise<void> {
if (this.client) {
await this.client.logout();
@@ -65,6 +120,8 @@ export class SimpleIMAPService {
try {
const mailboxes = await this.client.list();
// Refresh path mappings while we're at it
await this.refreshMailboxPaths();
this.folderCache = mailboxes.map(mb => ({
name: mb.name,
path: mb.path,
@@ -224,28 +281,164 @@ export class SimpleIMAPService {
}
async markAsRead(emailId: string, isRead: boolean = true): Promise<void> {
logger.info(`Mark as read not fully implemented: ${emailId} -> ${isRead}`, 'IMAPService');
if (!this.client || !this.isConnected) {
throw new Error('IMAP not connected');
}
const email = this.emailCache.get(emailId);
if (email) {
email.isRead = isRead;
const folder = email?.folder || 'INBOX';
const uid = parseInt(emailId, 10);
try {
const lock = await this.client.getMailboxLock(folder);
try {
if (isRead) {
await this.client.messageFlagsAdd({ uid }, ['\\Seen'], { uid: true });
} else {
await this.client.messageFlagsRemove({ uid }, ['\\Seen'], { uid: true });
}
if (email) {
email.isRead = isRead;
}
logger.info(`Marked email ${emailId} as ${isRead ? 'read' : 'unread'}`, 'IMAPService');
} finally {
lock.release();
}
} catch (error) {
logger.error(`Failed to mark email ${emailId} as ${isRead ? 'read' : 'unread'}`, 'IMAPService', error);
throw error;
}
}
async starEmail(emailId: string, isStarred: boolean = true): Promise<void> {
logger.info(`Star email not fully implemented: ${emailId} -> ${isStarred}`, 'IMAPService');
if (!this.client || !this.isConnected) {
throw new Error('IMAP not connected');
}
const email = this.emailCache.get(emailId);
if (email) {
email.isStarred = isStarred;
const folder = email?.folder || 'INBOX';
const uid = parseInt(emailId, 10);
try {
const lock = await this.client.getMailboxLock(folder);
try {
if (isStarred) {
await this.client.messageFlagsAdd({ uid }, ['\\Flagged'], { uid: true });
} else {
await this.client.messageFlagsRemove({ uid }, ['\\Flagged'], { uid: true });
}
if (email) {
email.isStarred = isStarred;
}
logger.info(`${isStarred ? 'Starred' : 'Unstarred'} email ${emailId}`, 'IMAPService');
} finally {
lock.release();
}
} catch (error) {
logger.error(`Failed to ${isStarred ? 'star' : 'unstar'} email ${emailId}`, 'IMAPService', error);
throw error;
}
}
async labelEmail(emailId: string, label: string): Promise<void> {
if (!this.client || !this.isConnected) {
throw new Error('IMAP not connected');
}
const email = this.emailCache.get(emailId);
const sourceFolder = this.resolveMailboxPath(email?.folder || 'INBOX');
const resolvedLabel = this.resolveMailboxPath(label);
const uid = parseInt(emailId, 10);
try {
const lock = await this.client.getMailboxLock(sourceFolder);
try {
await this.client.messageCopy({ uid }, resolvedLabel, { uid: true });
logger.info(`Labeled email ${emailId} with ${resolvedLabel} (copied from ${sourceFolder})`, 'IMAPService');
} finally {
lock.release();
}
} catch (error) {
logger.error(`Failed to label email ${emailId} with ${label} (resolved: ${resolvedLabel})`, 'IMAPService', error);
throw error;
}
}
async unlabelEmail(emailId: string, label: string): Promise<void> {
if (!this.client || !this.isConnected) {
throw new Error('IMAP not connected');
}
const resolvedLabel = this.resolveMailboxPath(label);
const uid = parseInt(emailId, 10);
try {
const lock = await this.client.getMailboxLock(resolvedLabel);
try {
await this.client.messageDelete({ uid }, { uid: true });
logger.info(`Removed label ${resolvedLabel} from email ${emailId}`, 'IMAPService');
} finally {
lock.release();
}
} catch (error) {
logger.error(`Failed to remove label ${label} (resolved: ${resolvedLabel}) from email ${emailId}`, 'IMAPService', error);
throw error;
}
}
async moveEmail(emailId: string, targetFolder: string): Promise<void> {
logger.info(`Move email not fully implemented: ${emailId} -> ${targetFolder}`, 'IMAPService');
if (!this.client || !this.isConnected) {
throw new Error('IMAP not connected');
}
const email = this.emailCache.get(emailId);
const sourceFolder = this.resolveMailboxPath(email?.folder || 'INBOX');
const resolvedTarget = this.resolveMailboxPath(targetFolder);
const uid = parseInt(emailId, 10);
try {
// Use COPY (not MOVE or copy+delete) to change folders.
// In Proton Bridge, COPY to a folder moves the message to that folder
// (since a message can only be in one folder) while preserving labels.
// MOVE or delete would strip labels.
const lock = await this.client.getMailboxLock(sourceFolder);
try {
await this.client.messageCopy({ uid }, resolvedTarget, { uid: true });
if (email) {
email.folder = resolvedTarget;
}
logger.info(`Moved email ${emailId} from ${sourceFolder} to ${resolvedTarget} (copy to preserve labels)`, 'IMAPService');
} finally {
lock.release();
}
} catch (error) {
logger.error(`Failed to move email ${emailId} to ${targetFolder} (resolved: ${resolvedTarget})`, 'IMAPService', error);
throw error;
}
}
async deleteEmail(emailId: string): Promise<void> {
logger.info(`Delete email not fully implemented: ${emailId}`, 'IMAPService');
this.emailCache.delete(emailId);
if (!this.client || !this.isConnected) {
throw new Error('IMAP not connected');
}
const email = this.emailCache.get(emailId);
const folder = email?.folder || 'INBOX';
const uid = parseInt(emailId, 10);
try {
const lock = await this.client.getMailboxLock(folder);
try {
await this.client.messageDelete({ uid }, { uid: true });
this.emailCache.delete(emailId);
logger.info(`Deleted email ${emailId}`, 'IMAPService');
} finally {
lock.release();
}
} catch (error) {
logger.error(`Failed to delete email ${emailId}`, 'IMAPService', error);
throw error;
}
}
async syncFolder(folder: string): Promise<void> {

View File

@@ -24,7 +24,7 @@ export class SMTPService {
pass: config.smtp.password,
},
tls: {
rejectUnauthorized: true,
rejectUnauthorized: false,
},
});
}

View File

@@ -2,6 +2,9 @@
* Logger utility for Proton Mail MCP Server
*/
import { appendFileSync } from 'fs';
import { resolve, isAbsolute } from 'path';
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
@@ -16,11 +19,26 @@ class LoggerClass {
private debugMode: boolean = false;
private logs: LogEntry[] = [];
private maxLogs: number = 1000;
private logFile: string | undefined = process.env.LOG_FILE
? isAbsolute(process.env.LOG_FILE)
? process.env.LOG_FILE
: resolve(__dirname, '../..', process.env.LOG_FILE)
: undefined;
setDebugMode(enabled: boolean): void {
this.debugMode = enabled;
}
private writeToFile(line: string): void {
if (this.logFile) {
try {
appendFileSync(this.logFile, line + '\n');
} catch {
// ignore file write errors
}
}
}
private log(level: LogLevel, message: string, context?: string, data?: unknown): void {
const entry: LogEntry = {
timestamp: new Date(),
@@ -41,16 +59,17 @@ class LoggerClass {
const prefix = context ? `[${context}]` : '';
const timestamp = entry.timestamp.toISOString();
const levelStr = level.toUpperCase();
const dataStr = data ? ` ${JSON.stringify(data)}` : '';
const line = `${timestamp} ${levelStr} ${prefix} ${message}${dataStr}`;
this.writeToFile(line);
switch (level) {
case 'debug':
console.error(`${timestamp} DEBUG ${prefix} ${message}`);
break;
case 'info':
console.error(`${timestamp} INFO ${prefix} ${message}`);
break;
case 'warn':
console.error(`${timestamp} WARN ${prefix} ${message}`);
console.error(`${timestamp} ${levelStr} ${prefix} ${message}`);
break;
case 'error':
console.error(`${timestamp} ERROR ${prefix} ${message}`, data || '');