main
   1package imap
   2
   3import (
   4	"crypto/tls"
   5	"encoding/json"
   6	"fmt"
   7	"io"
   8	"log"
   9	"strconv"
  10	"sync"
  11	"time"
  12
  13	goimap "github.com/emersion/go-imap"
  14	"github.com/emersion/go-imap/client"
  15	"github.com/xlgmokha/mcp/pkg/mcp"
  16)
  17
  18// ImapOperations provides IMAP email operations
  19type ImapOperations struct {
  20	mu       sync.RWMutex
  21	client   *client.Client
  22	server   string
  23	username string
  24	password string
  25	port     int
  26	useTLS   bool
  27	connected bool
  28}
  29
  30type FolderInfo struct {
  31	Name     string `json:"name"`
  32	Messages uint32 `json:"messages"`
  33	Recent   uint32 `json:"recent"`
  34	Unseen   uint32 `json:"unseen"`
  35}
  36
  37type MessageInfo struct {
  38	UID        uint32            `json:"uid"`
  39	SeqNum     uint32            `json:"seq_num"`
  40	Subject    string            `json:"subject"`
  41	From       []string          `json:"from"`
  42	To         []string          `json:"to"`
  43	Date       time.Time         `json:"date"`
  44	Size       uint32            `json:"size"`
  45	Flags      []string          `json:"flags"`
  46	Headers    map[string]string `json:"headers,omitempty"`
  47	Body       string            `json:"body,omitempty"`
  48	Attachments []AttachmentInfo `json:"attachments,omitempty"`
  49}
  50
  51type AttachmentInfo struct {
  52	Filename string `json:"filename"`
  53	MimeType string `json:"mime_type"`
  54	Size     int    `json:"size"`
  55	PartID   string `json:"part_id"`
  56}
  57
  58type ConnectionInfo struct {
  59	Server     string                 `json:"server"`
  60	Username   string                 `json:"username"`
  61	Port       int                    `json:"port"`
  62	UseTLS     bool                   `json:"use_tls"`
  63	Connected  bool                   `json:"connected"`
  64	Capabilities []string             `json:"capabilities"`
  65	ServerInfo map[string]interface{} `json:"server_info"`
  66}
  67
  68// NewImapOperations creates a new ImapOperations helper
  69func NewImapOperations(server, username, password string, port int, useTLS bool) *ImapOperations {
  70	return &ImapOperations{
  71		server:   server,
  72		username: username,
  73		password: password,
  74		port:     port,
  75		useTLS:   useTLS,
  76	}
  77}
  78
  79// New creates a new IMAP MCP server
  80func New(server, username, password string, port int, useTLS bool) *mcp.Server {
  81	imap := NewImapOperations(server, username, password, port, useTLS)
  82	builder := mcp.NewServerBuilder("imap", "1.0.0")
  83
  84	// Add imap_list_folders tool
  85	builder.AddTool(mcp.NewTool("imap_list_folders", "List all folders in the email account (INBOX, Sent, Drafts, etc.)", map[string]interface{}{
  86		"type":       "object",
  87		"properties": map[string]interface{}{},
  88	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
  89		return imap.handleListFolders(req)
  90	}))
  91
  92	// Add imap_list_messages tool  
  93	builder.AddTool(mcp.NewTool("imap_list_messages", "List messages in a folder with optional pagination and filtering", map[string]interface{}{
  94		"type": "object",
  95		"properties": map[string]interface{}{
  96			"folder": map[string]interface{}{
  97				"type":        "string",
  98				"description": "Folder name (default: INBOX)",
  99			},
 100			"limit": map[string]interface{}{
 101				"type":        "integer",
 102				"description": "Maximum number of messages to return (default: 20)",
 103				"minimum":     1,
 104				"maximum":     100,
 105			},
 106			"offset": map[string]interface{}{
 107				"type":        "integer",
 108				"description": "Number of messages to skip (for pagination) (default: 0)",
 109				"minimum":     0,
 110			},
 111			"unread_only": map[string]interface{}{
 112				"type":        "boolean",
 113				"description": "Show only unread messages (default: false)",
 114			},
 115		},
 116	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 117		return imap.handleListMessages(req)
 118	}))
 119
 120	// Add imap_read_message tool
 121	builder.AddTool(mcp.NewTool("imap_read_message", "Read the full content of a specific email message", map[string]interface{}{
 122		"type": "object",
 123		"properties": map[string]interface{}{
 124			"uid": map[string]interface{}{
 125				"type":        "integer",
 126				"description": "Message UID",
 127			},
 128			"folder": map[string]interface{}{
 129				"type":        "string",
 130				"description": "Folder name (default: INBOX)",
 131			},
 132			"mark_as_read": map[string]interface{}{
 133				"type":        "boolean",
 134				"description": "Mark message as read when reading (default: false)",
 135			},
 136		},
 137		"required": []string{"uid"},
 138	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 139		return imap.handleReadMessage(req)
 140	}))
 141
 142	// Add imap_search_messages tool
 143	builder.AddTool(mcp.NewTool("imap_search_messages", "Search for messages by content, sender, subject, or other criteria", map[string]interface{}{
 144		"type": "object",
 145		"properties": map[string]interface{}{
 146			"query": map[string]interface{}{
 147				"type":        "string",
 148				"description": "Search query text",
 149			},
 150			"folder": map[string]interface{}{
 151				"type":        "string",
 152				"description": "Folder to search in (default: INBOX)",
 153			},
 154			"search_type": map[string]interface{}{
 155				"type":        "string",
 156				"description": "Type of search (subject, from, body, all) (default: all)",
 157				"enum":        []string{"subject", "from", "to", "body", "all"},
 158			},
 159			"since_date": map[string]interface{}{
 160				"type":        "string",
 161				"description": "Search messages since this date (YYYY-MM-DD format)",
 162			},
 163			"before_date": map[string]interface{}{
 164				"type":        "string",
 165				"description": "Search messages before this date (YYYY-MM-DD format)",
 166			},
 167			"limit": map[string]interface{}{
 168				"type":        "integer",
 169				"description": "Maximum number of results (default: 50)",
 170				"minimum":     1,
 171				"maximum":     200,
 172			},
 173		},
 174		"required": []string{"query"},
 175	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 176		return imap.handleSearchMessages(req)
 177	}))
 178
 179	// Add imap_get_folder_stats tool
 180	builder.AddTool(mcp.NewTool("imap_get_folder_stats", "Get statistics for a specific folder (message counts, sizes, etc.)", map[string]interface{}{
 181		"type": "object",
 182		"properties": map[string]interface{}{
 183			"folder": map[string]interface{}{
 184				"type":        "string",
 185				"description": "Folder name (default: INBOX)",
 186			},
 187		},
 188	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 189		return imap.handleGetFolderStats(req)
 190	}))
 191
 192	// Add imap_mark_as_read tool
 193	builder.AddTool(mcp.NewTool("imap_mark_as_read", "Mark messages as read or unread", map[string]interface{}{
 194		"type": "object",
 195		"properties": map[string]interface{}{
 196			"uids": map[string]interface{}{
 197				"type":        "array",
 198				"items":       map[string]interface{}{"type": "integer"},
 199				"description": "Array of message UIDs to mark",
 200			},
 201			"folder": map[string]interface{}{
 202				"type":        "string",
 203				"description": "Folder name (default: INBOX)",
 204			},
 205			"mark_as_read": map[string]interface{}{
 206				"type":        "boolean",
 207				"description": "True to mark as read, false to mark as unread (default: true)",
 208			},
 209		},
 210		"required": []string{"uids"},
 211	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 212		return imap.handleMarkAsRead(req)
 213	}))
 214
 215	// Add imap_get_attachments tool
 216	builder.AddTool(mcp.NewTool("imap_get_attachments", "List attachments for a specific message (placeholder)", map[string]interface{}{
 217		"type": "object",
 218		"properties": map[string]interface{}{
 219			"uid": map[string]interface{}{
 220				"type":        "integer",
 221				"description": "Message UID",
 222			},
 223			"folder": map[string]interface{}{
 224				"type":        "string",
 225				"description": "Folder name (default: INBOX)",
 226			},
 227		},
 228		"required": []string{"uid"},
 229	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 230		return imap.handleGetAttachments(req)
 231	}))
 232
 233	// Add imap_get_connection_info tool
 234	builder.AddTool(mcp.NewTool("imap_get_connection_info", "Get IMAP server connection information and capabilities", map[string]interface{}{
 235		"type": "object",
 236	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 237		return imap.handleGetConnectionInfo(req)
 238	}))
 239
 240	// Add imap_delete_message tool
 241	builder.AddTool(mcp.NewTool("imap_delete_message", "Delete a single message (requires confirmation for safety)", map[string]interface{}{
 242		"type": "object",
 243		"properties": map[string]interface{}{
 244			"uid": map[string]interface{}{
 245				"type":        "integer",
 246				"description": "Message UID to delete",
 247			},
 248			"folder": map[string]interface{}{
 249				"type":        "string",
 250				"description": "Folder name (default: INBOX)",
 251			},
 252			"confirmed": map[string]interface{}{
 253				"type":        "boolean",
 254				"description": "Must be true to confirm permanent deletion",
 255			},
 256		},
 257		"required": []string{"uid", "confirmed"},
 258	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 259		return imap.handleDeleteMessage(req)
 260	}))
 261
 262	// Add imap_delete_messages tool
 263	builder.AddTool(mcp.NewTool("imap_delete_messages", "Delete multiple messages at once (requires confirmation for safety)", map[string]interface{}{
 264		"type": "object",
 265		"properties": map[string]interface{}{
 266			"uids": map[string]interface{}{
 267				"type":        "array",
 268				"items":       map[string]interface{}{"type": "integer"},
 269				"description": "Array of message UIDs to delete",
 270			},
 271			"folder": map[string]interface{}{
 272				"type":        "string",
 273				"description": "Folder name (default: INBOX)",
 274			},
 275			"confirmed": map[string]interface{}{
 276				"type":        "boolean",
 277				"description": "Must be true to confirm permanent deletion",
 278			},
 279		},
 280		"required": []string{"uids", "confirmed"},
 281	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 282		return imap.handleDeleteMessages(req)
 283	}))
 284
 285	// Add imap_move_to_trash tool
 286	builder.AddTool(mcp.NewTool("imap_move_to_trash", "Move messages to trash folder (safe, reversible deletion)", map[string]interface{}{
 287		"type": "object",
 288		"properties": map[string]interface{}{
 289			"uids": map[string]interface{}{
 290				"type":        "array",
 291				"items":       map[string]interface{}{"type": "integer"},
 292				"description": "Array of message UIDs to move to trash",
 293			},
 294			"folder": map[string]interface{}{
 295				"type":        "string",
 296				"description": "Source folder name (default: INBOX)",
 297			},
 298			"trash_folder": map[string]interface{}{
 299				"type":        "string",
 300				"description": "Trash folder name (default: Trash)",
 301			},
 302		},
 303		"required": []string{"uids"},
 304	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 305		return imap.handleMoveToTrash(req)
 306	}))
 307
 308	// Add imap_expunge_folder tool
 309	builder.AddTool(mcp.NewTool("imap_expunge_folder", "Permanently remove all messages marked for deletion from a folder", map[string]interface{}{
 310		"type": "object",
 311		"properties": map[string]interface{}{
 312			"folder": map[string]interface{}{
 313				"type":        "string",
 314				"description": "Folder name (default: INBOX)",
 315			},
 316			"confirmed": map[string]interface{}{
 317				"type":        "boolean",
 318				"description": "Must be true to confirm permanent expunge",
 319			},
 320		},
 321		"required": []string{"confirmed"},
 322	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 323		return imap.handleExpungeFolder(req)
 324	}))
 325
 326	// Add prompts
 327	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) {
 328		return imap.handleAnalysisPrompt(req)
 329	}))
 330
 331	builder.AddPrompt(mcp.NewPrompt("email-search", "Contextual email search with AI-powered insights and filtering", []mcp.PromptArgument{}, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
 332		return imap.handleSearchPrompt(req)
 333	}))
 334
 335	return builder.Build()
 336}
 337
 338
 339// Helper methods for ImapOperations
 340
 341func (imap *ImapOperations) connect() error {
 342	if imap.connected && imap.client != nil {
 343		return nil
 344	}
 345
 346	address := fmt.Sprintf("%s:%d", imap.server, imap.port)
 347	
 348	var c *client.Client
 349	var err error
 350
 351	if imap.useTLS {
 352		c, err = client.DialTLS(address, &tls.Config{ServerName: imap.server})
 353	} else {
 354		c, err = client.Dial(address)
 355		if err == nil {
 356			if err = c.StartTLS(&tls.Config{ServerName: imap.server}); err != nil {
 357				log.Printf("STARTTLS failed: %v", err)
 358			}
 359		}
 360	}
 361
 362	if err != nil {
 363		return fmt.Errorf("failed to connect to IMAP server: %v", err)
 364	}
 365
 366	if err = c.Login(imap.username, imap.password); err != nil {
 367		c.Close()
 368		return fmt.Errorf("authentication failed: %v", err)
 369	}
 370
 371	imap.mu.Lock()
 372	imap.client = c
 373	imap.connected = true
 374	imap.mu.Unlock()
 375
 376	return nil
 377}
 378
 379func (imap *ImapOperations) ensureConnection() error {
 380	imap.mu.RLock()
 381	connected := imap.connected
 382	imap.mu.RUnlock()
 383
 384	if !connected {
 385		return imap.connect()
 386	}
 387	return nil
 388}
 389
 390func (imap *ImapOperations) handleListFolders(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 391	if err := imap.ensureConnection(); err != nil {
 392		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 393	}
 394
 395	imap.mu.RLock()
 396	client := imap.client
 397	imap.mu.RUnlock()
 398
 399	mailboxes := make(chan *goimap.MailboxInfo, 10)
 400	done := make(chan error, 1)
 401	go func() {
 402		done <- client.List("", "*", mailboxes)
 403	}()
 404
 405	var mailboxList []*goimap.MailboxInfo
 406	for m := range mailboxes {
 407		mailboxList = append(mailboxList, m)
 408	}
 409
 410	if err := <-done; err != nil {
 411		return mcp.NewToolError(fmt.Sprintf("Failed to list folders: %v", err)), nil
 412	}
 413
 414	var folders []FolderInfo
 415	for _, m := range mailboxList {
 416		mbox, err := client.Select(m.Name, true)
 417		if err != nil {
 418			// Add folder without stats if we can't select it
 419			folders = append(folders, FolderInfo{
 420				Name:     m.Name,
 421				Messages: 0,
 422				Recent:   0,
 423				Unseen:   0,
 424			})
 425			continue
 426		}
 427
 428		folders = append(folders, FolderInfo{
 429			Name:     m.Name,
 430			Messages: mbox.Messages,
 431			Recent:   mbox.Recent,
 432			Unseen:   mbox.Unseen,
 433		})
 434	}
 435
 436	result, _ := json.Marshal(folders)
 437	return mcp.CallToolResult{
 438		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: string(result)}},
 439	}, nil
 440}
 441
 442func (imap *ImapOperations) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 443	if err := imap.ensureConnection(); err != nil {
 444		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 445	}
 446
 447	folder := "INBOX"
 448	if f, ok := req.Arguments["folder"].(string); ok {
 449		folder = f
 450	}
 451
 452	limit := 50
 453	if l, ok := req.Arguments["limit"].(float64); ok {
 454		limit = int(l)
 455	}
 456
 457	imap.mu.RLock()
 458	client := imap.client
 459	imap.mu.RUnlock()
 460
 461	mbox, err := client.Select(folder, true)
 462	if err != nil {
 463		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
 464	}
 465
 466	if mbox.Messages == 0 {
 467		return mcp.CallToolResult{
 468			Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "[]"}},
 469		}, nil
 470	}
 471
 472	from := uint32(1)
 473	to := mbox.Messages
 474	if limit > 0 && int(mbox.Messages) > limit {
 475		from = mbox.Messages - uint32(limit) + 1
 476	}
 477
 478	seqset := new(goimap.SeqSet)
 479	seqset.AddRange(from, to)
 480
 481	messages := make(chan *goimap.Message, 10)
 482	done := make(chan error, 1)
 483	go func() {
 484		done <- client.Fetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags, goimap.FetchRFC822Size}, messages)
 485	}()
 486
 487	var messageList []MessageInfo
 488	for msg := range messages {
 489		if msg.Envelope == nil {
 490			continue
 491		}
 492
 493		fromAddrs := make([]string, 0)
 494		for _, addr := range msg.Envelope.From {
 495			if addr.PersonalName != "" {
 496				fromAddrs = append(fromAddrs, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
 497			} else {
 498				fromAddrs = append(fromAddrs, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
 499			}
 500		}
 501
 502		toAddrs := make([]string, 0)
 503		for _, addr := range msg.Envelope.To {
 504			if addr.PersonalName != "" {
 505				toAddrs = append(toAddrs, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
 506			} else {
 507				toAddrs = append(toAddrs, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
 508			}
 509		}
 510
 511		flags := make([]string, len(msg.Flags))
 512		for i, flag := range msg.Flags {
 513			flags[i] = string(flag)
 514		}
 515
 516		messageList = append(messageList, MessageInfo{
 517			UID:     msg.Uid,
 518			SeqNum:  msg.SeqNum,
 519			Subject: msg.Envelope.Subject,
 520			From:    fromAddrs,
 521			To:      toAddrs,
 522			Date:    msg.Envelope.Date,
 523			Size:    msg.Size,
 524			Flags:   flags,
 525		})
 526	}
 527
 528	if err := <-done; err != nil {
 529		return mcp.NewToolError(fmt.Sprintf("Failed to fetch messages: %v", err)), nil
 530	}
 531
 532	result, _ := json.Marshal(messageList)
 533	return mcp.CallToolResult{
 534		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: string(result)}},
 535	}, nil
 536}
 537
 538func (imap *ImapOperations) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 539	if err := imap.ensureConnection(); err != nil {
 540		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 541	}
 542
 543	folder := "INBOX"
 544	if f, ok := req.Arguments["folder"].(string); ok {
 545		folder = f
 546	}
 547
 548	var uid uint32
 549	if u, ok := req.Arguments["uid"].(float64); ok {
 550		uid = uint32(u)
 551	} else if u, ok := req.Arguments["uid"].(string); ok {
 552		if parsed, err := strconv.ParseUint(u, 10, 32); err == nil {
 553			uid = uint32(parsed)
 554		}
 555	}
 556
 557	if uid == 0 {
 558		return mcp.NewToolError("uid parameter is required"), nil
 559	}
 560
 561	imap.mu.RLock()
 562	client := imap.client
 563	imap.mu.RUnlock()
 564
 565	if _, err := client.Select(folder, true); err != nil {
 566		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
 567	}
 568
 569	seqset := new(goimap.SeqSet)
 570	seqset.AddNum(uid)
 571
 572	messages := make(chan *goimap.Message, 1)
 573	done := make(chan error, 1)
 574	go func() {
 575		done <- client.UidFetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags, goimap.FetchRFC822Size, goimap.FetchRFC822}, messages)
 576	}()
 577
 578	var message *goimap.Message
 579	for msg := range messages {
 580		message = msg
 581		break
 582	}
 583
 584	if err := <-done; err != nil {
 585		return mcp.NewToolError(fmt.Sprintf("Failed to fetch message: %v", err)), nil
 586	}
 587
 588	if message == nil {
 589		return mcp.NewToolError("Message not found"), nil
 590	}
 591
 592	var body string
 593	if r := message.GetBody(&goimap.BodySectionName{}); r != nil {
 594		if bodyBytes, err := io.ReadAll(r); err == nil {
 595			body = string(bodyBytes)
 596		}
 597	}
 598
 599	fromAddrs := make([]string, 0)
 600	for _, addr := range message.Envelope.From {
 601		if addr.PersonalName != "" {
 602			fromAddrs = append(fromAddrs, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
 603		} else {
 604			fromAddrs = append(fromAddrs, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
 605		}
 606	}
 607
 608	toAddrs := make([]string, 0)
 609	for _, addr := range message.Envelope.To {
 610		if addr.PersonalName != "" {
 611			toAddrs = append(toAddrs, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
 612		} else {
 613			toAddrs = append(toAddrs, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
 614		}
 615	}
 616
 617	flags := make([]string, len(message.Flags))
 618	for i, flag := range message.Flags {
 619		flags[i] = string(flag)
 620	}
 621
 622	messageInfo := MessageInfo{
 623		UID:     message.Uid,
 624		SeqNum:  message.SeqNum,
 625		Subject: message.Envelope.Subject,
 626		From:    fromAddrs,
 627		To:      toAddrs,
 628		Date:    message.Envelope.Date,
 629		Size:    message.Size,
 630		Flags:   flags,
 631		Body:    body,
 632	}
 633
 634	result, _ := json.Marshal(messageInfo)
 635	return mcp.CallToolResult{
 636		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: string(result)}},
 637	}, nil
 638}
 639
 640func (imap *ImapOperations) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 641	if err := imap.ensureConnection(); err != nil {
 642		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 643	}
 644
 645	folder := "INBOX"
 646	if f, ok := req.Arguments["folder"].(string); ok {
 647		folder = f
 648	}
 649
 650	query, ok := req.Arguments["query"].(string)
 651	if !ok || query == "" {
 652		return mcp.NewToolError("query parameter is required"), nil
 653	}
 654
 655	imap.mu.RLock()
 656	client := imap.client
 657	imap.mu.RUnlock()
 658
 659	if _, err := client.Select(folder, false); err != nil {
 660		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
 661	}
 662
 663	criteria := &goimap.SearchCriteria{
 664		Text: []string{query},
 665	}
 666
 667	if sender, ok := req.Arguments["sender"].(string); ok && sender != "" {
 668		criteria.Header = make(map[string][]string)
 669		criteria.Header["FROM"] = []string{sender}
 670	}
 671
 672	if subject, ok := req.Arguments["subject"].(string); ok && subject != "" {
 673		if criteria.Header == nil {
 674			criteria.Header = make(map[string][]string)
 675		}
 676		criteria.Header["SUBJECT"] = []string{subject}
 677	}
 678
 679	uids, err := client.UidSearch(criteria)
 680	if err != nil {
 681		return mcp.NewToolError(fmt.Sprintf("Search failed: %v", err)), nil
 682	}
 683
 684	if len(uids) == 0 {
 685		return mcp.CallToolResult{
 686			Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "[]"}},
 687		}, nil
 688	}
 689
 690	seqset := new(goimap.SeqSet)
 691	seqset.AddNum(uids...)
 692
 693	messages := make(chan *goimap.Message, 10)
 694	done := make(chan error, 1)
 695	go func() {
 696		done <- client.UidFetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags, goimap.FetchRFC822Size}, messages)
 697	}()
 698
 699	var messageList []MessageInfo
 700	for msg := range messages {
 701		if msg.Envelope == nil {
 702			continue
 703		}
 704
 705		fromAddrs := make([]string, 0)
 706		for _, addr := range msg.Envelope.From {
 707			if addr.PersonalName != "" {
 708				fromAddrs = append(fromAddrs, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
 709			} else {
 710				fromAddrs = append(fromAddrs, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
 711			}
 712		}
 713
 714		toAddrs := make([]string, 0)
 715		for _, addr := range msg.Envelope.To {
 716			if addr.PersonalName != "" {
 717				toAddrs = append(toAddrs, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
 718			} else {
 719				toAddrs = append(toAddrs, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
 720			}
 721		}
 722
 723		flags := make([]string, len(msg.Flags))
 724		for i, flag := range msg.Flags {
 725			flags[i] = string(flag)
 726		}
 727
 728		messageList = append(messageList, MessageInfo{
 729			UID:     msg.Uid,
 730			SeqNum:  msg.SeqNum,
 731			Subject: msg.Envelope.Subject,
 732			From:    fromAddrs,
 733			To:      toAddrs,
 734			Date:    msg.Envelope.Date,
 735			Size:    msg.Size,
 736			Flags:   flags,
 737		})
 738	}
 739
 740	if err := <-done; err != nil {
 741		return mcp.NewToolError(fmt.Sprintf("Failed to fetch search results: %v", err)), nil
 742	}
 743
 744	result, _ := json.Marshal(messageList)
 745	return mcp.CallToolResult{
 746		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: string(result)}},
 747	}, nil
 748}
 749
 750func (imap *ImapOperations) handleGetFolderStats(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 751	if err := imap.ensureConnection(); err != nil {
 752		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 753	}
 754
 755	folder := "INBOX"
 756	if f, ok := req.Arguments["folder"].(string); ok {
 757		folder = f
 758	}
 759
 760	imap.mu.RLock()
 761	client := imap.client
 762	imap.mu.RUnlock()
 763
 764	mbox, err := client.Select(folder, true)
 765	if err != nil {
 766		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
 767	}
 768
 769	stats := FolderInfo{
 770		Name:     folder,
 771		Messages: mbox.Messages,
 772		Recent:   mbox.Recent,
 773		Unseen:   mbox.Unseen,
 774	}
 775
 776	result, _ := json.Marshal(stats)
 777	return mcp.CallToolResult{
 778		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: string(result)}},
 779	}, nil
 780}
 781
 782func (imap *ImapOperations) handleMarkAsRead(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 783	if err := imap.ensureConnection(); err != nil {
 784		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 785	}
 786
 787	folder := "INBOX"
 788	if f, ok := req.Arguments["folder"].(string); ok {
 789		folder = f
 790	}
 791
 792	var uid uint32
 793	if u, ok := req.Arguments["uid"].(float64); ok {
 794		uid = uint32(u)
 795	} else if u, ok := req.Arguments["uid"].(string); ok {
 796		if parsed, err := strconv.ParseUint(u, 10, 32); err == nil {
 797			uid = uint32(parsed)
 798		}
 799	}
 800
 801	if uid == 0 {
 802		return mcp.NewToolError("uid parameter is required"), nil
 803	}
 804
 805	markAsRead := true
 806	if mar, ok := req.Arguments["mark_as_read"].(bool); ok {
 807		markAsRead = mar
 808	}
 809
 810	imap.mu.RLock()
 811	client := imap.client
 812	imap.mu.RUnlock()
 813
 814	if _, err := client.Select(folder, false); err != nil {
 815		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
 816	}
 817
 818	seqset := new(goimap.SeqSet)
 819	seqset.AddNum(uid)
 820
 821	var operation goimap.StoreItem
 822	flags := []interface{}{goimap.SeenFlag}
 823	
 824	if markAsRead {
 825		operation = goimap.FormatFlagsOp(goimap.AddFlags, true)
 826	} else {
 827		operation = goimap.FormatFlagsOp(goimap.RemoveFlags, true)
 828	}
 829
 830	ch := make(chan *goimap.Message, 1)
 831	if err := client.UidStore(seqset, operation, flags, ch); err != nil {
 832		return mcp.NewToolError(fmt.Sprintf("Failed to update message flags: %v", err)), nil
 833	}
 834	// Drain the channel
 835	for range ch {
 836	}
 837
 838	status := "read"
 839	if !markAsRead {
 840		status = "unread"
 841	}
 842
 843	return mcp.CallToolResult{
 844		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Message %d marked as %s", uid, status)}},
 845	}, nil
 846}
 847
 848func (imap *ImapOperations) handleGetAttachments(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 849	return mcp.CallToolResult{
 850		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "Attachment handling not yet implemented"}},
 851	}, nil
 852}
 853
 854func (imap *ImapOperations) handleGetConnectionInfo(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 855	imap.mu.RLock()
 856	connected := imap.connected
 857	client := imap.client
 858	imap.mu.RUnlock()
 859
 860	info := ConnectionInfo{
 861		Server:    imap.server,
 862		Username:  imap.username,
 863		Port:      imap.port,
 864		UseTLS:    imap.useTLS,
 865		Connected: connected,
 866		Capabilities: []string{},
 867		ServerInfo: make(map[string]interface{}),
 868	}
 869
 870	if connected && client != nil {
 871		if caps, err := client.Capability(); err == nil {
 872			for cap := range caps {
 873				info.Capabilities = append(info.Capabilities, cap)
 874			}
 875		}
 876		
 877		info.ServerInfo["connection_status"] = "connected"
 878		info.ServerInfo["last_connected"] = time.Now().Format(time.RFC3339)
 879	} else {
 880		info.ServerInfo["connection_status"] = "disconnected"
 881	}
 882
 883	result, _ := json.Marshal(info)
 884	return mcp.CallToolResult{
 885		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: string(result)}},
 886	}, nil
 887}
 888
 889func (imap *ImapOperations) handleAnalysisPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
 890	return imap.getEmailAnalysisPrompt(req.Arguments)
 891}
 892
 893func (imap *ImapOperations) handleSearchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
 894	return imap.getEmailSearchPrompt(req.Arguments)
 895}
 896
 897func (imap *ImapOperations) getEmailAnalysisPrompt(args map[string]interface{}) (mcp.GetPromptResult, error) {
 898	prompt := `Analyze the provided email content and provide insights including:
 899
 9001. **Sentiment Analysis**: Overall tone (positive, negative, neutral)
 9012. **Key Points**: Main topics and important information
 9023. **Action Items**: Any tasks, requests, or follow-ups needed
 9034. **Summary**: Concise summary of the email content
 9045. **Priority Level**: Assessment of urgency/importance
 9056. **Category**: Type of email (business, personal, newsletter, etc.)
 906
 907Please provide your analysis in a structured format with clear sections.`
 908
 909	if folder, ok := args["folder"].(string); ok {
 910		if uid, ok := args["uid"].(string); ok {
 911			prompt += fmt.Sprintf("\n\nAnalyze the email in folder '%s' with UID '%s'.", folder, uid)
 912		}
 913	}
 914
 915	if query, ok := args["search_query"].(string); ok {
 916		prompt += fmt.Sprintf("\n\nFind and analyze emails matching: %s", query)
 917	}
 918
 919	return mcp.GetPromptResult{
 920		Messages: []mcp.PromptMessage{
 921			{Role: "user", Content: mcp.TextContent{Type: "text", Text: prompt}},
 922		},
 923	}, nil
 924}
 925
 926func (imap *ImapOperations) getEmailSearchPrompt(args map[string]interface{}) (mcp.GetPromptResult, error) {
 927	query, ok := args["query"].(string)
 928	if !ok {
 929		return mcp.GetPromptResult{}, fmt.Errorf("query parameter is required")
 930	}
 931
 932	folder := "INBOX"
 933	if f, ok := args["folder"].(string); ok {
 934		folder = f
 935	}
 936
 937	dateRange := ""
 938	if dr, ok := args["date_range"].(string); ok {
 939		dateRange = dr
 940	}
 941
 942	prompt := fmt.Sprintf(`Search for emails with the following criteria:
 943
 944**Search Query**: %s
 945**Folder**: %s`, query, folder)
 946
 947	if dateRange != "" {
 948		prompt += fmt.Sprintf("\n**Date Range**: %s", dateRange)
 949	}
 950
 951	prompt += `
 952
 953Please provide:
 9541. **Search Strategy**: How to best find relevant emails
 9552. **Refined Query**: Optimized search terms and filters
 9563. **Expected Results**: What types of emails we might find
 9574. **Follow-up Actions**: Suggested next steps after finding results
 958
 959Use IMAP search capabilities effectively to find the most relevant emails.`
 960
 961	return mcp.GetPromptResult{
 962		Messages: []mcp.PromptMessage{
 963			{Role: "user", Content: mcp.TextContent{Type: "text", Text: prompt}},
 964		},
 965	}, nil
 966}
 967
 968func (imap *ImapOperations) handleDeleteMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 969	if err := imap.ensureConnection(); err != nil {
 970		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
 971	}
 972
 973	folder := "INBOX"
 974	if f, ok := req.Arguments["folder"].(string); ok {
 975		folder = f
 976	}
 977
 978	var uid uint32
 979	if u, ok := req.Arguments["uid"].(float64); ok {
 980		uid = uint32(u)
 981	} else if u, ok := req.Arguments["uid"].(string); ok {
 982		if parsed, err := strconv.ParseUint(u, 10, 32); err == nil {
 983			uid = uint32(parsed)
 984		}
 985	}
 986
 987	if uid == 0 {
 988		return mcp.NewToolError("uid parameter is required"), nil
 989	}
 990
 991	// Check for confirmation flag
 992	confirmed := false
 993	if c, ok := req.Arguments["confirmed"].(bool); ok {
 994		confirmed = c
 995	}
 996
 997	if !confirmed {
 998		return mcp.CallToolResult{
 999			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)}},
1000		}, nil
1001	}
1002
1003	imap.mu.RLock()
1004	client := imap.client
1005	imap.mu.RUnlock()
1006
1007	if _, err := client.Select(folder, false); err != nil {
1008		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
1009	}
1010
1011	seqset := new(goimap.SeqSet)
1012	seqset.AddNum(uid)
1013
1014	// Mark as deleted
1015	operation := goimap.FormatFlagsOp(goimap.AddFlags, true)
1016	flags := []interface{}{goimap.DeletedFlag}
1017
1018	ch := make(chan *goimap.Message, 1)
1019	if err := client.UidStore(seqset, operation, flags, ch); err != nil {
1020		return mcp.NewToolError(fmt.Sprintf("Failed to mark message as deleted: %v", err)), nil
1021	}
1022	// Drain the channel
1023	for range ch {
1024	}
1025
1026	// Expunge to permanently delete
1027	if err := client.Expunge(nil); err != nil {
1028		return mcp.NewToolError(fmt.Sprintf("Failed to expunge message: %v", err)), nil
1029	}
1030
1031	return mcp.CallToolResult{
1032		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Message UID %d permanently deleted from folder '%s'", uid, folder)}},
1033	}, nil
1034}
1035
1036func (imap *ImapOperations) handleDeleteMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
1037	if err := imap.ensureConnection(); err != nil {
1038		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
1039	}
1040
1041	folder := "INBOX"
1042	if f, ok := req.Arguments["folder"].(string); ok {
1043		folder = f
1044	}
1045
1046	// Get UIDs array
1047	var uids []uint32
1048	if uidArray, ok := req.Arguments["uids"].([]interface{}); ok {
1049		for _, u := range uidArray {
1050			switch v := u.(type) {
1051			case float64:
1052				uids = append(uids, uint32(v))
1053			case string:
1054				if parsed, err := strconv.ParseUint(v, 10, 32); err == nil {
1055					uids = append(uids, uint32(parsed))
1056				}
1057			}
1058		}
1059	}
1060
1061	if len(uids) == 0 {
1062		return mcp.NewToolError("uids parameter is required (array of message UIDs)"), nil
1063	}
1064
1065	// Check for confirmation flag
1066	confirmed := false
1067	if c, ok := req.Arguments["confirmed"].(bool); ok {
1068		confirmed = c
1069	}
1070
1071	if !confirmed {
1072		return mcp.CallToolResult{
1073			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)}},
1074		}, nil
1075	}
1076
1077	imap.mu.RLock()
1078	client := imap.client
1079	imap.mu.RUnlock()
1080
1081	if _, err := client.Select(folder, false); err != nil {
1082		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
1083	}
1084
1085	seqset := new(goimap.SeqSet)
1086	seqset.AddNum(uids...)
1087
1088	// Mark as deleted
1089	operation := goimap.FormatFlagsOp(goimap.AddFlags, true)
1090	flags := []interface{}{goimap.DeletedFlag}
1091
1092	ch := make(chan *goimap.Message, 10)
1093	if err := client.UidStore(seqset, operation, flags, ch); err != nil {
1094		return mcp.NewToolError(fmt.Sprintf("Failed to mark messages as deleted: %v", err)), nil
1095	}
1096	// Drain the channel
1097	for range ch {
1098	}
1099
1100	// Expunge to permanently delete
1101	if err := client.Expunge(nil); err != nil {
1102		return mcp.NewToolError(fmt.Sprintf("Failed to expunge messages: %v", err)), nil
1103	}
1104
1105	return mcp.CallToolResult{
1106		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("%d messages permanently deleted from folder '%s'", len(uids), folder)}},
1107	}, nil
1108}
1109
1110func (imap *ImapOperations) handleMoveToTrash(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
1111	if err := imap.ensureConnection(); err != nil {
1112		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
1113	}
1114
1115	folder := "INBOX"
1116	if f, ok := req.Arguments["folder"].(string); ok {
1117		folder = f
1118	}
1119
1120	trashFolder := "[Gmail]/Trash"
1121	if tf, ok := req.Arguments["trash_folder"].(string); ok {
1122		trashFolder = tf
1123	}
1124
1125	// Get UIDs (support both single uid and uids array)
1126	var uids []uint32
1127	if u, ok := req.Arguments["uid"].(float64); ok {
1128		uids = append(uids, uint32(u))
1129	} else if u, ok := req.Arguments["uid"].(string); ok {
1130		if parsed, err := strconv.ParseUint(u, 10, 32); err == nil {
1131			uids = append(uids, uint32(parsed))
1132		}
1133	} else if uidArray, ok := req.Arguments["uids"].([]interface{}); ok {
1134		for _, u := range uidArray {
1135			switch v := u.(type) {
1136			case float64:
1137				uids = append(uids, uint32(v))
1138			case string:
1139				if parsed, err := strconv.ParseUint(v, 10, 32); err == nil {
1140					uids = append(uids, uint32(parsed))
1141				}
1142			}
1143		}
1144	}
1145
1146	if len(uids) == 0 {
1147		return mcp.NewToolError("uid or uids parameter is required"), nil
1148	}
1149
1150	imap.mu.RLock()
1151	client := imap.client
1152	imap.mu.RUnlock()
1153
1154	if _, err := client.Select(folder, false); err != nil {
1155		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
1156	}
1157
1158	seqset := new(goimap.SeqSet)
1159	seqset.AddNum(uids...)
1160
1161	// Try to use MOVE command (Gmail supports this)
1162	if err := client.UidMove(seqset, trashFolder); err != nil {
1163		// Fallback to copy + mark deleted + expunge
1164		if err := client.UidCopy(seqset, trashFolder); err != nil {
1165			return mcp.NewToolError(fmt.Sprintf("Failed to copy messages to trash: %v", err)), nil
1166		}
1167
1168		// Mark original messages as deleted
1169		operation := goimap.FormatFlagsOp(goimap.AddFlags, true)
1170		flags := []interface{}{goimap.DeletedFlag}
1171
1172		ch := make(chan *goimap.Message, 10)
1173		if err := client.UidStore(seqset, operation, flags, ch); err != nil {
1174			return mcp.NewToolError(fmt.Sprintf("Failed to mark messages as deleted: %v", err)), nil
1175		}
1176		// Drain the channel
1177		for range ch {
1178		}
1179
1180		if err := client.Expunge(nil); err != nil {
1181			return mcp.NewToolError(fmt.Sprintf("Failed to expunge messages: %v", err)), nil
1182		}
1183	}
1184
1185	return mcp.CallToolResult{
1186		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("%d message(s) moved to trash folder '%s'", len(uids), trashFolder)}},
1187	}, nil
1188}
1189
1190func (imap *ImapOperations) handleExpungeFolder(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
1191	if err := imap.ensureConnection(); err != nil {
1192		return mcp.NewToolError(fmt.Sprintf("Connection failed: %v", err)), nil
1193	}
1194
1195	folder := "INBOX"
1196	if f, ok := req.Arguments["folder"].(string); ok {
1197		folder = f
1198	}
1199
1200	// Check for confirmation flag
1201	confirmed := false
1202	if c, ok := req.Arguments["confirmed"].(bool); ok {
1203		confirmed = c
1204	}
1205
1206	if !confirmed {
1207		return mcp.CallToolResult{
1208			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)}},
1209		}, nil
1210	}
1211
1212	imap.mu.RLock()
1213	client := imap.client
1214	imap.mu.RUnlock()
1215
1216	if _, err := client.Select(folder, false); err != nil {
1217		return mcp.NewToolError(fmt.Sprintf("Failed to select folder %s: %v", folder, err)), nil
1218	}
1219
1220	if err := client.Expunge(nil); err != nil {
1221		return mcp.NewToolError(fmt.Sprintf("Failed to expunge folder: %v", err)), nil
1222	}
1223
1224	return mcp.CallToolResult{
1225		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("All deleted messages permanently removed from folder '%s'", folder)}},
1226	}, nil
1227}
1228
1229func (imap *ImapOperations) Close() error {
1230	imap.mu.Lock()
1231	defer imap.mu.Unlock()
1232
1233	if imap.client != nil {
1234		imap.client.Close()
1235		imap.client = nil
1236	}
1237	imap.connected = false
1238	return nil
1239}