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}