diff --git a/EmailSearch/EmailSearchTools.cs b/EmailSearch/EmailSearchTools.cs new file mode 100644 index 0000000..ac9eedb --- /dev/null +++ b/EmailSearch/EmailSearchTools.cs @@ -0,0 +1,397 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using OutlookApp = NetOffice.OutlookApi.Application; +using NetOffice.OutlookApi; +using NetOffice.OutlookApi.Enums; + +[McpServerToolType] +public class EmailSearchTools +{ + private static readonly string[] SupportedDateFormats = + [ + "yyyy-MM-dd", + "MM/dd/yyyy", + "dd/MM/yyyy", + "yyyy/MM/dd", + "MM-dd-yyyy", + "dd-MM-yyyy" + ]; + + [McpServerTool, Description("Search emails in Outlook by keywords, sender, subject, or date range. Returns matching emails with subject, sender, date, and body preview.")] + public static string SearchEmails( + [Description("Keywords to search for in email subject and body")] string? keywords = null, + [Description("Filter by sender email or name")] string? sender = null, + [Description("Filter by subject contains")] string? subject = null, + [Description("Number of days back to search (default 365)")] int daysBack = 365, + [Description("Maximum number of results to return (default 25)")] int maxResults = 25, + [Description("Number of results to skip for pagination (default 0)")] int offset = 0, + [Description("Outlook folder to search: Inbox, SentMail, Drafts, DeletedItems, Junk, or All (default All)")] string folder = "All", + [Description("Filter by attachment: 'true' for emails with attachments, 'false' for without, or filename to search")] string? hasAttachment = null, + [Description("Filter by importance: High, Normal, or Low")] string? importance = null, + [Description("Filter by category name")] string? category = null, + [Description("Filter by flag status: Flagged, Completed, or NotFlagged")] string? flagStatus = null) + { + try + { + using var outlookApp = new OutlookApp(); + var ns = outlookApp.GetNamespace("MAPI"); + var foldersToSearch = GetFoldersToSearch(ns, folder); + var cutoffDate = DateTime.Now.AddDays(-daysBack); + + var filters = new SearchFilters + { + Keywords = keywords, + Sender = sender, + Subject = subject, + HasAttachment = hasAttachment, + Importance = ParseImportance(importance), + Category = category, + FlagStatus = ParseFlagStatus(flagStatus) + }; + + var allResults = new List(); + foreach (var mailFolder in foldersToSearch) + { + SearchFolder(mailFolder, filters, cutoffDate, maxResults + offset, allResults); + if (allResults.Count >= maxResults + offset) + break; + } + + // Apply pagination + var pagedResults = allResults.Skip(offset).Take(maxResults).ToList(); + + if (pagedResults.Count == 0) + return offset > 0 + ? $"No more emails found. Showing results {offset + 1}+ of {allResults.Count} total." + : "No emails found matching the search criteria."; + + return FormatSearchResults(pagedResults, offset, allResults.Count); + } + catch (System.Exception ex) + { + return $"Error searching emails: {ex.Message}"; + } + } + + [McpServerTool, Description("Read the full body of a specific email by subject and date. Use after SearchEmails to get complete email content.")] + public static string ReadEmail( + [Description("Exact or partial subject line to match")] string subject, + [Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy)")] string date, + [Description("Outlook folder: Inbox, SentMail, Drafts, DeletedItems, Junk, or All (default All)")] string folder = "All") + { + try + { + if (!TryParseDate(date, out var targetDate)) + return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy"; + + using var outlookApp = new OutlookApp(); + var ns = outlookApp.GetNamespace("MAPI"); + var foldersToSearch = GetFoldersToSearch(ns, folder); + + foreach (var mailFolder in foldersToSearch) + { + var mail = FindEmail(mailFolder, subject, targetDate); + if (mail != null) + return FormatFullEmail(mail); + } + + return "Email not found with the specified subject and date."; + } + catch (System.Exception ex) + { + return $"Error reading email: {ex.Message}"; + } + } + + private static List GetFoldersToSearch(_NameSpace ns, string folder) + { + var folders = new List(); + var folderMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Inbox"] = OlDefaultFolders.olFolderInbox, + ["SentMail"] = OlDefaultFolders.olFolderSentMail, + ["Sent"] = OlDefaultFolders.olFolderSentMail, + ["Drafts"] = OlDefaultFolders.olFolderDrafts, + ["DeletedItems"] = OlDefaultFolders.olFolderDeletedItems, + ["Deleted"] = OlDefaultFolders.olFolderDeletedItems, + ["Trash"] = OlDefaultFolders.olFolderDeletedItems, + ["Junk"] = OlDefaultFolders.olFolderJunk, + ["Spam"] = OlDefaultFolders.olFolderJunk + }; + + if (folder.Equals("All", StringComparison.OrdinalIgnoreCase)) + { + folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderInbox)); + folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderSentMail)); + } + else if (folderMap.TryGetValue(folder, out var olFolder)) + { + try + { + folders.Add(ns.GetDefaultFolder(olFolder)); + } + catch + { + // Folder may not exist (e.g., Archive on some configurations) + } + } + + return folders; + } + + private static MailItem? FindEmail(MAPIFolder folder, string subject, DateTime targetDate) + { + var items = folder.Items; + items.Sort("[ReceivedTime]", true); + + // Use date filter for performance + var filter = $"[ReceivedTime] >= '{targetDate:MM/dd/yyyy}' AND [ReceivedTime] < '{targetDate.AddDays(1):MM/dd/yyyy}'"; + var filteredItems = items.Restrict(filter); + + foreach (var item in filteredItems) + { + if (item is MailItem mail && + mail.Subject != null && + mail.Subject.Contains(subject, StringComparison.OrdinalIgnoreCase)) + { + return mail; + } + } + + return null; + } + + private static void SearchFolder( + MAPIFolder folder, + SearchFilters filters, + DateTime cutoffDate, + int maxResults, + List results) + { + try + { + var items = folder.Items; + items.Sort("[ReceivedTime]", true); + + var filter = $"[ReceivedTime] >= '{cutoffDate:MM/dd/yyyy}'"; + var filteredItems = items.Restrict(filter); + + foreach (var item in filteredItems) + { + if (results.Count >= maxResults) + break; + + if (item is MailItem mail && MatchesFilters(mail, filters)) + { + results.Add(EmailResult.FromMailItem(mail, folder.Name)); + } + } + } + catch + { + // Skip folders that can't be accessed + } + } + + private static bool MatchesFilters(MailItem mail, SearchFilters filters) + { + // Sender filter + if (!string.IsNullOrEmpty(filters.Sender)) + { + var senderMatch = + (mail.SenderName?.Contains(filters.Sender, StringComparison.OrdinalIgnoreCase) ?? false) || + (mail.SenderEmailAddress?.Contains(filters.Sender, StringComparison.OrdinalIgnoreCase) ?? false); + if (!senderMatch) return false; + } + + // Subject filter + if (!string.IsNullOrEmpty(filters.Subject)) + { + if (!(mail.Subject?.Contains(filters.Subject, StringComparison.OrdinalIgnoreCase) ?? false)) + return false; + } + + // Keywords filter + if (!string.IsNullOrEmpty(filters.Keywords)) + { + var keywordMatch = + (mail.Subject?.Contains(filters.Keywords, StringComparison.OrdinalIgnoreCase) ?? false) || + (mail.Body?.Contains(filters.Keywords, StringComparison.OrdinalIgnoreCase) ?? false); + if (!keywordMatch) return false; + } + + // Attachment filter + if (!string.IsNullOrEmpty(filters.HasAttachment)) + { + if (filters.HasAttachment.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + if (mail.Attachments.Count == 0) return false; + } + else if (filters.HasAttachment.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + if (mail.Attachments.Count > 0) return false; + } + else + { + // Search for attachment by filename + var hasMatchingAttachment = false; + foreach (var attachment in mail.Attachments) + { + if (attachment is Attachment att && + att.FileName.Contains(filters.HasAttachment, StringComparison.OrdinalIgnoreCase)) + { + hasMatchingAttachment = true; + break; + } + } + if (!hasMatchingAttachment) return false; + } + } + + // Importance filter + if (filters.Importance.HasValue) + { + if (mail.Importance != filters.Importance.Value) return false; + } + + // Category filter + if (!string.IsNullOrEmpty(filters.Category)) + { + if (string.IsNullOrEmpty(mail.Categories)) return false; + if (!mail.Categories.Contains(filters.Category, StringComparison.OrdinalIgnoreCase)) return false; + } + + // Flag status filter + if (filters.FlagStatus.HasValue) + { + if (mail.FlagStatus != filters.FlagStatus.Value) return false; + } + + return true; + } + + private static string FormatSearchResults(List results, int offset, int totalCount) + { + var output = new StringBuilder(); + + if (offset > 0 || totalCount > results.Count) + output.AppendLine($"Showing {offset + 1}-{offset + results.Count} of {totalCount} email(s):"); + else + output.AppendLine($"Found {results.Count} email(s):"); + + output.AppendLine(); + + for (int i = 0; i < results.Count; i++) + { + var email = results[i]; + output.AppendLine($"--- Email {offset + i + 1} ---"); + output.AppendLine($"Subject: {email.Subject}"); + output.AppendLine($"From: {email.Sender}"); + + if (!string.IsNullOrEmpty(email.CC)) + output.AppendLine($"CC: {email.CC}"); + if (!string.IsNullOrEmpty(email.BCC)) + output.AppendLine($"BCC: {email.BCC}"); + + output.AppendLine($"Date: {email.ReceivedDate:yyyy-MM-dd HH:mm}"); + output.AppendLine($"Folder: {email.Folder}"); + + if (!string.IsNullOrEmpty(email.Importance) && email.Importance != "Normal") + output.AppendLine($"Importance: {email.Importance}"); + + if (!string.IsNullOrEmpty(email.Categories)) + output.AppendLine($"Categories: {email.Categories}"); + + if (!string.IsNullOrEmpty(email.FlagStatus) && email.FlagStatus != "NotFlagged") + output.AppendLine($"Flag: {email.FlagStatus}"); + + if (email.AttachmentCount > 0) + output.AppendLine($"Attachments ({email.AttachmentCount}): {email.AttachmentNames}"); + + output.AppendLine($"Preview: {email.BodyPreview}"); + output.AppendLine(); + } + + return output.ToString(); + } + + private static string FormatFullEmail(MailItem mail) + { + var output = new StringBuilder(); + output.AppendLine($"Subject: {mail.Subject}"); + output.AppendLine($"From: {mail.SenderName} <{mail.SenderEmailAddress}>"); + output.AppendLine($"To: {mail.To}"); + + if (!string.IsNullOrEmpty(mail.CC)) + output.AppendLine($"CC: {mail.CC}"); + if (!string.IsNullOrEmpty(mail.BCC)) + output.AppendLine($"BCC: {mail.BCC}"); + + output.AppendLine($"Date: {mail.ReceivedTime:yyyy-MM-dd HH:mm}"); + + if (mail.Importance != OlImportance.olImportanceNormal) + output.AppendLine($"Importance: {mail.Importance.ToString().Replace("olImportance", "")}"); + + if (!string.IsNullOrEmpty(mail.Categories)) + output.AppendLine($"Categories: {mail.Categories}"); + + var attachmentNames = GetAttachmentNames(mail); + if (attachmentNames.Count > 0) + output.AppendLine($"Attachments ({attachmentNames.Count}): {string.Join(", ", attachmentNames)}"); + + output.AppendLine("---"); + output.AppendLine(); + output.Append(mail.Body); + + return output.ToString(); + } + + private static List GetAttachmentNames(MailItem mail) + { + var names = new List(); + foreach (var attachment in mail.Attachments) + { + if (attachment is Attachment att) + names.Add(att.FileName); + } + return names; + } + + private static bool TryParseDate(string dateString, out DateTime result) + { + return DateTime.TryParseExact( + dateString, + SupportedDateFormats, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out result); + } + + private static OlImportance? ParseImportance(string? importance) + { + if (string.IsNullOrEmpty(importance)) return null; + + return importance.ToLowerInvariant() switch + { + "high" => OlImportance.olImportanceHigh, + "low" => OlImportance.olImportanceLow, + "normal" => OlImportance.olImportanceNormal, + _ => null + }; + } + + private static OlFlagStatus? ParseFlagStatus(string? flagStatus) + { + if (string.IsNullOrEmpty(flagStatus)) return null; + + return flagStatus.ToLowerInvariant() switch + { + "flagged" or "marked" => OlFlagStatus.olFlagMarked, + "completed" or "complete" => OlFlagStatus.olFlagComplete, + "notflagged" or "none" or "clear" => OlFlagStatus.olNoFlag, + _ => null + }; + } +} diff --git a/EmailSearch/Program.cs b/EmailSearch/Program.cs new file mode 100644 index 0000000..d98069c --- /dev/null +++ b/EmailSearch/Program.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ModelContextProtocol.Server; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +var app = builder.Build(); +await app.RunAsync();