main
   1package maildir
   2
   3import (
   4	"encoding/json"
   5	"fmt"
   6	"io"
   7	"mime"
   8	"mime/multipart"
   9	"net/mail"
  10	"os"
  11	"path/filepath"
  12	"regexp"
  13	"sort"
  14	"strings"
  15	"time"
  16
  17	"github.com/xlgmokha/mcp/pkg/mcp"
  18)
  19
  20// MaildirOperations provides maildir email analysis operations
  21type MaildirOperations struct {
  22	allowedPaths []string
  23}
  24
  25// MessageInfo represents basic email message information
  26type MessageInfo struct {
  27	ID          string            `json:"id"`
  28	Filename    string            `json:"filename"`
  29	Subject     string            `json:"subject"`
  30	From        string            `json:"from"`
  31	To          string            `json:"to"`
  32	Date        time.Time         `json:"date"`
  33	Flags       []string          `json:"flags"`
  34	Folder      string            `json:"folder"`
  35	Size        int64             `json:"size"`
  36	Headers     map[string]string `json:"headers"`
  37	MessageID   string            `json:"message_id"`
  38	InReplyTo   string            `json:"in_reply_to"`
  39	References  []string          `json:"references"`
  40}
  41
  42// Message represents a full email message
  43type Message struct {
  44	MessageInfo
  45	Body     string `json:"body"`
  46	HTMLBody string `json:"html_body,omitempty"`
  47}
  48
  49// FolderInfo represents maildir folder information
  50type FolderInfo struct {
  51	Name         string `json:"name"`
  52	Path         string `json:"path"`
  53	MessageCount int    `json:"message_count"`
  54	UnreadCount  int    `json:"unread_count"`
  55}
  56
  57// ContactInfo represents contact analysis data
  58type ContactInfo struct {
  59	Email      string    `json:"email"`
  60	Name       string    `json:"name"`
  61	Count      int       `json:"count"`
  62	LastSeen   time.Time `json:"last_seen"`
  63	FirstSeen  time.Time `json:"first_seen"`
  64	IsOutgoing bool      `json:"is_outgoing"`
  65}
  66
  67// NewMaildirOperations creates a new MaildirOperations helper
  68func NewMaildirOperations(allowedPaths []string) *MaildirOperations {
  69	// Normalize and validate allowed paths
  70	normalizedPaths := make([]string, len(allowedPaths))
  71	for i, path := range allowedPaths {
  72		absPath, err := filepath.Abs(expandHome(path))
  73		if err != nil {
  74			panic(fmt.Sprintf("Invalid maildir path: %s", path))
  75		}
  76		normalizedPaths[i] = filepath.Clean(absPath)
  77	}
  78
  79	return &MaildirOperations{
  80		allowedPaths: normalizedPaths,
  81	}
  82}
  83
  84// New creates a new Maildir MCP server
  85func New(allowedPaths []string) *mcp.Server {
  86	maildir := NewMaildirOperations(allowedPaths)
  87	builder := mcp.NewServerBuilder("maildir-server", "1.0.0")
  88
  89	// Add maildir_scan_folders tool
  90	builder.AddTool(mcp.NewTool("maildir_scan_folders", "Scan and list all folders in the Maildir structure", map[string]interface{}{
  91		"type": "object",
  92	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
  93		return maildir.handleScanFolders(req)
  94	}))
  95
  96	// Add maildir_list_messages tool
  97	builder.AddTool(mcp.NewTool("maildir_list_messages", "List messages in a specific folder with optional pagination and filtering", map[string]interface{}{
  98		"type": "object",
  99		"properties": map[string]interface{}{
 100			"folder": map[string]interface{}{
 101				"type":        "string",
 102				"description": "The folder name to list messages from",
 103			},
 104			"limit": map[string]interface{}{
 105				"type":        "integer",
 106				"description": "Maximum number of messages to return",
 107				"minimum":     1,
 108				"default":     50,
 109			},
 110			"offset": map[string]interface{}{
 111				"type":        "integer",
 112				"description": "Number of messages to skip",
 113				"minimum":     0,
 114				"default":     0,
 115			},
 116			"unread_only": map[string]interface{}{
 117				"type":        "boolean",
 118				"description": "Only return unread messages",
 119				"default":     false,
 120			},
 121		},
 122		"required": []string{"folder"},
 123	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 124		return maildir.handleListMessages(req)
 125	}))
 126
 127	// Add maildir_read_message tool
 128	builder.AddTool(mcp.NewTool("maildir_read_message", "Read the full content of a specific message", map[string]interface{}{
 129		"type": "object",
 130		"properties": map[string]interface{}{
 131			"message_id": map[string]interface{}{
 132				"type":        "string",
 133				"description": "The unique message ID",
 134			},
 135		},
 136		"required": []string{"message_id"},
 137	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 138		return maildir.handleReadMessage(req)
 139	}))
 140
 141	// Add maildir_search_messages tool
 142	builder.AddTool(mcp.NewTool("maildir_search_messages", "Search for messages across all folders using various criteria", map[string]interface{}{
 143		"type": "object",
 144		"properties": map[string]interface{}{
 145			"query": map[string]interface{}{
 146				"type":        "string",
 147				"description": "Search query string",
 148			},
 149			"sender": map[string]interface{}{
 150				"type":        "string",
 151				"description": "Filter by sender email or name",
 152			},
 153			"subject": map[string]interface{}{
 154				"type":        "string",
 155				"description": "Filter by subject line",
 156			},
 157			"folder": map[string]interface{}{
 158				"type":        "string",
 159				"description": "Limit search to specific folder",
 160			},
 161			"limit": map[string]interface{}{
 162				"type":        "integer",
 163				"description": "Maximum number of results",
 164				"minimum":     1,
 165				"default":     50,
 166			},
 167		},
 168	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 169		return maildir.handleSearchMessages(req)
 170	}))
 171
 172	// Add maildir_get_thread tool
 173	builder.AddTool(mcp.NewTool("maildir_get_thread", "Get all messages in a conversation thread", map[string]interface{}{
 174		"type": "object",
 175		"properties": map[string]interface{}{
 176			"message_id": map[string]interface{}{
 177				"type":        "string",
 178				"description": "Message ID to find thread for",
 179			},
 180		},
 181		"required": []string{"message_id"},
 182	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 183		return maildir.handleGetThread(req)
 184	}))
 185
 186	// Add maildir_analyze_contacts tool
 187	builder.AddTool(mcp.NewTool("maildir_analyze_contacts", "Analyze contact frequency and relationship data from email history", map[string]interface{}{
 188		"type": "object",
 189		"properties": map[string]interface{}{
 190			"limit": map[string]interface{}{
 191				"type":        "integer",
 192				"description": "Maximum number of contacts to analyze",
 193				"minimum":     1,
 194				"default":     100,
 195			},
 196		},
 197	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 198		return maildir.handleAnalyzeContacts(req)
 199	}))
 200
 201	// Add maildir_get_statistics tool
 202	builder.AddTool(mcp.NewTool("maildir_get_statistics", "Get comprehensive statistics about the maildir including message counts, storage usage, and activity patterns", map[string]interface{}{
 203		"type": "object",
 204	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 205		return maildir.handleGetStatistics(req)
 206	}))
 207
 208	// Add maildir resources  
 209	builder.AddResource(mcp.NewResource("maildir://folders", "Maildir Folders", "Browse available maildir folders and structures", func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
 210		return mcp.ReadResourceResult{
 211			Contents: []mcp.Content{
 212				mcp.NewTextContent("Use maildir_scan_folders tool to explore available maildir folders and their message counts"),
 213			},
 214		}, nil
 215	}))
 216
 217	return builder.Build()
 218}
 219
 220
 221// Helper methods for MaildirOperations
 222
 223
 224
 225// HandleScanFolders implements the maildir_scan_folders tool
 226func (maildir *MaildirOperations) handleScanFolders(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 227	maildirPath, ok := req.Arguments["maildir_path"].(string)
 228	if !ok {
 229		return mcp.NewToolError("maildir_path is required"), nil
 230	}
 231
 232	if !maildir.isPathAllowed(maildirPath) {
 233		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 234	}
 235
 236	includeCounts := true
 237	if ic, ok := req.Arguments["include_counts"].(bool); ok {
 238		includeCounts = ic
 239	}
 240
 241	folders, err := maildir.scanFolders(maildirPath, includeCounts)
 242	if err != nil {
 243		return mcp.NewToolError(fmt.Sprintf("failed to scan folders: %v", err)), nil
 244	}
 245
 246	result, err := json.Marshal(map[string]interface{}{
 247		"folders": folders,
 248	})
 249	if err != nil {
 250		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 251	}
 252
 253	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 254}
 255
 256// HandleListMessages implements the maildir_list_messages tool
 257func (maildir *MaildirOperations) handleListMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 258	maildirPath, ok := req.Arguments["maildir_path"].(string)
 259	if !ok {
 260		return mcp.NewToolError("maildir_path is required"), nil
 261	}
 262
 263	if !maildir.isPathAllowed(maildirPath) {
 264		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 265	}
 266
 267	folder := "INBOX"
 268	if f, ok := req.Arguments["folder"].(string); ok {
 269		folder = f
 270	}
 271
 272	limit := 50
 273	if l, ok := req.Arguments["limit"].(float64); ok {
 274		limit = int(l)
 275		if limit > 200 {
 276			limit = 200
 277		}
 278	}
 279
 280	offset := 0
 281	if o, ok := req.Arguments["offset"].(float64); ok {
 282		offset = int(o)
 283	}
 284
 285	messages, total, err := maildir.listMessages(maildirPath, folder, limit, offset, req.Arguments)
 286	if err != nil {
 287		return mcp.NewToolError(fmt.Sprintf("failed to list messages: %v", err)), nil
 288	}
 289
 290	result, err := json.Marshal(map[string]interface{}{
 291		"messages": messages,
 292		"total":    total,
 293		"offset":   offset,
 294		"limit":    limit,
 295	})
 296	if err != nil {
 297		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 298	}
 299
 300	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 301}
 302
 303// HandleReadMessage implements the maildir_read_message tool
 304func (maildir *MaildirOperations) handleReadMessage(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 305	maildirPath, ok := req.Arguments["maildir_path"].(string)
 306	if !ok {
 307		return mcp.NewToolError("maildir_path is required"), nil
 308	}
 309
 310	if !maildir.isPathAllowed(maildirPath) {
 311		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 312	}
 313
 314	messageID, ok := req.Arguments["message_id"].(string)
 315	if !ok {
 316		return mcp.NewToolError("message_id is required"), nil
 317	}
 318
 319	includeHTML := false
 320	if ih, ok := req.Arguments["include_html"].(bool); ok {
 321		includeHTML = ih
 322	}
 323
 324	includeHeaders := true
 325	if ih, ok := req.Arguments["include_headers"].(bool); ok {
 326		includeHeaders = ih
 327	}
 328
 329	sanitizeContent := true
 330	if sc, ok := req.Arguments["sanitize_content"].(bool); ok {
 331		sanitizeContent = sc
 332	}
 333
 334	message, err := maildir.readMessage(maildirPath, messageID, includeHTML, includeHeaders, sanitizeContent)
 335	if err != nil {
 336		return mcp.NewToolError(fmt.Sprintf("failed to read message: %v", err)), nil
 337	}
 338
 339	result, err := json.Marshal(message)
 340	if err != nil {
 341		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 342	}
 343
 344	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 345}
 346
 347// HandleSearchMessages implements the maildir_search_messages tool
 348func (maildir *MaildirOperations) handleSearchMessages(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 349	maildirPath, ok := req.Arguments["maildir_path"].(string)
 350	if !ok {
 351		return mcp.NewToolError("maildir_path is required"), nil
 352	}
 353
 354	if !maildir.isPathAllowed(maildirPath) {
 355		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 356	}
 357
 358	query, ok := req.Arguments["query"].(string)
 359	if !ok {
 360		return mcp.NewToolError("query is required"), nil
 361	}
 362
 363	limit := 50
 364	if l, ok := req.Arguments["limit"].(float64); ok {
 365		limit = int(l)
 366		if limit > 200 {
 367			limit = 200
 368		}
 369	}
 370
 371	results, err := maildir.searchMessages(maildirPath, query, limit, req.Arguments)
 372	if err != nil {
 373		return mcp.NewToolError(fmt.Sprintf("failed to search messages: %v", err)), nil
 374	}
 375
 376	result, err := json.Marshal(map[string]interface{}{
 377		"results": results,
 378		"query":   query,
 379	})
 380	if err != nil {
 381		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 382	}
 383
 384	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 385}
 386
 387// HandleGetThread implements the maildir_get_thread tool
 388func (maildir *MaildirOperations) handleGetThread(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 389	maildirPath, ok := req.Arguments["maildir_path"].(string)
 390	if !ok {
 391		return mcp.NewToolError("maildir_path is required"), nil
 392	}
 393
 394	if !maildir.isPathAllowed(maildirPath) {
 395		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 396	}
 397
 398	messageID, ok := req.Arguments["message_id"].(string)
 399	if !ok {
 400		return mcp.NewToolError("message_id is required"), nil
 401	}
 402
 403	maxDepth := 50
 404	if md, ok := req.Arguments["max_depth"].(float64); ok {
 405		maxDepth = int(md)
 406	}
 407
 408	thread, err := maildir.getThread(maildirPath, messageID, maxDepth)
 409	if err != nil {
 410		return mcp.NewToolError(fmt.Sprintf("failed to get thread: %v", err)), nil
 411	}
 412
 413	result, err := json.Marshal(map[string]interface{}{
 414		"thread": thread,
 415	})
 416	if err != nil {
 417		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 418	}
 419
 420	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 421}
 422
 423// HandleAnalyzeContacts implements the maildir_analyze_contacts tool
 424func (maildir *MaildirOperations) handleAnalyzeContacts(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 425	maildirPath, ok := req.Arguments["maildir_path"].(string)
 426	if !ok {
 427		return mcp.NewToolError("maildir_path is required"), nil
 428	}
 429
 430	if !maildir.isPathAllowed(maildirPath) {
 431		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 432	}
 433
 434	contacts, err := maildir.analyzeContacts(maildirPath, req.Arguments)
 435	if err != nil {
 436		return mcp.NewToolError(fmt.Sprintf("failed to analyze contacts: %v", err)), nil
 437	}
 438
 439	result, err := json.Marshal(map[string]interface{}{
 440		"contacts": contacts,
 441	})
 442	if err != nil {
 443		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 444	}
 445
 446	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 447}
 448
 449// HandleGetStatistics implements the maildir_get_statistics tool
 450func (maildir *MaildirOperations) handleGetStatistics(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 451	maildirPath, ok := req.Arguments["maildir_path"].(string)
 452	if !ok {
 453		return mcp.NewToolError("maildir_path is required"), nil
 454	}
 455
 456	if !maildir.isPathAllowed(maildirPath) {
 457		return mcp.NewToolError("access denied: path not in allowed directories"), nil
 458	}
 459
 460	stats, err := maildir.getStatistics(maildirPath, req.Arguments)
 461	if err != nil {
 462		return mcp.NewToolError(fmt.Sprintf("failed to get statistics: %v", err)), nil
 463	}
 464
 465	result, err := json.Marshal(stats)
 466	if err != nil {
 467		return mcp.NewToolError(fmt.Sprintf("failed to marshal result: %v", err)), nil
 468	}
 469
 470	return mcp.NewToolResult(mcp.NewTextContent(string(result))), nil
 471}
 472
 473
 474// Helper functions
 475
 476// isPathAllowed checks if the given path is within allowed directories
 477func (maildir *MaildirOperations) isPathAllowed(path string) bool {
 478	absPath, err := filepath.Abs(expandHome(path))
 479	if err != nil {
 480		return false
 481	}
 482	absPath = filepath.Clean(absPath)
 483
 484	for _, allowedPath := range maildir.allowedPaths {
 485		if strings.HasPrefix(absPath, allowedPath) {
 486			return true
 487		}
 488	}
 489	return false
 490}
 491
 492// expandHome expands ~ to user's home directory
 493func expandHome(path string) string {
 494	if strings.HasPrefix(path, "~/") {
 495		home, err := os.UserHomeDir()
 496		if err != nil {
 497			return path
 498		}
 499		return filepath.Join(home, path[2:])
 500	}
 501	return path
 502}
 503
 504// scanFolders scans maildir folders and returns folder information
 505func (maildir *MaildirOperations) scanFolders(maildirPath string, includeCounts bool) ([]FolderInfo, error) {
 506	var folders []FolderInfo
 507
 508	// Walk the maildir directory to find folders
 509	err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
 510		if err != nil {
 511			return nil // Skip directories with errors
 512		}
 513
 514		if !info.IsDir() {
 515			return nil
 516		}
 517
 518		// Check if this looks like a maildir folder (has cur, new, tmp subdirs)
 519		curDir := filepath.Join(path, "cur")
 520		newDir := filepath.Join(path, "new")
 521		tmpDir := filepath.Join(path, "tmp")
 522
 523		if _, err := os.Stat(curDir); err == nil {
 524			if _, err := os.Stat(newDir); err == nil {
 525				if _, err := os.Stat(tmpDir); err == nil {
 526					// This is a maildir folder
 527					relPath, _ := filepath.Rel(maildirPath, path)
 528					if relPath == "." {
 529						relPath = "INBOX"
 530					}
 531
 532					folder := FolderInfo{
 533						Name: relPath,
 534						Path: path,
 535					}
 536
 537					if includeCounts {
 538						// Count messages in cur and new directories
 539						curCount := maildir.countMessagesInDir(curDir)
 540						newCount := maildir.countMessagesInDir(newDir)
 541						folder.MessageCount = curCount + newCount
 542						folder.UnreadCount = newCount
 543					}
 544
 545					folders = append(folders, folder)
 546				}
 547			}
 548		}
 549
 550		return nil
 551	})
 552
 553	if err != nil {
 554		return nil, err
 555	}
 556
 557	// Sort folders by name
 558	sort.Slice(folders, func(i, j int) bool {
 559		return folders[i].Name < folders[j].Name
 560	})
 561
 562	return folders, nil
 563}
 564
 565// countMessagesInDir counts the number of message files in a directory
 566func (maildir *MaildirOperations) countMessagesInDir(dirPath string) int {
 567	entries, err := os.ReadDir(dirPath)
 568	if err != nil {
 569		return 0
 570	}
 571
 572	count := 0
 573	for _, entry := range entries {
 574		if !entry.IsDir() && maildir.isMaildirMessage(entry.Name()) {
 575			count++
 576		}
 577	}
 578	return count
 579}
 580
 581// isMaildirMessage checks if a filename looks like a maildir message
 582func (maildir *MaildirOperations) isMaildirMessage(filename string) bool {
 583	// Basic check for maildir message format
 584	return strings.Contains(filename, ".") && !strings.HasPrefix(filename, ".")
 585}
 586
 587// listMessages lists messages in a specific folder with filtering and pagination
 588func (maildir *MaildirOperations) listMessages(maildirPath, folder string, limit, offset int, filters map[string]interface{}) ([]MessageInfo, int, error) {
 589	folderPath := filepath.Join(maildirPath, folder)
 590	if folder == "INBOX" && maildirPath == folderPath {
 591		// Handle case where INBOX might be the root maildir
 592		folderPath = maildirPath
 593	}
 594
 595	var allMessages []MessageInfo
 596
 597	// Scan cur and new directories
 598	for _, subdir := range []string{"cur", "new"} {
 599		dirPath := filepath.Join(folderPath, subdir)
 600		messages, err := maildir.scanMessagesInDir(dirPath, folder, subdir == "new")
 601		if err != nil {
 602			continue // Skip if directory doesn't exist or can't be read
 603		}
 604		allMessages = append(allMessages, messages...)
 605	}
 606
 607	// Apply filters
 608	filteredMessages := maildir.applyMessageFilters(allMessages, filters)
 609
 610	// Sort by date (newest first)
 611	sort.Slice(filteredMessages, func(i, j int) bool {
 612		return filteredMessages[i].Date.After(filteredMessages[j].Date)
 613	})
 614
 615	total := len(filteredMessages)
 616
 617	// Apply pagination
 618	start := offset
 619	if start >= total {
 620		return []MessageInfo{}, total, nil
 621	}
 622
 623	end := start + limit
 624	if end > total {
 625		end = total
 626	}
 627
 628	return filteredMessages[start:end], total, nil
 629}
 630
 631// scanMessagesInDir scans messages in a specific directory
 632func (maildir *MaildirOperations) scanMessagesInDir(dirPath, folder string, isNew bool) ([]MessageInfo, error) {
 633	entries, err := os.ReadDir(dirPath)
 634	if err != nil {
 635		return nil, err
 636	}
 637
 638	var messages []MessageInfo
 639	for _, entry := range entries {
 640		if entry.IsDir() || !maildir.isMaildirMessage(entry.Name()) {
 641			continue
 642		}
 643
 644		messagePath := filepath.Join(dirPath, entry.Name())
 645		message, err := maildir.parseMessageInfo(messagePath, folder, entry.Name(), isNew)
 646		if err != nil {
 647			continue // Skip malformed messages
 648		}
 649
 650		messages = append(messages, message)
 651	}
 652
 653	return messages, nil
 654}
 655
 656// parseMessageInfo parses basic message information from a maildir message file
 657func (maildir *MaildirOperations) parseMessageInfo(messagePath, folder, filename string, isNew bool) (MessageInfo, error) {
 658	file, err := os.Open(messagePath)
 659	if err != nil {
 660		return MessageInfo{}, err
 661	}
 662	defer file.Close()
 663
 664	// Get file info for size
 665	fileInfo, err := file.Stat()
 666	if err != nil {
 667		return MessageInfo{}, err
 668	}
 669
 670	// Parse email headers
 671	msg, err := mail.ReadMessage(file)
 672	if err != nil {
 673		return MessageInfo{}, err
 674	}
 675
 676	// Extract flags from filename
 677	flags := maildir.parseMaildirFlags(filename, isNew)
 678
 679	// Parse date
 680	dateStr := msg.Header.Get("Date")
 681	date, err := mail.ParseDate(dateStr)
 682	if err != nil {
 683		date = fileInfo.ModTime() // Fallback to file modification time
 684	}
 685
 686	// Extract references for threading
 687	references := maildir.parseReferences(msg.Header.Get("References"))
 688
 689	messageInfo := MessageInfo{
 690		ID:        maildir.generateMessageID(messagePath),
 691		Filename:  filename,
 692		Subject:   msg.Header.Get("Subject"),
 693		From:      msg.Header.Get("From"),
 694		To:        msg.Header.Get("To"),
 695		Date:      date,
 696		Flags:     flags,
 697		Folder:    folder,
 698		Size:      fileInfo.Size(),
 699		MessageID: msg.Header.Get("Message-ID"),
 700		InReplyTo: msg.Header.Get("In-Reply-To"),
 701		References: references,
 702		Headers: map[string]string{
 703			"Subject":    msg.Header.Get("Subject"),
 704			"From":       msg.Header.Get("From"),
 705			"To":         msg.Header.Get("To"),
 706			"Date":       dateStr,
 707			"Message-ID": msg.Header.Get("Message-ID"),
 708		},
 709	}
 710
 711	return messageInfo, nil
 712}
 713
 714// parseMaildirFlags parses maildir flags from filename
 715func (maildir *MaildirOperations) parseMaildirFlags(filename string, isNew bool) []string {
 716	var flags []string
 717
 718	if isNew {
 719		flags = append(flags, "New")
 720	}
 721
 722	// Parse standard maildir flags from filename
 723	// Format: unique_name:2,flags
 724	parts := strings.Split(filename, ":2,")
 725	if len(parts) == 2 {
 726		flagStr := parts[1]
 727		for _, flag := range flagStr {
 728			switch flag {
 729			case 'S':
 730				flags = append(flags, "Seen")
 731			case 'R':
 732				flags = append(flags, "Replied")
 733			case 'F':
 734				flags = append(flags, "Flagged")
 735			case 'T':
 736				flags = append(flags, "Trashed")
 737			case 'D':
 738				flags = append(flags, "Draft")
 739			case 'P':
 740				flags = append(flags, "Passed")
 741			}
 742		}
 743	} else if !isNew {
 744		// If no flags but in cur directory, assume Seen
 745		flags = append(flags, "Seen")
 746	}
 747
 748	return flags
 749}
 750
 751// parseReferences parses References header for email threading
 752func (maildir *MaildirOperations) parseReferences(referencesStr string) []string {
 753	if referencesStr == "" {
 754		return nil
 755	}
 756
 757	// Simple parsing - split by whitespace and extract message IDs
 758	re := regexp.MustCompile(`<[^>]+>`)
 759	matches := re.FindAllString(referencesStr, -1)
 760	
 761	var references []string
 762	for _, match := range matches {
 763		references = append(references, match)
 764	}
 765	
 766	return references
 767}
 768
 769// generateMessageID generates a unique ID for a message based on its path
 770func (maildir *MaildirOperations) generateMessageID(messagePath string) string {
 771	// Use the filename without path as the ID
 772	return filepath.Base(messagePath)
 773}
 774
 775// applyMessageFilters applies various filters to messages
 776func (maildir *MaildirOperations) applyMessageFilters(messages []MessageInfo, filters map[string]interface{}) []MessageInfo {
 777	var filtered []MessageInfo
 778
 779	for _, msg := range messages {
 780		if maildir.messageMatchesFilters(msg, filters) {
 781			filtered = append(filtered, msg)
 782		}
 783	}
 784
 785	return filtered
 786}
 787
 788// messageMatchesFilters checks if a message matches the given filters
 789func (maildir *MaildirOperations) messageMatchesFilters(msg MessageInfo, filters map[string]interface{}) bool {
 790	// Date range filter
 791	if dateFromStr, ok := filters["date_from"].(string); ok {
 792		if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
 793			if msg.Date.Before(dateFrom) {
 794				return false
 795			}
 796		}
 797	}
 798
 799	if dateToStr, ok := filters["date_to"].(string); ok {
 800		if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
 801			if msg.Date.After(dateTo.Add(24 * time.Hour)) {
 802				return false
 803			}
 804		}
 805	}
 806
 807	// Sender filter
 808	if sender, ok := filters["sender"].(string); ok {
 809		if !strings.Contains(strings.ToLower(msg.From), strings.ToLower(sender)) {
 810			return false
 811		}
 812	}
 813
 814	// Subject filter
 815	if subjectContains, ok := filters["subject_contains"].(string); ok {
 816		if !strings.Contains(strings.ToLower(msg.Subject), strings.ToLower(subjectContains)) {
 817			return false
 818		}
 819	}
 820
 821	// Unread only filter
 822	if unreadOnly, ok := filters["unread_only"].(bool); ok && unreadOnly {
 823		hasNewFlag := false
 824		for _, flag := range msg.Flags {
 825			if flag == "New" {
 826				hasNewFlag = true
 827				break
 828			}
 829		}
 830		if !hasNewFlag {
 831			return false
 832		}
 833	}
 834
 835	return true
 836}
 837
 838// readMessage reads a full message with content
 839func (maildir *MaildirOperations) readMessage(maildirPath, messageID string, includeHTML, includeHeaders, sanitizeContent bool) (*Message, error) {
 840	// Find the message file
 841	messagePath, err := maildir.findMessagePath(maildirPath, messageID)
 842	if err != nil {
 843		return nil, err
 844	}
 845
 846	file, err := os.Open(messagePath)
 847	if err != nil {
 848		return nil, err
 849	}
 850	defer file.Close()
 851
 852	// Parse the message
 853	msg, err := mail.ReadMessage(file)
 854	if err != nil {
 855		return nil, err
 856	}
 857
 858	// Get file info for message info
 859	_, err = file.Stat()
 860	if err != nil {
 861		return nil, err
 862	}
 863
 864	// Parse basic message info
 865	folder := maildir.extractFolderFromPath(messagePath)
 866	isNew := strings.Contains(messagePath, "/new/")
 867	messageInfo, err := maildir.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
 868	if err != nil {
 869		return nil, err
 870	}
 871
 872	// Extract message body
 873	body, htmlBody, err := maildir.extractMessageBody(msg, includeHTML)
 874	if err != nil {
 875		return nil, err
 876	}
 877
 878	if sanitizeContent {
 879		body = maildir.sanitizeContent(body)
 880		if htmlBody != "" {
 881			htmlBody = maildir.sanitizeContent(htmlBody)
 882		}
 883	}
 884
 885	message := &Message{
 886		MessageInfo: messageInfo,
 887		Body:        body,
 888		HTMLBody:    htmlBody,
 889	}
 890
 891	return message, nil
 892}
 893
 894// findMessagePath finds the full path to a message file by ID
 895func (maildir *MaildirOperations) findMessagePath(maildirPath, messageID string) (string, error) {
 896	var foundPath string
 897
 898	err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
 899		if err != nil {
 900			return nil
 901		}
 902
 903		if !info.IsDir() && filepath.Base(path) == messageID {
 904			foundPath = path
 905			return filepath.SkipAll
 906		}
 907
 908		return nil
 909	})
 910
 911	if err != nil {
 912		return "", err
 913	}
 914
 915	if foundPath == "" {
 916		return "", fmt.Errorf("message not found: %s", messageID)
 917	}
 918
 919	return foundPath, nil
 920}
 921
 922// extractFolderFromPath extracts folder name from message path
 923func (maildir *MaildirOperations) extractFolderFromPath(messagePath string) string {
 924	// Navigate up from the message file to find the folder
 925	dir := filepath.Dir(messagePath) // cur or new directory
 926	dir = filepath.Dir(dir)          // folder directory
 927	return filepath.Base(dir)
 928}
 929
 930// extractMessageBody extracts plain text and HTML body from message
 931func (maildir *MaildirOperations) extractMessageBody(msg *mail.Message, includeHTML bool) (string, string, error) {
 932	contentType := msg.Header.Get("Content-Type")
 933	
 934	if contentType == "" {
 935		// Plain text message
 936		body, err := io.ReadAll(msg.Body)
 937		if err != nil {
 938			return "", "", err
 939		}
 940		return string(body), "", nil
 941	}
 942
 943	mediaType, params, err := mime.ParseMediaType(contentType)
 944	if err != nil {
 945		// Fallback to reading as plain text
 946		body, err := io.ReadAll(msg.Body)
 947		if err != nil {
 948			return "", "", err
 949		}
 950		return string(body), "", nil
 951	}
 952
 953	if strings.HasPrefix(mediaType, "text/plain") {
 954		body, err := io.ReadAll(msg.Body)
 955		if err != nil {
 956			return "", "", err
 957		}
 958		return string(body), "", nil
 959	}
 960
 961	if strings.HasPrefix(mediaType, "text/html") {
 962		body, err := io.ReadAll(msg.Body)
 963		if err != nil {
 964			return "", "", err
 965		}
 966		htmlBody := string(body)
 967		plainBody := maildir.htmlToPlainText(htmlBody)
 968		if includeHTML {
 969			return plainBody, htmlBody, nil
 970		}
 971		return plainBody, "", nil
 972	}
 973
 974	if strings.HasPrefix(mediaType, "multipart/") {
 975		return maildir.extractMultipartBody(msg.Body, params["boundary"], includeHTML)
 976	}
 977
 978	// Fallback to reading as plain text
 979	body, err := io.ReadAll(msg.Body)
 980	if err != nil {
 981		return "", "", err
 982	}
 983	return string(body), "", nil
 984}
 985
 986// extractMultipartBody extracts body from multipart message
 987func (maildir *MaildirOperations) extractMultipartBody(body io.Reader, boundary string, includeHTML bool) (string, string, error) {
 988	mr := multipart.NewReader(body, boundary)
 989	
 990	var plainBody, htmlBody string
 991	
 992	for {
 993		part, err := mr.NextPart()
 994		if err == io.EOF {
 995			break
 996		}
 997		if err != nil {
 998			return "", "", err
 999		}
1000
1001		contentType := part.Header.Get("Content-Type")
1002		mediaType, _, _ := mime.ParseMediaType(contentType)
1003		
1004		partBody, err := io.ReadAll(part)
1005		if err != nil {
1006			continue
1007		}
1008
1009		switch mediaType {
1010		case "text/plain":
1011			plainBody = string(partBody)
1012		case "text/html":
1013			htmlBody = string(partBody)
1014		}
1015	}
1016
1017	// If we only have HTML, convert it to plain text
1018	if plainBody == "" && htmlBody != "" {
1019		plainBody = maildir.htmlToPlainText(htmlBody)
1020	}
1021
1022	if includeHTML {
1023		return plainBody, htmlBody, nil
1024	}
1025	return plainBody, "", nil
1026}
1027
1028// htmlToPlainText converts HTML to plain text (simple implementation)
1029func (maildir *MaildirOperations) htmlToPlainText(html string) string {
1030	// Simple HTML to text conversion - remove tags
1031	re := regexp.MustCompile(`<[^>]*>`)
1032	text := re.ReplaceAllString(html, "")
1033	
1034	// Decode common HTML entities
1035	text = strings.ReplaceAll(text, "&amp;", "&")
1036	text = strings.ReplaceAll(text, "&lt;", "<")
1037	text = strings.ReplaceAll(text, "&gt;", ">")
1038	text = strings.ReplaceAll(text, "&quot;", "\"")
1039	text = strings.ReplaceAll(text, "&#39;", "'")
1040	text = strings.ReplaceAll(text, "&nbsp;", " ")
1041	
1042	return strings.TrimSpace(text)
1043}
1044
1045// sanitizeContent sanitizes message content (basic implementation)
1046func (maildir *MaildirOperations) sanitizeContent(content string) string {
1047	// Basic PII masking - this is a simple implementation
1048	// In production, you'd want more sophisticated PII detection
1049	
1050	// Mask phone numbers
1051	phoneRegex := regexp.MustCompile(`\b\d{3}-\d{3}-\d{4}\b`)
1052	content = phoneRegex.ReplaceAllString(content, "XXX-XXX-XXXX")
1053	
1054	// Mask SSNs
1055	ssnRegex := regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`)
1056	content = ssnRegex.ReplaceAllString(content, "XXX-XX-XXXX")
1057	
1058	return content
1059}
1060
1061// searchMessages performs full-text search across messages
1062func (maildir *MaildirOperations) searchMessages(maildirPath, query string, limit int, filters map[string]interface{}) ([]MessageInfo, error) {
1063	var results []MessageInfo
1064	
1065	// Simple implementation - scan all messages and search in subject/from/body
1066	err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
1067		if err != nil || info.IsDir() || !maildir.isMaildirMessage(info.Name()) {
1068			return nil
1069		}
1070
1071		// Skip if not in cur or new directory
1072		if !strings.Contains(path, "/cur/") && !strings.Contains(path, "/new/") {
1073			return nil
1074		}
1075
1076		matches, messageInfo := maildir.searchInMessage(path, query)
1077		if matches {
1078			results = append(results, messageInfo)
1079		}
1080
1081		return nil
1082	})
1083
1084	if err != nil {
1085		return nil, err
1086	}
1087
1088	// Sort by date (newest first)
1089	sort.Slice(results, func(i, j int) bool {
1090		return results[i].Date.After(results[j].Date)
1091	})
1092
1093	// Apply limit
1094	if len(results) > limit {
1095		results = results[:limit]
1096	}
1097
1098	return results, nil
1099}
1100
1101// searchInMessage searches for query in a specific message
1102func (maildir *MaildirOperations) searchInMessage(messagePath, query string) (bool, MessageInfo) {
1103	file, err := os.Open(messagePath)
1104	if err != nil {
1105		return false, MessageInfo{}
1106	}
1107	defer file.Close()
1108
1109	folder := maildir.extractFolderFromPath(messagePath)
1110	isNew := strings.Contains(messagePath, "/new/")
1111	messageInfo, err := maildir.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
1112	if err != nil {
1113		return false, MessageInfo{}
1114	}
1115
1116	queryLower := strings.ToLower(query)
1117
1118	// Search in subject, from, to
1119	if strings.Contains(strings.ToLower(messageInfo.Subject), queryLower) ||
1120		strings.Contains(strings.ToLower(messageInfo.From), queryLower) ||
1121		strings.Contains(strings.ToLower(messageInfo.To), queryLower) {
1122		return true, messageInfo
1123	}
1124
1125	// Search in message body (basic implementation)
1126	file.Seek(0, 0)
1127	msg, err := mail.ReadMessage(file)
1128	if err != nil {
1129		return false, messageInfo
1130	}
1131
1132	body, _, err := maildir.extractMessageBody(msg, false)
1133	if err != nil {
1134		return false, messageInfo
1135	}
1136
1137	if strings.Contains(strings.ToLower(body), queryLower) {
1138		return true, messageInfo
1139	}
1140
1141	return false, messageInfo
1142}
1143
1144// getThread retrieves email thread for a message
1145func (maildir *MaildirOperations) getThread(maildirPath, messageID string, maxDepth int) ([]MessageInfo, error) {
1146	// Find the starting message
1147	messagePath, err := maildir.findMessagePath(maildirPath, messageID)
1148	if err != nil {
1149		return nil, err
1150	}
1151
1152	folder := maildir.extractFolderFromPath(messagePath)
1153	isNew := strings.Contains(messagePath, "/new/")
1154	startMessage, err := maildir.parseMessageInfo(messagePath, folder, filepath.Base(messagePath), isNew)
1155	if err != nil {
1156		return nil, err
1157	}
1158
1159	// Simple threading implementation - find messages with matching Message-ID, In-Reply-To, References
1160	thread := []MessageInfo{startMessage}
1161	
1162	// This is a simplified implementation
1163	// A full implementation would build a proper thread tree
1164	
1165	return thread, nil
1166}
1167
1168// analyzeContacts analyzes contact information from messages
1169func (maildir *MaildirOperations) analyzeContacts(maildirPath string, filters map[string]interface{}) ([]ContactInfo, error) {
1170	contactMap := make(map[string]*ContactInfo)
1171
1172	err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
1173		if err != nil || info.IsDir() || !maildir.isMaildirMessage(info.Name()) {
1174			return nil
1175		}
1176
1177		// Skip if not in cur or new directory
1178		if !strings.Contains(path, "/cur/") && !strings.Contains(path, "/new/") {
1179			return nil
1180		}
1181
1182		folder := maildir.extractFolderFromPath(path)
1183		isNew := strings.Contains(path, "/new/")
1184		messageInfo, err := maildir.parseMessageInfo(path, folder, filepath.Base(path), isNew)
1185		if err != nil {
1186			return nil
1187		}
1188
1189		// Process From address
1190		if messageInfo.From != "" {
1191			maildir.processContact(contactMap, messageInfo.From, messageInfo.Date, false)
1192		}
1193
1194		// Process To addresses
1195		if messageInfo.To != "" {
1196			toAddresses := strings.Split(messageInfo.To, ",")
1197			for _, addr := range toAddresses {
1198				maildir.processContact(contactMap, strings.TrimSpace(addr), messageInfo.Date, true)
1199			}
1200		}
1201
1202		return nil
1203	})
1204
1205	if err != nil {
1206		return nil, err
1207	}
1208
1209	// Convert map to slice and sort by message count
1210	var contacts []ContactInfo
1211	for _, contact := range contactMap {
1212		contacts = append(contacts, *contact)
1213	}
1214
1215	sort.Slice(contacts, func(i, j int) bool {
1216		return contacts[i].Count > contacts[j].Count
1217	})
1218
1219	return contacts, nil
1220}
1221
1222// processContact processes a contact address for analysis
1223func (maildir *MaildirOperations) processContact(contactMap map[string]*ContactInfo, address string, date time.Time, isOutgoing bool) {
1224	// Extract email address from "Name <email>" format
1225	addr, err := mail.ParseAddress(address)
1226	if err != nil {
1227		// Fallback for simple email addresses
1228		addr = &mail.Address{Address: address}
1229	}
1230
1231	email := strings.ToLower(addr.Address)
1232	if email == "" {
1233		return
1234	}
1235
1236	contact, exists := contactMap[email]
1237	if !exists {
1238		contact = &ContactInfo{
1239			Email:      email,
1240			Name:       addr.Name,
1241			Count:      0,
1242			FirstSeen:  date,
1243			LastSeen:   date,
1244			IsOutgoing: isOutgoing,
1245		}
1246		contactMap[email] = contact
1247	}
1248
1249	contact.Count++
1250	if date.After(contact.LastSeen) {
1251		contact.LastSeen = date
1252	}
1253	if date.Before(contact.FirstSeen) {
1254		contact.FirstSeen = date
1255	}
1256
1257	// Update name if we have a better one
1258	if addr.Name != "" && contact.Name == "" {
1259		contact.Name = addr.Name
1260	}
1261}
1262
1263// getStatistics generates maildir usage statistics
1264func (maildir *MaildirOperations) getStatistics(maildirPath string, filters map[string]interface{}) (map[string]interface{}, error) {
1265	stats := make(map[string]interface{})
1266	
1267	// Basic statistics implementation
1268	totalMessages := 0
1269	totalSize := int64(0)
1270	folderCounts := make(map[string]int)
1271
1272	err := filepath.Walk(maildirPath, func(path string, info os.FileInfo, err error) error {
1273		if err != nil || info.IsDir() || !maildir.isMaildirMessage(info.Name()) {
1274			return nil
1275		}
1276
1277		// Skip if not in cur or new directory
1278		if !strings.Contains(path, "/cur/") && !strings.Contains(path, "/new/") {
1279			return nil
1280		}
1281
1282		totalMessages++
1283		totalSize += info.Size()
1284
1285		folder := maildir.extractFolderFromPath(path)
1286		folderCounts[folder]++
1287
1288		return nil
1289	})
1290
1291	if err != nil {
1292		return nil, err
1293	}
1294
1295	stats["total_messages"] = totalMessages
1296	stats["total_size_bytes"] = totalSize
1297	stats["folder_counts"] = folderCounts
1298
1299	return stats, nil
1300}