Commit dfce622
cmd/imap/main.go
@@ -159,7 +159,7 @@ func main() {
}
// Create and start server
- imapServer := imap.NewServer(*server, *username, *password, *port, *useTLS)
+ imapServer := imap.New(*server, *username, *password, *port, *useTLS)
ctx := context.Background()
if err := imapServer.Run(ctx); err != nil {
pkg/imap/server.go
@@ -10,13 +10,13 @@ import (
"sync"
"time"
- "github.com/emersion/go-imap"
+ goimap "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/xlgmokha/mcp/pkg/mcp"
)
-type Server struct {
- *mcp.Server
+// ImapOperations provides IMAP email operations
+type ImapOperations struct {
mu sync.RWMutex
client *client.Client
server string
@@ -65,350 +65,295 @@ type ConnectionInfo struct {
ServerInfo map[string]interface{} `json:"server_info"`
}
-func NewServer(server, username, password string, port int, useTLS bool) *Server {
- s := &Server{
- Server: mcp.NewServer("imap", "IMAP email server for reading and managing emails", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{}),
+// NewImapOperations creates a new ImapOperations helper
+func NewImapOperations(server, username, password string, port int, useTLS bool) *ImapOperations {
+ return &ImapOperations{
server: server,
username: username,
password: password,
port: port,
useTLS: useTLS,
}
-
- s.registerTools()
-
- // Register prompts
- analysisPrompt := mcp.Prompt{
- Name: "email-analysis",
- Description: "Analyze email content with AI insights including sentiment, summary, and key points",
- }
- searchPrompt := mcp.Prompt{
- Name: "email-search",
- Description: "Contextual email search with AI-powered insights and filtering",
- }
- s.RegisterPrompt(analysisPrompt, s.handleAnalysisPrompt)
- s.RegisterPrompt(searchPrompt, s.handleSearchPrompt)
-
- return s
}
-// ListTools returns all available IMAP email management tools
-func (s *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "imap_list_folders",
- Description: "List all folders in the email account (INBOX, Sent, Drafts, etc.)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{},
+// New creates a new IMAP MCP server
+func New(server, username, password string, port int, useTLS bool) *mcp.Server {
+ imap := NewImapOperations(server, username, password, port, useTLS)
+ builder := mcp.NewServerBuilder("imap-server", "1.0.0")
+
+ // Add imap_list_folders tool
+ builder.AddTool(mcp.NewTool("imap_list_folders", "List all folders in the email account (INBOX, Sent, Drafts, etc.)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleListFolders(req)
+ }))
+
+ // Add imap_list_messages tool
+ builder.AddTool(mcp.NewTool("imap_list_messages", "List messages in a folder with optional pagination and filtering", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
},
- },
- {
- Name: "imap_list_messages",
- Description: "List messages in a folder with optional pagination and filtering",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of messages to return (default: 20)",
- "minimum": 1,
- "maximum": 100,
- },
- "offset": map[string]interface{}{
- "type": "integer",
- "description": "Number of messages to skip (for pagination) (default: 0)",
- "minimum": 0,
- },
- "unread_only": map[string]interface{}{
- "type": "boolean",
- "description": "Show only unread messages (default: false)",
- },
- },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of messages to return (default: 20)",
+ "minimum": 1,
+ "maximum": 100,
},
- },
- {
- Name: "imap_read_message",
- Description: "Read the full content of a specific email message",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "uid": map[string]interface{}{
- "type": "integer",
- "description": "Message UID",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- "mark_as_read": map[string]interface{}{
- "type": "boolean",
- "description": "Mark message as read when reading (default: false)",
- },
- },
- "required": []string{"uid"},
+ "offset": map[string]interface{}{
+ "type": "integer",
+ "description": "Number of messages to skip (for pagination) (default: 0)",
+ "minimum": 0,
+ },
+ "unread_only": map[string]interface{}{
+ "type": "boolean",
+ "description": "Show only unread messages (default: false)",
},
},
- {
- Name: "imap_search_messages",
- Description: "Search for messages by content, sender, subject, or other criteria",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "Search query text",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder to search in (default: INBOX)",
- },
- "search_type": map[string]interface{}{
- "type": "string",
- "description": "Type of search (subject, from, body, all) (default: all)",
- "enum": []string{"subject", "from", "to", "body", "all"},
- },
- "since_date": map[string]interface{}{
- "type": "string",
- "description": "Search messages since this date (YYYY-MM-DD format)",
- },
- "before_date": map[string]interface{}{
- "type": "string",
- "description": "Search messages before this date (YYYY-MM-DD format)",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of results (default: 50)",
- "minimum": 1,
- "maximum": 200,
- },
- },
- "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleListMessages(req)
+ }))
+
+ // Add imap_read_message tool
+ builder.AddTool(mcp.NewTool("imap_read_message", "Read the full content of a specific email message", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "uid": map[string]interface{}{
+ "type": "integer",
+ "description": "Message UID",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
+ },
+ "mark_as_read": map[string]interface{}{
+ "type": "boolean",
+ "description": "Mark message as read when reading (default: false)",
},
},
- {
- Name: "imap_get_folder_stats",
- Description: "Get statistics for a folder (total messages, unread count, etc.)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- },
+ "required": []string{"uid"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleReadMessage(req)
+ }))
+
+ // Add imap_search_messages tool
+ builder.AddTool(mcp.NewTool("imap_search_messages", "Search for messages by content, sender, subject, or other criteria", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query": map[string]interface{}{
+ "type": "string",
+ "description": "Search query text",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder to search in (default: INBOX)",
+ },
+ "search_type": map[string]interface{}{
+ "type": "string",
+ "description": "Type of search (subject, from, body, all) (default: all)",
+ "enum": []string{"subject", "from", "to", "body", "all"},
+ },
+ "since_date": map[string]interface{}{
+ "type": "string",
+ "description": "Search messages since this date (YYYY-MM-DD format)",
+ },
+ "before_date": map[string]interface{}{
+ "type": "string",
+ "description": "Search messages before this date (YYYY-MM-DD format)",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of results (default: 50)",
+ "minimum": 1,
+ "maximum": 200,
},
},
- {
- Name: "imap_mark_as_read",
- Description: "Mark one or more messages as read or unread",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "uid": map[string]interface{}{
- "type": "integer",
- "description": "Message UID to mark",
- },
- "uids": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "integer",
- },
- "description": "Array of message UIDs to mark (alternative to single uid)",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- "read": map[string]interface{}{
- "type": "boolean",
- "description": "Mark as read (true) or unread (false) (default: true)",
- },
- },
+ "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleSearchMessages(req)
+ }))
+
+ // Add imap_get_folder_stats tool
+ builder.AddTool(mcp.NewTool("imap_get_folder_stats", "Get statistics for a specific folder (message counts, sizes, etc.)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
},
},
- {
- Name: "imap_get_attachments",
- Description: "List attachments for a message",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "uid": map[string]interface{}{
- "type": "integer",
- "description": "Message UID",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- },
- "required": []string{"uid"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleGetFolderStats(req)
+ }))
+
+ // Add imap_mark_as_read tool
+ builder.AddTool(mcp.NewTool("imap_mark_as_read", "Mark messages as read or unread", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "uids": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{"type": "integer"},
+ "description": "Array of message UIDs to mark",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
+ },
+ "mark_as_read": map[string]interface{}{
+ "type": "boolean",
+ "description": "True to mark as read, false to mark as unread (default: true)",
},
},
- {
- Name: "imap_get_connection_info",
- Description: "Get information about the IMAP server connection and capabilities",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{},
+ "required": []string{"uids"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleMarkAsRead(req)
+ }))
+
+ // Add imap_get_attachments tool
+ builder.AddTool(mcp.NewTool("imap_get_attachments", "List attachments for a specific message (placeholder)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "uid": map[string]interface{}{
+ "type": "integer",
+ "description": "Message UID",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
},
},
- {
- Name: "imap_delete_message",
- Description: "Delete a single message (requires confirmation for safety)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "uid": map[string]interface{}{
- "type": "integer",
- "description": "Message UID to delete",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- "confirmed": map[string]interface{}{
- "type": "boolean",
- "description": "Confirmation that you want to permanently delete this message",
- },
- },
- "required": []string{"uid", "confirmed"},
+ "required": []string{"uid"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleGetAttachments(req)
+ }))
+
+ // Add imap_get_connection_info tool
+ builder.AddTool(mcp.NewTool("imap_get_connection_info", "Get IMAP server connection information and capabilities", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleGetConnectionInfo(req)
+ }))
+
+ // Add imap_delete_message tool
+ builder.AddTool(mcp.NewTool("imap_delete_message", "Delete a single message (requires confirmation for safety)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "uid": map[string]interface{}{
+ "type": "integer",
+ "description": "Message UID to delete",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
+ },
+ "confirmed": map[string]interface{}{
+ "type": "boolean",
+ "description": "Must be true to confirm permanent deletion",
},
},
- {
- Name: "imap_delete_messages",
- Description: "Delete multiple messages in bulk (requires confirmation for safety)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "uids": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "integer",
- },
- "description": "Array of message UIDs to delete",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name (default: INBOX)",
- },
- "confirmed": map[string]interface{}{
- "type": "boolean",
- "description": "Confirmation that you want to permanently delete these messages",
- },
- },
- "required": []string{"uids", "confirmed"},
+ "required": []string{"uid", "confirmed"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleDeleteMessage(req)
+ }))
+
+ // Add imap_delete_messages tool
+ builder.AddTool(mcp.NewTool("imap_delete_messages", "Delete multiple messages at once (requires confirmation for safety)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "uids": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{"type": "integer"},
+ "description": "Array of message UIDs to delete",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
+ },
+ "confirmed": map[string]interface{}{
+ "type": "boolean",
+ "description": "Must be true to confirm permanent deletion",
},
},
- {
- Name: "imap_move_to_trash",
- Description: "Move messages to trash folder (safer alternative to permanent deletion)",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "uid": map[string]interface{}{
- "type": "integer",
- "description": "Message UID to move to trash",
- },
- "uids": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "integer",
- },
- "description": "Array of message UIDs to move to trash (alternative to single uid)",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Source folder name (default: INBOX)",
- },
- "trash_folder": map[string]interface{}{
- "type": "string",
- "description": "Trash folder name (default: Trash or [Gmail]/Trash)",
- },
- },
+ "required": []string{"uids", "confirmed"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleDeleteMessages(req)
+ }))
+
+ // Add imap_move_to_trash tool
+ builder.AddTool(mcp.NewTool("imap_move_to_trash", "Move messages to trash folder (safe, reversible deletion)", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "uids": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{"type": "integer"},
+ "description": "Array of message UIDs to move to trash",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Source folder name (default: INBOX)",
+ },
+ "trash_folder": map[string]interface{}{
+ "type": "string",
+ "description": "Trash folder name (default: Trash)",
},
},
- {
- Name: "imap_expunge_folder",
- Description: "Permanently remove messages marked for deletion from a folder",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Folder name to expunge (default: INBOX)",
- },
- "confirmed": map[string]interface{}{
- "type": "boolean",
- "description": "Confirmation that you want to permanently expunge deleted messages",
- },
- },
- "required": []string{"confirmed"},
+ "required": []string{"uids"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleMoveToTrash(req)
+ }))
+
+ // Add imap_expunge_folder tool
+ builder.AddTool(mcp.NewTool("imap_expunge_folder", "Permanently remove all messages marked for deletion from a folder", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Folder name (default: INBOX)",
+ },
+ "confirmed": map[string]interface{}{
+ "type": "boolean",
+ "description": "Must be true to confirm permanent expunge",
},
},
- }
-}
+ "required": []string{"confirmed"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return imap.handleExpungeFolder(req)
+ }))
-// registerTools registers all IMAP tools with the server
-func (s *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := s.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "imap_list_folders":
- handler = s.handleListFolders
- case "imap_list_messages":
- handler = s.handleListMessages
- case "imap_read_message":
- handler = s.handleReadMessage
- case "imap_search_messages":
- handler = s.handleSearchMessages
- case "imap_get_folder_stats":
- handler = s.handleGetFolderStats
- case "imap_mark_as_read":
- handler = s.handleMarkAsRead
- case "imap_get_attachments":
- handler = s.handleGetAttachments
- case "imap_get_connection_info":
- handler = s.handleGetConnectionInfo
- case "imap_delete_message":
- handler = s.handleDeleteMessage
- case "imap_delete_messages":
- handler = s.handleDeleteMessages
- case "imap_move_to_trash":
- handler = s.handleMoveToTrash
- case "imap_expunge_folder":
- handler = s.handleExpungeFolder
- default:
- continue
- }
- s.RegisterToolWithDefinition(tool, handler)
- }
+ // Add prompts
+ builder.AddPrompt(mcp.NewPrompt("email-analysis", "Analyze email content with AI insights including sentiment, summary, and key points", []mcp.PromptArgument{}, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ return imap.handleAnalysisPrompt(req)
+ }))
+
+ builder.AddPrompt(mcp.NewPrompt("email-search", "Contextual email search with AI-powered insights and filtering", []mcp.PromptArgument{}, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ return imap.handleSearchPrompt(req)
+ }))
+
+ return builder.Build()
}
-func (s *Server) connect() error {
- if s.connected && s.client != nil {
+
+// Helper methods for ImapOperations
+
+func (imap *ImapOperations) connect() error {
+ if imap.connected && imap.client != nil {
return nil
}
- address := fmt.Sprintf("%s:%d", s.server, s.port)
+ address := fmt.Sprintf("%s:%d", imap.server, imap.port)
var c *client.Client
var err error
- if s.useTLS {
- c, err = client.DialTLS(address, &tls.Config{ServerName: s.server})
+ if imap.useTLS {
+ c, err = client.DialTLS(address, &tls.Config{ServerName: imap.server})
} else {
c, err = client.Dial(address)
if err == nil {
- if err = c.StartTLS(&tls.Config{ServerName: s.server}); err != nil {
+ if err = c.StartTLS(&tls.Config{ServerName: imap.server}); err != nil {
log.Printf("STARTTLS failed: %v", err)
}
}
@@ -418,46 +363,46 @@ func (s *Server) connect() error {
return fmt.Errorf("failed to connect to IMAP server: %v", err)
}
- if err = c.Login(s.username, s.password); err != nil {
+ if err = c.Login(imap.username, imap.password); err != nil {
c.Close()
return fmt.Errorf("authentication failed: %v", err)
}
- s.mu.Lock()
- s.client = c
- s.connected = true
- s.mu.Unlock()
+ imap.mu.Lock()
+ imap.client = c
+ imap.connected = true
+ imap.mu.Unlock()
return nil
}
-func (s *Server) ensureConnection() error {
- s.mu.RLock()
- connected := s.connected
- s.mu.RUnlock()
+func (imap *ImapOperations) ensureConnection() error {
+ imap.mu.RLock()
+ connected := imap.connected
+ imap.mu.RUnlock()
if !connected {
- return s.connect()
+ return imap.connect()
}
return nil
}
-func (s *Server) handleListFolders(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleListFolders(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.mu.RUnlock()
- mailboxes := make(chan *imap.MailboxInfo, 10)
+ mailboxes := make(chan *goimap.MailboxInfo, 10)
done := make(chan error, 1)
go func() {
done <- client.List("", "*", mailboxes)
}()
- var mailboxList []*imap.MailboxInfo
+ var mailboxList []*goimap.MailboxInfo
for m := range mailboxes {
mailboxList = append(mailboxList, m)
}
@@ -494,8 +439,8 @@ func (s *Server) handleListFolders(req mcp.CallToolRequest) (mcp.CallToolResult,
}, nil
}
-func (s *Server) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -509,9 +454,9 @@ func (s *Server) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult
limit = int(l)
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.mu.RUnlock()
mbox, err := client.Select(folder, true)
if err != nil {
@@ -530,13 +475,13 @@ func (s *Server) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult
from = mbox.Messages - uint32(limit) + 1
}
- seqset := new(imap.SeqSet)
+ seqset := new(goimap.SeqSet)
seqset.AddRange(from, to)
- messages := make(chan *imap.Message, 10)
+ messages := make(chan *goimap.Message, 10)
done := make(chan error, 1)
go func() {
- done <- client.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Size}, messages)
+ done <- client.Fetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags, goimap.FetchRFC822Size}, messages)
}()
var messageList []MessageInfo
@@ -590,8 +535,8 @@ func (s *Server) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult
}, nil
}
-func (s *Server) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -613,24 +558,24 @@ func (s *Server) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult,
return mcp.NewToolError("uid parameter is required"), nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.mu.RUnlock()
if _, err := client.Select(folder, true); err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
}
- seqset := new(imap.SeqSet)
+ seqset := new(goimap.SeqSet)
seqset.AddNum(uid)
- messages := make(chan *imap.Message, 1)
+ messages := make(chan *goimap.Message, 1)
done := make(chan error, 1)
go func() {
- done <- client.UidFetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Size, imap.FetchRFC822}, messages)
+ done <- client.UidFetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags, goimap.FetchRFC822Size, goimap.FetchRFC822}, messages)
}()
- var message *imap.Message
+ var message *goimap.Message
for msg := range messages {
message = msg
break
@@ -645,7 +590,7 @@ func (s *Server) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult,
}
var body string
- if r := message.GetBody(&imap.BodySectionName{}); r != nil {
+ if r := message.GetBody(&goimap.BodySectionName{}); r != nil {
if bodyBytes, err := io.ReadAll(r); err == nil {
body = string(bodyBytes)
}
@@ -692,8 +637,8 @@ func (s *Server) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult,
}, nil
}
-func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -707,15 +652,15 @@ func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResu
return mcp.NewToolError("query parameter is required"), nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.mu.RUnlock()
if _, err := client.Select(folder, false); err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
}
- criteria := &imap.SearchCriteria{
+ criteria := &goimap.SearchCriteria{
Text: []string{query},
}
@@ -742,13 +687,13 @@ func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResu
}, nil
}
- seqset := new(imap.SeqSet)
+ seqset := new(goimap.SeqSet)
seqset.AddNum(uids...)
- messages := make(chan *imap.Message, 10)
+ messages := make(chan *goimap.Message, 10)
done := make(chan error, 1)
go func() {
- done <- client.UidFetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Size}, messages)
+ done <- client.UidFetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags, goimap.FetchRFC822Size}, messages)
}()
var messageList []MessageInfo
@@ -802,8 +747,8 @@ func (s *Server) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResu
}, nil
}
-func (s *Server) handleGetFolderStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleGetFolderStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -812,9 +757,9 @@ func (s *Server) handleGetFolderStats(req mcp.CallToolRequest) (mcp.CallToolResu
folder = f
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.mu.RUnlock()
mbox, err := client.Select(folder, true)
if err != nil {
@@ -834,8 +779,8 @@ func (s *Server) handleGetFolderStats(req mcp.CallToolRequest) (mcp.CallToolResu
}, nil
}
-func (s *Server) handleMarkAsRead(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleMarkAsRead(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -862,27 +807,27 @@ func (s *Server) handleMarkAsRead(req mcp.CallToolRequest) (mcp.CallToolResult,
markAsRead = mar
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.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 := new(goimap.SeqSet)
seqset.AddNum(uid)
- var operation imap.StoreItem
- flags := []interface{}{imap.SeenFlag}
+ var operation goimap.StoreItem
+ flags := []interface{}{goimap.SeenFlag}
if markAsRead {
- operation = imap.FormatFlagsOp(imap.AddFlags, true)
+ operation = goimap.FormatFlagsOp(goimap.AddFlags, true)
} else {
- operation = imap.FormatFlagsOp(imap.RemoveFlags, true)
+ operation = goimap.FormatFlagsOp(goimap.RemoveFlags, true)
}
- ch := make(chan *imap.Message, 1)
+ ch := make(chan *goimap.Message, 1)
if err := client.UidStore(seqset, operation, flags, ch); err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to update message flags: %v", err)), nil
}
@@ -900,23 +845,23 @@ func (s *Server) handleMarkAsRead(req mcp.CallToolRequest) (mcp.CallToolResult,
}, nil
}
-func (s *Server) handleGetAttachments(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (imap *ImapOperations) handleGetAttachments(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
return mcp.CallToolResult{
Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "Attachment handling not yet implemented"}},
}, nil
}
-func (s *Server) handleGetConnectionInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- s.mu.RLock()
- connected := s.connected
- client := s.client
- s.mu.RUnlock()
+func (imap *ImapOperations) handleGetConnectionInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ imap.mu.RLock()
+ connected := imap.connected
+ client := imap.client
+ imap.mu.RUnlock()
info := ConnectionInfo{
- Server: s.server,
- Username: s.username,
- Port: s.port,
- UseTLS: s.useTLS,
+ Server: imap.server,
+ Username: imap.username,
+ Port: imap.port,
+ UseTLS: imap.useTLS,
Connected: connected,
Capabilities: []string{},
ServerInfo: make(map[string]interface{}),
@@ -941,15 +886,15 @@ func (s *Server) handleGetConnectionInfo(req mcp.CallToolRequest) (mcp.CallToolR
}, nil
}
-func (s *Server) handleAnalysisPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
- return s.getEmailAnalysisPrompt(req.Arguments)
+func (imap *ImapOperations) handleAnalysisPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ return imap.getEmailAnalysisPrompt(req.Arguments)
}
-func (s *Server) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
- return s.getEmailSearchPrompt(req.Arguments)
+func (imap *ImapOperations) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ return imap.getEmailSearchPrompt(req.Arguments)
}
-func (s *Server) getEmailAnalysisPrompt(args map[string]interface{}) (mcp.GetPromptResult, error) {
+func (imap *ImapOperations) getEmailAnalysisPrompt(args map[string]interface{}) (mcp.GetPromptResult, error) {
prompt := `Analyze the provided email content and provide insights including:
1. **Sentiment Analysis**: Overall tone (positive, negative, neutral)
@@ -978,7 +923,7 @@ Please provide your analysis in a structured format with clear sections.`
}, nil
}
-func (s *Server) getEmailSearchPrompt(args map[string]interface{}) (mcp.GetPromptResult, error) {
+func (imap *ImapOperations) getEmailSearchPrompt(args map[string]interface{}) (mcp.GetPromptResult, error) {
query, ok := args["query"].(string)
if !ok {
return mcp.GetPromptResult{}, fmt.Errorf("query parameter is required")
@@ -1020,8 +965,8 @@ 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 {
+func (imap *ImapOperations) handleDeleteMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -1055,22 +1000,22 @@ func (s *Server) handleDeleteMessage(req mcp.CallToolRequest) (mcp.CallToolResul
}, nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.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 := new(goimap.SeqSet)
seqset.AddNum(uid)
// Mark as deleted
- operation := imap.FormatFlagsOp(imap.AddFlags, true)
- flags := []interface{}{imap.DeletedFlag}
+ operation := goimap.FormatFlagsOp(goimap.AddFlags, true)
+ flags := []interface{}{goimap.DeletedFlag}
- ch := make(chan *imap.Message, 1)
+ ch := make(chan *goimap.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
}
@@ -1088,8 +1033,8 @@ func (s *Server) handleDeleteMessage(req mcp.CallToolRequest) (mcp.CallToolResul
}, nil
}
-func (s *Server) handleDeleteMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleDeleteMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -1129,22 +1074,22 @@ func (s *Server) handleDeleteMessages(req mcp.CallToolRequest) (mcp.CallToolResu
}, nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.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 := new(goimap.SeqSet)
seqset.AddNum(uids...)
// Mark as deleted
- operation := imap.FormatFlagsOp(imap.AddFlags, true)
- flags := []interface{}{imap.DeletedFlag}
+ operation := goimap.FormatFlagsOp(goimap.AddFlags, true)
+ flags := []interface{}{goimap.DeletedFlag}
- ch := make(chan *imap.Message, 10)
+ ch := make(chan *goimap.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
}
@@ -1162,8 +1107,8 @@ func (s *Server) handleDeleteMessages(req mcp.CallToolRequest) (mcp.CallToolResu
}, nil
}
-func (s *Server) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -1202,15 +1147,15 @@ func (s *Server) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult,
return mcp.NewToolError("uid or uids parameter is required"), nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.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 := new(goimap.SeqSet)
seqset.AddNum(uids...)
// Try to use MOVE command (Gmail supports this)
@@ -1221,10 +1166,10 @@ func (s *Server) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// Mark original messages as deleted
- operation := imap.FormatFlagsOp(imap.AddFlags, true)
- flags := []interface{}{imap.DeletedFlag}
+ operation := goimap.FormatFlagsOp(goimap.AddFlags, true)
+ flags := []interface{}{goimap.DeletedFlag}
- ch := make(chan *imap.Message, 10)
+ ch := make(chan *goimap.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
}
@@ -1242,8 +1187,8 @@ func (s *Server) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult,
}, nil
}
-func (s *Server) handleExpungeFolder(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- if err := s.ensureConnection(); err != nil {
+func (imap *ImapOperations) handleExpungeFolder(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := imap.ensureConnection(); err != nil {
return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
}
@@ -1264,9 +1209,9 @@ func (s *Server) handleExpungeFolder(req mcp.CallToolRequest) (mcp.CallToolResul
}, nil
}
- s.mu.RLock()
- client := s.client
- s.mu.RUnlock()
+ imap.mu.RLock()
+ client := imap.client
+ imap.mu.RUnlock()
if _, err := client.Select(folder, false); err != nil {
return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
@@ -1281,14 +1226,14 @@ func (s *Server) handleExpungeFolder(req mcp.CallToolRequest) (mcp.CallToolResul
}, nil
}
-func (s *Server) Close() error {
- s.mu.Lock()
- defer s.mu.Unlock()
+func (imap *ImapOperations) Close() error {
+ imap.mu.Lock()
+ defer imap.mu.Unlock()
- if s.client != nil {
- s.client.Close()
- s.client = nil
+ if imap.client != nil {
+ imap.client.Close()
+ imap.client = nil
}
- s.connected = false
+ imap.connected = false
return nil
}
pkg/maildir/server.go
@@ -17,9 +17,8 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server implements the Maildir MCP server
-type Server struct {
- *mcp.Server
+// MaildirOperations provides maildir email analysis operations
+type MaildirOperations struct {
allowedPaths []string
}
@@ -65,10 +64,8 @@ type ContactInfo struct {
IsOutgoing bool `json:"is_outgoing"`
}
-// New creates a new Maildir MCP server
-func New(allowedPaths []string) *Server {
- server := mcp.NewServer("maildir-server", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
+// NewMaildirOperations creates a new MaildirOperations helper
+func NewMaildirOperations(allowedPaths []string) *MaildirOperations {
// Normalize and validate allowed paths
normalizedPaths := make([]string, len(allowedPaths))
for i, path := range allowedPaths {
@@ -79,228 +76,151 @@ func New(allowedPaths []string) *Server {
normalizedPaths[i] = filepath.Clean(absPath)
}
- maildirServer := &Server{
- Server: server,
+ return &MaildirOperations{
allowedPaths: normalizedPaths,
}
-
- // Register all maildir tools, prompts, resources, and roots
- maildirServer.registerTools()
- maildirServer.registerPrompts()
- maildirServer.registerResources()
- maildirServer.registerRoots()
-
- return maildirServer
}
-
-// ListTools returns all available Maildir tools
-func (ms *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "maildir_scan_folders",
- Description: "Scan and list all folders in the Maildir structure",
- InputSchema: map[string]interface{}{
- "type": "object",
+// New creates a new Maildir MCP server
+func New(allowedPaths []string) *mcp.Server {
+ maildir := NewMaildirOperations(allowedPaths)
+ builder := mcp.NewServerBuilder("maildir-server", "1.0.0")
+
+ // Add maildir_scan_folders tool
+ builder.AddTool(mcp.NewTool("maildir_scan_folders", "Scan and list all folders in the Maildir structure", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleScanFolders(req)
+ }))
+
+ // Add maildir_list_messages tool
+ builder.AddTool(mcp.NewTool("maildir_list_messages", "List messages in a specific folder with optional pagination and filtering", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "The folder name to list messages from",
},
- },
- {
- Name: "maildir_list_messages",
- Description: "List messages in a specific folder with optional pagination and filtering",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "folder": map[string]interface{}{
- "type": "string",
- "description": "The folder name to list messages from",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of messages to return",
- "minimum": 1,
- "default": 50,
- },
- "offset": map[string]interface{}{
- "type": "integer",
- "description": "Number of messages to skip",
- "minimum": 0,
- "default": 0,
- },
- "unread_only": map[string]interface{}{
- "type": "boolean",
- "description": "Only return unread messages",
- "default": false,
- },
- },
- "required": []string{"folder"},
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of messages to return",
+ "minimum": 1,
+ "default": 50,
},
- },
- {
- Name: "maildir_read_message",
- Description: "Read the full content of a specific message",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "message_id": map[string]interface{}{
- "type": "string",
- "description": "The unique message ID",
- },
- },
- "required": []string{"message_id"},
+ "offset": map[string]interface{}{
+ "type": "integer",
+ "description": "Number of messages to skip",
+ "minimum": 0,
+ "default": 0,
+ },
+ "unread_only": map[string]interface{}{
+ "type": "boolean",
+ "description": "Only return unread messages",
+ "default": false,
},
},
- {
- Name: "maildir_search_messages",
- Description: "Search for messages across all folders using various criteria",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "Search query string",
- },
- "sender": map[string]interface{}{
- "type": "string",
- "description": "Filter by sender email or name",
- },
- "subject": map[string]interface{}{
- "type": "string",
- "description": "Filter by subject line",
- },
- "folder": map[string]interface{}{
- "type": "string",
- "description": "Limit search to specific folder",
- },
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of results",
- "minimum": 1,
- "default": 50,
- },
- },
+ "required": []string{"folder"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleListMessages(req)
+ }))
+
+ // Add maildir_read_message tool
+ builder.AddTool(mcp.NewTool("maildir_read_message", "Read the full content of a specific message", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "message_id": map[string]interface{}{
+ "type": "string",
+ "description": "The unique message ID",
},
},
- {
- Name: "maildir_get_thread",
- Description: "Get all messages in a conversation thread",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "message_id": map[string]interface{}{
- "type": "string",
- "description": "Message ID to find thread for",
- },
- },
- "required": []string{"message_id"},
+ "required": []string{"message_id"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleReadMessage(req)
+ }))
+
+ // Add maildir_search_messages tool
+ builder.AddTool(mcp.NewTool("maildir_search_messages", "Search for messages across all folders using various criteria", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query": map[string]interface{}{
+ "type": "string",
+ "description": "Search query string",
+ },
+ "sender": map[string]interface{}{
+ "type": "string",
+ "description": "Filter by sender email or name",
+ },
+ "subject": map[string]interface{}{
+ "type": "string",
+ "description": "Filter by subject line",
+ },
+ "folder": map[string]interface{}{
+ "type": "string",
+ "description": "Limit search to specific folder",
+ },
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of results",
+ "minimum": 1,
+ "default": 50,
},
},
- {
- Name: "maildir_analyze_contacts",
- Description: "Analyze contact frequency and relationship data from email history",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "limit": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of contacts to analyze",
- "minimum": 1,
- "default": 100,
- },
- },
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleSearchMessages(req)
+ }))
+
+ // Add maildir_get_thread tool
+ builder.AddTool(mcp.NewTool("maildir_get_thread", "Get all messages in a conversation thread", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "message_id": map[string]interface{}{
+ "type": "string",
+ "description": "Message ID to find thread for",
},
},
- {
- Name: "maildir_get_statistics",
- Description: "Get comprehensive statistics about the maildir including message counts, storage usage, and activity patterns",
- InputSchema: map[string]interface{}{
- "type": "object",
+ "required": []string{"message_id"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleGetThread(req)
+ }))
+
+ // Add maildir_analyze_contacts tool
+ builder.AddTool(mcp.NewTool("maildir_analyze_contacts", "Analyze contact frequency and relationship data from email history", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "limit": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of contacts to analyze",
+ "minimum": 1,
+ "default": 100,
},
},
- }
-}
-
-// registerTools registers all Maildir tools with the server
-func (ms *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := ms.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "maildir_scan_folders":
- handler = ms.HandleScanFolders
- case "maildir_list_messages":
- handler = ms.HandleListMessages
- case "maildir_read_message":
- handler = ms.HandleReadMessage
- case "maildir_search_messages":
- handler = ms.HandleSearchMessages
- case "maildir_get_thread":
- handler = ms.HandleGetThread
- case "maildir_analyze_contacts":
- handler = ms.HandleAnalyzeContacts
- case "maildir_get_statistics":
- handler = ms.HandleGetStatistics
- default:
- continue
- }
- ms.RegisterToolWithDefinition(tool, handler)
- }
-}
-
-// registerPrompts registers all Maildir prompts with the server
-func (ms *Server) registerPrompts() {
- // No specific prompts for maildir at the moment
-}
-
-// registerResources sets up resource handling (lazy loading)
-func (ms *Server) registerResources() {
- // Register placeholder resources for each allowed path to make them discoverable
- for _, path := range ms.allowedPaths {
- maildirURI := "maildir://" + path
- pathName := filepath.Base(path)
- if pathName == "." || pathName == "/" {
- pathName = path
- }
-
- resource := mcp.Resource{
- URI: maildirURI,
- Name: fmt.Sprintf("Maildir: %s", pathName),
- Description: fmt.Sprintf("Maildir folders in %s", path),
- MimeType: "application/x-maildir",
- }
-
- ms.Server.RegisterResourceWithDefinition(resource, ms.HandleReadResource)
- }
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleAnalyzeContacts(req)
+ }))
+
+ // Add maildir_get_statistics tool
+ builder.AddTool(mcp.NewTool("maildir_get_statistics", "Get comprehensive statistics about the maildir including message counts, storage usage, and activity patterns", map[string]interface{}{
+ "type": "object",
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return maildir.handleGetStatistics(req)
+ }))
+
+ return builder.Build()
}
-// registerRoots registers maildir paths as roots
-func (ms *Server) registerRoots() {
- for _, path := range ms.allowedPaths {
- // Create maildir:// URI for the path
- maildirURI := "maildir://" + path
- // Create a user-friendly name from the path
- pathName := filepath.Base(path)
- if pathName == "." || pathName == "/" {
- pathName = path
- }
+// Helper methods for MaildirOperations
- root := mcp.NewRoot(maildirURI, fmt.Sprintf("Maildir: %s", pathName))
- ms.RegisterRoot(root)
- }
-}
// HandleScanFolders implements the maildir_scan_folders tool
-func (ms *Server) HandleScanFolders(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleScanFolders(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
@@ -309,7 +229,7 @@ func (ms *Server) HandleScanFolders(req mcp.CallToolRequest) (mcp.CallToolResult
includeCounts = ic
}
- folders, err := ms.scanFolders(maildirPath, includeCounts)
+ folders, err := maildir.scanFolders(maildirPath, includeCounts)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to scan folders: %v", err)), nil
}
@@ -325,13 +245,13 @@ func (ms *Server) HandleScanFolders(req mcp.CallToolRequest) (mcp.CallToolResult
}
// HandleListMessages implements the maildir_list_messages tool
-func (ms *Server) HandleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
@@ -353,7 +273,7 @@ func (ms *Server) HandleListMessages(req mcp.CallToolRequest) (mcp.CallToolResul
offset = int(o)
}
- messages, total, err := ms.listMessages(maildirPath, folder, limit, offset, req.Arguments)
+ messages, total, err := maildir.listMessages(maildirPath, folder, limit, offset, req.Arguments)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to list messages: %v", err)), nil
}
@@ -372,13 +292,13 @@ func (ms *Server) HandleListMessages(req mcp.CallToolRequest) (mcp.CallToolResul
}
// HandleReadMessage implements the maildir_read_message tool
-func (ms *Server) HandleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
@@ -402,7 +322,7 @@ func (ms *Server) HandleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult
sanitizeContent = sc
}
- message, err := ms.readMessage(maildirPath, messageID, includeHTML, includeHeaders, sanitizeContent)
+ message, err := maildir.readMessage(maildirPath, messageID, includeHTML, includeHeaders, sanitizeContent)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to read message: %v", err)), nil
}
@@ -416,13 +336,13 @@ func (ms *Server) HandleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult
}
// HandleSearchMessages implements the maildir_search_messages tool
-func (ms *Server) HandleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
@@ -439,7 +359,7 @@ func (ms *Server) HandleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolRes
}
}
- results, err := ms.searchMessages(maildirPath, query, limit, req.Arguments)
+ results, err := maildir.searchMessages(maildirPath, query, limit, req.Arguments)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to search messages: %v", err)), nil
}
@@ -456,13 +376,13 @@ func (ms *Server) HandleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolRes
}
// HandleGetThread implements the maildir_get_thread tool
-func (ms *Server) HandleGetThread(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleGetThread(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
@@ -476,7 +396,7 @@ func (ms *Server) HandleGetThread(req mcp.CallToolRequest) (mcp.CallToolResult,
maxDepth = int(md)
}
- thread, err := ms.getThread(maildirPath, messageID, maxDepth)
+ thread, err := maildir.getThread(maildirPath, messageID, maxDepth)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to get thread: %v", err)), nil
}
@@ -492,17 +412,17 @@ func (ms *Server) HandleGetThread(req mcp.CallToolRequest) (mcp.CallToolResult,
}
// HandleAnalyzeContacts implements the maildir_analyze_contacts tool
-func (ms *Server) HandleAnalyzeContacts(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleAnalyzeContacts(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
- contacts, err := ms.analyzeContacts(maildirPath, req.Arguments)
+ contacts, err := maildir.analyzeContacts(maildirPath, req.Arguments)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to analyze contacts: %v", err)), nil
}
@@ -518,17 +438,17 @@ func (ms *Server) HandleAnalyzeContacts(req mcp.CallToolRequest) (mcp.CallToolRe
}
// HandleGetStatistics implements the maildir_get_statistics tool
-func (ms *Server) HandleGetStatistics(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (maildir *MaildirOperations) handleGetStatistics(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
maildirPath, ok := req.Arguments["maildir_path"].(string)
if !ok {
return mcp.NewToolError("maildir_path is required"), nil
}
- if !ms.isPathAllowed(maildirPath) {
+ if !maildir.isPathAllowed(maildirPath) {
return mcp.NewToolError("access denied: path not in allowed directories"), nil
}
- stats, err := ms.getStatistics(maildirPath, req.Arguments)
+ stats, err := maildir.getStatistics(maildirPath, req.Arguments)
if err != nil {
return mcp.NewToolError(fmt.Sprintf("failed to get statistics: %v", err)), nil
}
@@ -541,48 +461,18 @@ func (ms *Server) HandleGetStatistics(req mcp.CallToolRequest) (mcp.CallToolResu
return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
}
-// HandleReadResource implements resource reading for maildir URIs
-func (ms *Server) HandleReadResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
- // Parse the maildir URI to extract path and folder
- uri := req.URI
- if !strings.HasPrefix(uri, "maildir://") {
- return mcp.ReadResourceResult{}, fmt.Errorf("invalid maildir URI: %s", uri)
- }
-
- path := strings.TrimPrefix(uri, "maildir://")
-
- // For now, return folder information as JSON
- folders, err := ms.scanFolders(path, true)
- if err != nil {
- return mcp.ReadResourceResult{}, fmt.Errorf("failed to scan maildir: %v", err)
- }
-
- result, err := json.Marshal(map[string]interface{}{
- "folders": folders,
- "uri": uri,
- })
- if err != nil {
- return mcp.ReadResourceResult{}, fmt.Errorf("failed to marshal result: %v", err)
- }
-
- return mcp.ReadResourceResult{
- Contents: []mcp.Content{
- mcp.NewTextContent(string(result)),
- },
- }, nil
-}
// Helper functions
// isPathAllowed checks if the given path is within allowed directories
-func (ms *Server) isPathAllowed(path string) bool {
+func (maildir *MaildirOperations) isPathAllowed(path string) bool {
absPath, err := filepath.Abs(expandHome(path))
if err != nil {
return false
}
absPath = filepath.Clean(absPath)
- for _, allowedPath := range ms.allowedPaths {
+ for _, allowedPath := range maildir.allowedPaths {
if strings.HasPrefix(absPath, allowedPath) {
return true
}
@@ -603,7 +493,7 @@ func expandHome(path string) string {
}
// scanFolders scans maildir folders and returns folder information
-func (ms *Server) scanFolders(maildirPath string, includeCounts bool) ([]FolderInfo, error) {
+func (maildir *MaildirOperations) scanFolders(maildirPath string, includeCounts bool) ([]FolderInfo, error) {
var folders []FolderInfo
// Walk the maildir directory to find folders
@@ -637,8 +527,8 @@ func (ms *Server) scanFolders(maildirPath string, includeCounts bool) ([]FolderI
if includeCounts {
// Count messages in cur and new directories
- curCount := ms.countMessagesInDir(curDir)
- newCount := ms.countMessagesInDir(newDir)
+ curCount := maildir.countMessagesInDir(curDir)
+ newCount := maildir.countMessagesInDir(newDir)
folder.MessageCount = curCount + newCount
folder.UnreadCount = newCount
}
@@ -664,7 +554,7 @@ func (ms *Server) scanFolders(maildirPath string, includeCounts bool) ([]FolderI
}
// countMessagesInDir counts the number of message files in a directory
-func (ms *Server) countMessagesInDir(dirPath string) int {
+func (maildir *MaildirOperations) countMessagesInDir(dirPath string) int {
entries, err := os.ReadDir(dirPath)
if err != nil {
return 0
@@ -672,7 +562,7 @@ func (ms *Server) countMessagesInDir(dirPath string) int {
count := 0
for _, entry := range entries {
- if !entry.IsDir() && ms.isMaildirMessage(entry.Name()) {
+ if !entry.IsDir() && maildir.isMaildirMessage(entry.Name()) {
count++
}
}
@@ -680,13 +570,13 @@ func (ms *Server) countMessagesInDir(dirPath string) int {
}
// isMaildirMessage checks if a filename looks like a maildir message
-func (ms *Server) isMaildirMessage(filename string) bool {
+func (maildir *MaildirOperations) isMaildirMessage(filename string) bool {
// Basic check for maildir message format
return strings.Contains(filename, ".") && !strings.HasPrefix(filename, ".")
}
// listMessages lists messages in a specific folder with filtering and pagination
-func (ms *Server) listMessages(maildirPath, folder string, limit, offset int, filters map[string]interface{}) ([]MessageInfo, int, error) {
+func (maildir *MaildirOperations) listMessages(maildirPath, folder string, limit, offset int, filters map[string]interface{}) ([]MessageInfo, int, error) {
folderPath := filepath.Join(maildirPath, folder)
if folder == "INBOX" && maildirPath == folderPath {
// Handle case where INBOX might be the root maildir
@@ -698,7 +588,7 @@ func (ms *Server) listMessages(maildirPath, folder string, limit, offset int, fi
// Scan cur and new directories
for _, subdir := range []string{"cur", "new"} {
dirPath := filepath.Join(folderPath, subdir)
- messages, err := ms.scanMessagesInDir(dirPath, folder, subdir == "new")
+ messages, err := maildir.scanMessagesInDir(dirPath, folder, subdir == "new")
if err != nil {
continue // Skip if directory doesn't exist or can't be read
}
@@ -706,7 +596,7 @@ func (ms *Server) listMessages(maildirPath, folder string, limit, offset int, fi
}
// Apply filters
- filteredMessages := ms.applyMessageFilters(allMessages, filters)
+ filteredMessages := maildir.applyMessageFilters(allMessages, filters)
// Sort by date (newest first)
sort.Slice(filteredMessages, func(i, j int) bool {
@@ -730,7 +620,7 @@ func (ms *Server) listMessages(maildirPath, folder string, limit, offset int, fi
}
// scanMessagesInDir scans messages in a specific directory
-func (ms *Server) scanMessagesInDir(dirPath, folder string, isNew bool) ([]MessageInfo, error) {
+func (maildir *MaildirOperations) scanMessagesInDir(dirPath, folder string, isNew bool) ([]MessageInfo, error) {
entries, err := os.ReadDir(dirPath)
if err != nil {
return nil, err
@@ -738,12 +628,12 @@ func (ms *Server) scanMessagesInDir(dirPath, folder string, isNew bool) ([]Messa
var messages []MessageInfo
for _, entry := range entries {
- if entry.IsDir() || !ms.isMaildirMessage(entry.Name()) {
+ if entry.IsDir() || !maildir.isMaildirMessage(entry.Name()) {
continue
}
messagePath := filepath.Join(dirPath, entry.Name())
- message, err := ms.parseMessageInfo(messagePath, folder, entry.Name(), isNew)
+ message, err := maildir.parseMessageInfo(messagePath, folder, entry.Name(), isNew)
if err != nil {
continue // Skip malformed messages
}
@@ -755,7 +645,7 @@ func (ms *Server) scanMessagesInDir(dirPath, folder string, isNew bool) ([]Messa
}
// parseMessageInfo parses basic message information from a maildir message file
-func (ms *Server) parseMessageInfo(messagePath, folder, filename string, isNew bool) (MessageInfo, error) {
+func (maildir *MaildirOperations) parseMessageInfo(messagePath, folder, filename string, isNew bool) (MessageInfo, error) {
file, err := os.Open(messagePath)
if err != nil {
return MessageInfo{}, err
@@ -775,7 +665,7 @@ func (ms *Server) parseMessageInfo(messagePath, folder, filename string, isNew b
}
// Extract flags from filename
- flags := ms.parseMaildirFlags(filename, isNew)
+ flags := maildir.parseMaildirFlags(filename, isNew)
// Parse date
dateStr := msg.Header.Get("Date")
@@ -785,10 +675,10 @@ func (ms *Server) parseMessageInfo(messagePath, folder, filename string, isNew b
}
// Extract references for threading
- references := ms.parseReferences(msg.Header.Get("References"))
+ references := maildir.parseReferences(msg.Header.Get("References"))
messageInfo := MessageInfo{
- ID: ms.generateMessageID(messagePath),
+ ID: maildir.generateMessageID(messagePath),
Filename: filename,
Subject: msg.Header.Get("Subject"),
From: msg.Header.Get("From"),
@@ -813,7 +703,7 @@ func (ms *Server) parseMessageInfo(messagePath, folder, filename string, isNew b
}
// parseMaildirFlags parses maildir flags from filename
-func (ms *Server) parseMaildirFlags(filename string, isNew bool) []string {
+func (maildir *MaildirOperations) parseMaildirFlags(filename string, isNew bool) []string {
var flags []string
if isNew {
@@ -850,7 +740,7 @@ func (ms *Server) parseMaildirFlags(filename string, isNew bool) []string {
}
// parseReferences parses References header for email threading
-func (ms *Server) parseReferences(referencesStr string) []string {
+func (maildir *MaildirOperations) parseReferences(referencesStr string) []string {
if referencesStr == "" {
return nil
}
@@ -868,17 +758,17 @@ func (ms *Server) parseReferences(referencesStr string) []string {
}
// generateMessageID generates a unique ID for a message based on its path
-func (ms *Server) generateMessageID(messagePath string) string {
+func (maildir *MaildirOperations) generateMessageID(messagePath string) string {
// Use the filename without path as the ID
return filepath.Base(messagePath)
}
// applyMessageFilters applies various filters to messages
-func (ms *Server) applyMessageFilters(messages []MessageInfo, filters map[string]interface{}) []MessageInfo {
+func (maildir *MaildirOperations) applyMessageFilters(messages []MessageInfo, filters map[string]interface{}) []MessageInfo {
var filtered []MessageInfo
for _, msg := range messages {
- if ms.messageMatchesFilters(msg, filters) {
+ if maildir.messageMatchesFilters(msg, filters) {
filtered = append(filtered, msg)
}
}
@@ -887,7 +777,7 @@ func (ms *Server) applyMessageFilters(messages []MessageInfo, filters map[string
}
// messageMatchesFilters checks if a message matches the given filters
-func (ms *Server) messageMatchesFilters(msg MessageInfo, filters map[string]interface{}) bool {
+func (maildir *MaildirOperations) messageMatchesFilters(msg MessageInfo, filters map[string]interface{}) bool {
// Date range filter
if dateFromStr, ok := filters["date_from"].(string); ok {
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
@@ -937,9 +827,9 @@ func (ms *Server) messageMatchesFilters(msg MessageInfo, filters map[string]inte
}
// readMessage reads a full message with content
-func (ms *Server) readMessage(maildirPath, messageID string, includeHTML, includeHeaders, sanitizeContent bool) (*Message, error) {
+func (maildir *MaildirOperations) readMessage(maildirPath, messageID string, includeHTML, includeHeaders, sanitizeContent bool) (*Message, error) {
// Find the message file
- messagePath, err := ms.findMessagePath(maildirPath, messageID)
+ messagePath, err := maildir.findMessagePath(maildirPath, messageID)
if err != nil {
return nil, err
}
@@ -963,23 +853,23 @@ func (ms *Server) readMessage(maildirPath, messageID string, includeHTML, includ
}
// Parse basic message info
- folder := ms.extractFolderFromPath(messagePath)
+ folder := maildir.extractFolderFromPath(messagePath)
isNew := strings.Contains(messagePath, "/new/")
- messageInfo, err := ms.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
+ messageInfo, err := maildir.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
if err != nil {
return nil, err
}
// Extract message body
- body, htmlBody, err := ms.extractMessageBody(msg, includeHTML)
+ body, htmlBody, err := maildir.extractMessageBody(msg, includeHTML)
if err != nil {
return nil, err
}
if sanitizeContent {
- body = ms.sanitizeContent(body)
+ body = maildir.sanitizeContent(body)
if htmlBody != "" {
- htmlBody = ms.sanitizeContent(htmlBody)
+ htmlBody = maildir.sanitizeContent(htmlBody)
}
}
@@ -993,7 +883,7 @@ func (ms *Server) readMessage(maildirPath, messageID string, includeHTML, includ
}
// findMessagePath finds the full path to a message file by ID
-func (ms *Server) findMessagePath(maildirPath, messageID string) (string, error) {
+func (maildir *MaildirOperations) findMessagePath(maildirPath, messageID string) (string, error) {
var foundPath string
err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
@@ -1021,7 +911,7 @@ func (ms *Server) findMessagePath(maildirPath, messageID string) (string, error)
}
// extractFolderFromPath extracts folder name from message path
-func (ms *Server) extractFolderFromPath(messagePath string) string {
+func (maildir *MaildirOperations) extractFolderFromPath(messagePath string) string {
// Navigate up from the message file to find the folder
dir := filepath.Dir(messagePath) // cur or new directory
dir = filepath.Dir(dir) // folder directory
@@ -1029,7 +919,7 @@ func (ms *Server) extractFolderFromPath(messagePath string) string {
}
// extractMessageBody extracts plain text and HTML body from message
-func (ms *Server) extractMessageBody(msg *mail.Message, includeHTML bool) (string, string, error) {
+func (maildir *MaildirOperations) extractMessageBody(msg *mail.Message, includeHTML bool) (string, string, error) {
contentType := msg.Header.Get("Content-Type")
if contentType == "" {
@@ -1065,7 +955,7 @@ func (ms *Server) extractMessageBody(msg *mail.Message, includeHTML bool) (strin
return "", "", err
}
htmlBody := string(body)
- plainBody := ms.htmlToPlainText(htmlBody)
+ plainBody := maildir.htmlToPlainText(htmlBody)
if includeHTML {
return plainBody, htmlBody, nil
}
@@ -1073,7 +963,7 @@ func (ms *Server) extractMessageBody(msg *mail.Message, includeHTML bool) (strin
}
if strings.HasPrefix(mediaType, "multipart/") {
- return ms.extractMultipartBody(msg.Body, params["boundary"], includeHTML)
+ return maildir.extractMultipartBody(msg.Body, params["boundary"], includeHTML)
}
// Fallback to reading as plain text
@@ -1085,7 +975,7 @@ func (ms *Server) extractMessageBody(msg *mail.Message, includeHTML bool) (strin
}
// extractMultipartBody extracts body from multipart message
-func (ms *Server) extractMultipartBody(body io.Reader, boundary string, includeHTML bool) (string, string, error) {
+func (maildir *MaildirOperations) extractMultipartBody(body io.Reader, boundary string, includeHTML bool) (string, string, error) {
mr := multipart.NewReader(body, boundary)
var plainBody, htmlBody string
@@ -1117,7 +1007,7 @@ func (ms *Server) extractMultipartBody(body io.Reader, boundary string, includeH
// If we only have HTML, convert it to plain text
if plainBody == "" && htmlBody != "" {
- plainBody = ms.htmlToPlainText(htmlBody)
+ plainBody = maildir.htmlToPlainText(htmlBody)
}
if includeHTML {
@@ -1127,7 +1017,7 @@ func (ms *Server) extractMultipartBody(body io.Reader, boundary string, includeH
}
// htmlToPlainText converts HTML to plain text (simple implementation)
-func (ms *Server) htmlToPlainText(html string) string {
+func (maildir *MaildirOperations) htmlToPlainText(html string) string {
// Simple HTML to text conversion - remove tags
re := regexp.MustCompile(`<[^>]*>`)
text := re.ReplaceAllString(html, "")
@@ -1144,7 +1034,7 @@ func (ms *Server) htmlToPlainText(html string) string {
}
// sanitizeContent sanitizes message content (basic implementation)
-func (ms *Server) sanitizeContent(content string) string {
+func (maildir *MaildirOperations) sanitizeContent(content string) string {
// Basic PII masking - this is a simple implementation
// In production, you'd want more sophisticated PII detection
@@ -1160,12 +1050,12 @@ func (ms *Server) sanitizeContent(content string) string {
}
// searchMessages performs full-text search across messages
-func (ms *Server) searchMessages(maildirPath, query string, limit int, filters map[string]interface{}) ([]MessageInfo, error) {
+func (maildir *MaildirOperations) searchMessages(maildirPath, query string, limit int, filters map[string]interface{}) ([]MessageInfo, error) {
var results []MessageInfo
// Simple implementation - scan all messages and search in subject/from/body
err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
- if err != nil || info.IsDir() || !ms.isMaildirMessage(info.Name()) {
+ if err != nil || info.IsDir() || !maildir.isMaildirMessage(info.Name()) {
return nil
}
@@ -1174,7 +1064,7 @@ func (ms *Server) searchMessages(maildirPath, query string, limit int, filters m
return nil
}
- matches, messageInfo := ms.searchInMessage(path, query)
+ matches, messageInfo := maildir.searchInMessage(path, query)
if matches {
results = append(results, messageInfo)
}
@@ -1200,16 +1090,16 @@ func (ms *Server) searchMessages(maildirPath, query string, limit int, filters m
}
// searchInMessage searches for query in a specific message
-func (ms *Server) searchInMessage(messagePath, query string) (bool, MessageInfo) {
+func (maildir *MaildirOperations) searchInMessage(messagePath, query string) (bool, MessageInfo) {
file, err := os.Open(messagePath)
if err != nil {
return false, MessageInfo{}
}
defer file.Close()
- folder := ms.extractFolderFromPath(messagePath)
+ folder := maildir.extractFolderFromPath(messagePath)
isNew := strings.Contains(messagePath, "/new/")
- messageInfo, err := ms.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
+ messageInfo, err := maildir.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
if err != nil {
return false, MessageInfo{}
}
@@ -1230,7 +1120,7 @@ func (ms *Server) searchInMessage(messagePath, query string) (bool, MessageInfo)
return false, messageInfo
}
- body, _, err := ms.extractMessageBody(msg, false)
+ body, _, err := maildir.extractMessageBody(msg, false)
if err != nil {
return false, messageInfo
}
@@ -1243,16 +1133,16 @@ func (ms *Server) searchInMessage(messagePath, query string) (bool, MessageInfo)
}
// getThread retrieves email thread for a message
-func (ms *Server) getThread(maildirPath, messageID string, maxDepth int) ([]MessageInfo, error) {
+func (maildir *MaildirOperations) getThread(maildirPath, messageID string, maxDepth int) ([]MessageInfo, error) {
// Find the starting message
- messagePath, err := ms.findMessagePath(maildirPath, messageID)
+ messagePath, err := maildir.findMessagePath(maildirPath, messageID)
if err != nil {
return nil, err
}
- folder := ms.extractFolderFromPath(messagePath)
+ folder := maildir.extractFolderFromPath(messagePath)
isNew := strings.Contains(messagePath, "/new/")
- startMessage, err := ms.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
+ startMessage, err := maildir.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
if err != nil {
return nil, err
}
@@ -1267,11 +1157,11 @@ func (ms *Server) getThread(maildirPath, messageID string, maxDepth int) ([]Mess
}
// analyzeContacts analyzes contact information from messages
-func (ms *Server) analyzeContacts(maildirPath string, filters map[string]interface{}) ([]ContactInfo, error) {
+func (maildir *MaildirOperations) analyzeContacts(maildirPath string, filters map[string]interface{}) ([]ContactInfo, error) {
contactMap := make(map[string]*ContactInfo)
err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
- if err != nil || info.IsDir() || !ms.isMaildirMessage(info.Name()) {
+ if err != nil || info.IsDir() || !maildir.isMaildirMessage(info.Name()) {
return nil
}
@@ -1280,23 +1170,23 @@ func (ms *Server) analyzeContacts(maildirPath string, filters map[string]interfa
return nil
}
- folder := ms.extractFolderFromPath(path)
+ folder := maildir.extractFolderFromPath(path)
isNew := strings.Contains(path, "/new/")
- messageInfo, err := ms.parseMessageInfo(path, folder, filepath.Base(path), isNew)
+ messageInfo, err := maildir.parseMessageInfo(path, folder, filepath.Base(path), isNew)
if err != nil {
return nil
}
// Process From address
if messageInfo.From != "" {
- ms.processContact(contactMap, messageInfo.From, messageInfo.Date, false)
+ maildir.processContact(contactMap, messageInfo.From, messageInfo.Date, false)
}
// Process To addresses
if messageInfo.To != "" {
toAddresses := strings.Split(messageInfo.To, ",")
for _, addr := range toAddresses {
- ms.processContact(contactMap, strings.TrimSpace(addr), messageInfo.Date, true)
+ maildir.processContact(contactMap, strings.TrimSpace(addr), messageInfo.Date, true)
}
}
@@ -1321,7 +1211,7 @@ func (ms *Server) analyzeContacts(maildirPath string, filters map[string]interfa
}
// processContact processes a contact address for analysis
-func (ms *Server) processContact(contactMap map[string]*ContactInfo, address string, date time.Time, isOutgoing bool) {
+func (maildir *MaildirOperations) processContact(contactMap map[string]*ContactInfo, address string, date time.Time, isOutgoing bool) {
// Extract email address from "Name <email>" format
addr, err := mail.ParseAddress(address)
if err != nil {
@@ -1362,7 +1252,7 @@ func (ms *Server) processContact(contactMap map[string]*ContactInfo, address str
}
// getStatistics generates maildir usage statistics
-func (ms *Server) getStatistics(maildirPath string, filters map[string]interface{}) (map[string]interface{}, error) {
+func (maildir *MaildirOperations) getStatistics(maildirPath string, filters map[string]interface{}) (map[string]interface{}, error) {
stats := make(map[string]interface{})
// Basic statistics implementation
@@ -1371,7 +1261,7 @@ func (ms *Server) getStatistics(maildirPath string, filters map[string]interface
folderCounts := make(map[string]int)
err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
- if err != nil || info.IsDir() || !ms.isMaildirMessage(info.Name()) {
+ if err != nil || info.IsDir() || !maildir.isMaildirMessage(info.Name()) {
return nil
}
@@ -1383,7 +1273,7 @@ func (ms *Server) getStatistics(maildirPath string, filters map[string]interface
totalMessages++
totalSize += info.Size()
- folder := ms.extractFolderFromPath(path)
+ folder := maildir.extractFolderFromPath(path)
folderCounts[folder]++
return nil