Commit 971af77
Changed files (5)
cmd/imap/main.go
@@ -45,7 +45,7 @@ func main() {
fmt.Println(" IMAP_PORT Port number (overrides --port)")
fmt.Println(" IMAP_USE_TLS Use TLS: true/false (overrides --use-tls)")
fmt.Println()
- fmt.Println("TOOLS (8 total):")
+ fmt.Println("TOOLS (12 total):")
fmt.Println(" imap_list_folders - List all IMAP folders")
fmt.Println(" imap_list_messages - List messages in folder with pagination")
fmt.Println(" imap_read_message - Read full message content")
@@ -54,6 +54,10 @@ func main() {
fmt.Println(" imap_mark_as_read - Mark messages as read/unread")
fmt.Println(" imap_get_attachments - List message attachments")
fmt.Println(" imap_get_connection_info - Server connection info and capabilities")
+ fmt.Println(" imap_delete_message - Delete single message (requires confirmation)")
+ fmt.Println(" imap_delete_messages - Delete multiple messages (requires confirmation)")
+ fmt.Println(" imap_move_to_trash - Move messages to trash folder (safe delete)")
+ fmt.Println(" imap_expunge_folder - Permanently remove deleted messages")
fmt.Println()
fmt.Println("PROMPTS (2 total):")
fmt.Println(" email-analysis - AI-powered email content analysis")
@@ -72,6 +76,15 @@ func main() {
fmt.Println(" export IMAP_PASSWORD=app-password")
fmt.Println(" mcp-imap")
fmt.Println()
+ fmt.Println("DELETION EXAMPLES:")
+ fmt.Println(" # Safe delete (move to trash)")
+ fmt.Println(" imap_move_to_trash uid=12345")
+ fmt.Println(" imap_move_to_trash uids=[12345,12346,12347]")
+ fmt.Println()
+ fmt.Println(" # Permanent delete (requires confirmation)")
+ fmt.Println(" imap_delete_message uid=12345 confirmed=true")
+ fmt.Println(" imap_delete_messages uids=[12345,12346] confirmed=true")
+ fmt.Println()
fmt.Println("CLAUDE CODE CONFIGURATION:")
fmt.Println(" # Recommended: Use environment variables for security")
fmt.Println(" {")
pkg/imap/server.go
@@ -83,6 +83,10 @@ func NewServer(server, username, password string, port int, useTLS bool) *Server
s.RegisterTool("imap_mark_as_read", s.handleMarkAsRead)
s.RegisterTool("imap_get_attachments", s.handleGetAttachments)
s.RegisterTool("imap_get_connection_info", s.handleGetConnectionInfo)
+ s.RegisterTool("imap_delete_message", s.handleDeleteMessage)
+ s.RegisterTool("imap_delete_messages", s.handleDeleteMessages)
+ s.RegisterTool("imap_move_to_trash", s.handleMoveToTrash)
+ s.RegisterTool("imap_expunge_folder", s.handleExpungeFolder)
// Register prompts
analysisPrompt := mcp.Prompt{
@@ -726,6 +730,267 @@ Use IMAP search capabilities effectively to find the most relevant emails.`
}, nil
}
+func (s *Server) handleDeleteMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := s.ensureConnection(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
+ }
+
+ folder := "INBOX"
+ if f, ok := req.Arguments["folder"].(string); ok {
+ folder = f
+ }
+
+ var uid uint32
+ if u, ok := req.Arguments["uid"].(float64); ok {
+ uid = uint32(u)
+ } else if u, ok := req.Arguments["uid"].(string); ok {
+ if parsed, err := strconv.ParseUint(u, 10, 32); err == nil {
+ uid = uint32(parsed)
+ }
+ }
+
+ if uid == 0 {
+ return mcp.NewToolError("uid parameter is required"), nil
+ }
+
+ // Check for confirmation flag
+ confirmed := false
+ if c, ok := req.Arguments["confirmed"].(bool); ok {
+ confirmed = c
+ }
+
+ if !confirmed {
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("WARNING: This will permanently delete message UID %d from folder '%s'. To confirm, call again with 'confirmed': true", uid, folder)}},
+ }, nil
+ }
+
+ s.mu.RLock()
+ client := s.client
+ s.mu.RUnlock()
+
+ if _, err := client.Select(folder, false); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
+ }
+
+ seqset := new(imap.SeqSet)
+ seqset.AddNum(uid)
+
+ // Mark as deleted
+ operation := imap.FormatFlagsOp(imap.AddFlags, true)
+ flags := []interface{}{imap.DeletedFlag}
+
+ ch := make(chan *imap.Message, 1)
+ if err := client.UidStore(seqset, operation, flags, ch); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to mark message as deleted: %v", err)), nil
+ }
+ // Drain the channel
+ for range ch {
+ }
+
+ // Expunge to permanently delete
+ if err := client.Expunge(nil); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to expunge message: %v", err)), nil
+ }
+
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Message UID %d permanently deleted from folder '%s'", uid, folder)}},
+ }, nil
+}
+
+func (s *Server) handleDeleteMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := s.ensureConnection(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
+ }
+
+ folder := "INBOX"
+ if f, ok := req.Arguments["folder"].(string); ok {
+ folder = f
+ }
+
+ // Get UIDs array
+ var uids []uint32
+ if uidArray, ok := req.Arguments["uids"].([]interface{}); ok {
+ for _, u := range uidArray {
+ switch v := u.(type) {
+ case float64:
+ uids = append(uids, uint32(v))
+ case string:
+ if parsed, err := strconv.ParseUint(v, 10, 32); err == nil {
+ uids = append(uids, uint32(parsed))
+ }
+ }
+ }
+ }
+
+ if len(uids) == 0 {
+ return mcp.NewToolError("uids parameter is required (array of message UIDs)"), nil
+ }
+
+ // Check for confirmation flag
+ confirmed := false
+ if c, ok := req.Arguments["confirmed"].(bool); ok {
+ confirmed = c
+ }
+
+ if !confirmed {
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("WARNING: This will permanently delete %d messages from folder '%s'. UIDs: %v. To confirm, call again with 'confirmed': true", len(uids), folder, uids)}},
+ }, nil
+ }
+
+ s.mu.RLock()
+ client := s.client
+ s.mu.RUnlock()
+
+ if _, err := client.Select(folder, false); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
+ }
+
+ seqset := new(imap.SeqSet)
+ seqset.AddNum(uids...)
+
+ // Mark as deleted
+ operation := imap.FormatFlagsOp(imap.AddFlags, true)
+ flags := []interface{}{imap.DeletedFlag}
+
+ ch := make(chan *imap.Message, 10)
+ if err := client.UidStore(seqset, operation, flags, ch); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to mark messages as deleted: %v", err)), nil
+ }
+ // Drain the channel
+ for range ch {
+ }
+
+ // Expunge to permanently delete
+ if err := client.Expunge(nil); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to expunge messages: %v", err)), nil
+ }
+
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("%d messages permanently deleted from folder '%s'", len(uids), folder)}},
+ }, nil
+}
+
+func (s *Server) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := s.ensureConnection(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
+ }
+
+ folder := "INBOX"
+ if f, ok := req.Arguments["folder"].(string); ok {
+ folder = f
+ }
+
+ trashFolder := "[Gmail]/Trash"
+ if tf, ok := req.Arguments["trash_folder"].(string); ok {
+ trashFolder = tf
+ }
+
+ // Get UIDs (support both single uid and uids array)
+ var uids []uint32
+ if u, ok := req.Arguments["uid"].(float64); ok {
+ uids = append(uids, uint32(u))
+ } else if u, ok := req.Arguments["uid"].(string); ok {
+ if parsed, err := strconv.ParseUint(u, 10, 32); err == nil {
+ uids = append(uids, uint32(parsed))
+ }
+ } else if uidArray, ok := req.Arguments["uids"].([]interface{}); ok {
+ for _, u := range uidArray {
+ switch v := u.(type) {
+ case float64:
+ uids = append(uids, uint32(v))
+ case string:
+ if parsed, err := strconv.ParseUint(v, 10, 32); err == nil {
+ uids = append(uids, uint32(parsed))
+ }
+ }
+ }
+ }
+
+ if len(uids) == 0 {
+ return mcp.NewToolError("uid or uids parameter is required"), nil
+ }
+
+ s.mu.RLock()
+ client := s.client
+ s.mu.RUnlock()
+
+ if _, err := client.Select(folder, false); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
+ }
+
+ seqset := new(imap.SeqSet)
+ seqset.AddNum(uids...)
+
+ // Try to use MOVE command (Gmail supports this)
+ if err := client.UidMove(seqset, trashFolder); err != nil {
+ // Fallback to copy + mark deleted + expunge
+ if err := client.UidCopy(seqset, trashFolder); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to copy messages to trash: %v", err)), nil
+ }
+
+ // Mark original messages as deleted
+ operation := imap.FormatFlagsOp(imap.AddFlags, true)
+ flags := []interface{}{imap.DeletedFlag}
+
+ ch := make(chan *imap.Message, 10)
+ if err := client.UidStore(seqset, operation, flags, ch); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to mark messages as deleted: %v", err)), nil
+ }
+ // Drain the channel
+ for range ch {
+ }
+
+ if err := client.Expunge(nil); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to expunge messages: %v", err)), nil
+ }
+ }
+
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("%d message(s) moved to trash folder '%s'", len(uids), trashFolder)}},
+ }, nil
+}
+
+func (s *Server) handleExpungeFolder(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := s.ensureConnection(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
+ }
+
+ folder := "INBOX"
+ if f, ok := req.Arguments["folder"].(string); ok {
+ folder = f
+ }
+
+ // Check for confirmation flag
+ confirmed := false
+ if c, ok := req.Arguments["confirmed"].(bool); ok {
+ confirmed = c
+ }
+
+ if !confirmed {
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("WARNING: This will permanently remove all messages marked as deleted from folder '%s'. This action cannot be undone. To confirm, call again with 'confirmed': true", folder)}},
+ }, nil
+ }
+
+ s.mu.RLock()
+ client := s.client
+ s.mu.RUnlock()
+
+ if _, err := client.Select(folder, false); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
+ }
+
+ if err := client.Expunge(nil); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to expunge folder: %v", err)), nil
+ }
+
+ return mcp.CallToolResult{
+ Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("All deleted messages permanently removed from folder '%s'", folder)}},
+ }, nil
+}
+
func (s *Server) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
test/integration/main_test.go
@@ -271,7 +271,7 @@ func TestAllServers(t *testing.T) {
{
BinaryName: "mcp-imap",
Args: []string{"--server", "example.com", "--username", "test", "--password", "test"},
- ExpectedTools: []string{"imap_list_folders", "imap_list_messages", "imap_get_connection_info"},
+ ExpectedTools: []string{"imap_list_folders", "imap_list_messages", "imap_get_connection_info", "imap_delete_message", "imap_move_to_trash"},
ExpectedServers: "imap",
MinResources: 0, // No static resources (connection fails gracefully)
},
CLAUDE.md
@@ -499,7 +499,7 @@ The Signal MCP server is now **production-ready** with:
### **โ
Complete Feature Implementation**
-**All 8 Tools Implemented:**
+**All 12 Tools Implemented:**
- โ
`imap_list_folders` - List all IMAP folders (INBOX, Sent, Drafts, etc.)
- โ
`imap_list_messages` - List messages in folder with pagination and filtering
- โ
`imap_read_message` - Read full message content with headers, body, and metadata
@@ -508,6 +508,10 @@ The Signal MCP server is now **production-ready** with:
- โ
`imap_mark_as_read` - Mark messages as read/unread with flag management
- โ
`imap_get_attachments` - List message attachments (placeholder implementation)
- โ
`imap_get_connection_info` - Server connection status and capabilities
+- โ
`imap_delete_message` - Delete single message with confirmation requirement
+- โ
`imap_delete_messages` - Bulk delete multiple messages with confirmation
+- โ
`imap_move_to_trash` - Safe delete by moving to trash folder
+- โ
`imap_expunge_folder` - Permanently remove deleted messages from folder
**All 2 Prompts Implemented:**
- โ
`email-analysis` - AI-powered email content analysis (sentiment, summary, action items)
@@ -554,11 +558,12 @@ The Signal MCP server is now **production-ready** with:
### **๐ Ready for Production Use**
The IMAP MCP server is now **production-ready** with:
-- **Complete Functionality**: 8/8 tools and 2/2 prompts fully implemented
+- **Complete Functionality**: 12/12 tools and 2/2 prompts fully implemented
- **High Performance**: Sub-10ms startup, minimal memory footprint
- **Comprehensive Testing**: Integration tests with graceful error handling
- **Proper Documentation**: Updated help text and usage examples
- **Security Compliance**: TLS encryption and credential management via environment variables
+- **Safe Deletion**: Confirmation prompts and trash folder support for AI-assisted email management
**Dependencies Added:**
- `github.com/emersion/go-imap v1.2.1` - Professional IMAP client library
@@ -599,8 +604,40 @@ mcp-imap
}
```
+### **๐ค AI-Assisted Email Management**
+
+**The IMAP MCP server now enables powerful AI-driven email cleanup and management:**
+
+**Safe Deletion Workflow:**
+1. **Search**: Use `imap_search_messages` to find emails matching criteria
+2. **Review**: Use `imap_read_message` to analyze specific emails
+3. **Safe Delete**: Use `imap_move_to_trash` to move to Gmail Trash (reversible)
+4. **Permanent**: Use `imap_delete_message` with confirmation for permanent removal
+
+**AI Assistant Use Cases:**
+- *"Delete all promotional emails older than 6 months"*
+- *"Remove unread newsletters from last year"*
+- *"Clean up emails from specific senders"*
+- *"Delete emails matching certain subject patterns"*
+- *"Find and remove large attachments to free space"*
+
+**Safety Features:**
+- **Confirmation Required**: All permanent deletions require `confirmed: true`
+- **Trash by Default**: `imap_move_to_trash` for safe, reversible deletion
+- **Bulk Operations**: Handle multiple messages efficiently with UIDs array
+- **Warning Messages**: Clear warnings about permanent actions
+
+**Example AI Workflow:**
+```
+1. Search: "Find all emails from newsletters older than 1 year"
+2. Review: "Show me 5 examples of what would be deleted"
+3. Safe Delete: "Move these 150 newsletter emails to trash"
+4. Confirm: "If everything looks good, permanently delete from trash"
+```
+
**Security Notes:**
- Use app passwords for Gmail (not main account password)
- Consider environment variables for credential management
- All connections use TLS encryption by default
-- Credentials are not logged or stored persistently by the server
\ No newline at end of file
+- Credentials are not logged or stored persistently by the server
+- Deletion operations require explicit confirmation for safety
\ No newline at end of file
README.md
@@ -151,6 +151,10 @@ Connect to IMAP email servers like Gmail, Migadu, and other providers for email
- `imap_mark_as_read` - Mark messages as read/unread
- `imap_get_attachments` - List message attachments
- `imap_get_connection_info` - Server connection status and capabilities
+- `imap_delete_message` - Delete single message (requires confirmation)
+- `imap_delete_messages` - Delete multiple messages (requires confirmation)
+- `imap_move_to_trash` - Move messages to trash folder (safe delete)
+- `imap_expunge_folder` - Permanently remove deleted messages
**Prompts:**
- `email-analysis` - AI-powered email content analysis
@@ -162,6 +166,9 @@ Connect to IMAP email servers like Gmail, Migadu, and other providers for email
- Environment variable credential management
- Thread-safe connection handling
- Rich message parsing and formatting
+- Safe email deletion with confirmation prompts
+- Move to trash vs permanent delete options
+- Bulk operations for managing multiple messages
**Usage:**
```bash