Files
EmailSearch/EmailSearch/EmailSearchTools.cs
T
ajandClaude Opus 4.6 6e5b756de7 feat: use entryId for reliable email lookup, add batch junk move
Subject+date matching was fragile when emails shared subjects or timestamps
were ambiguous. Expose Outlook's EntryID through search/scan results so
MoveToJunk, ReadEmail, and AnalyzeSpam can target an exact email, with
subject+date retained as a fallback. Also adds BatchMoveToJunk to move many
spam emails in one call instead of round-tripping per email.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-06 23:15:02 -04:00

870 lines
34 KiB
C#

using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using OutlookApp = NetOffice.OutlookApi.Application;
using NetOffice.OutlookApi;
using NetOffice.OutlookApi.Enums;
using EmailSearch.SpamDetection;
[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, All, or any custom folder name (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<EmailResult>();
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("Move an email to the Junk folder. Prefer using entryId (from SearchEmails/ScanForSpam results) for reliable matching. Falls back to subject+date matching.")]
public static string MoveToJunk(
[Description("Entry ID of the email (from SearchEmails/ScanForSpam results). Most reliable way to identify an email.")] string? entryId = null,
[Description("Exact or partial subject line to match (used when entryId is not provided)")] string? subject = null,
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy) (used when entryId is not provided)")] string? date = null,
[Description("Outlook folder to search for the email: Inbox, SentMail, Drafts, All, or any custom folder name (default Inbox)")] string folder = "Inbox",
[Description("Optional time to find the closest match when multiple emails share the same subject and date (e.g., '13:46', '1:46 PM')")] string? time = null)
{
try
{
using var outlookApp = new OutlookApp();
var ns = outlookApp.GetNamespace("MAPI");
var junkFolder = ns.GetDefaultFolder(OlDefaultFolders.olFolderJunk);
MailItem? mail = null;
if (!string.IsNullOrEmpty(entryId))
{
mail = FindEmailByEntryId(ns, entryId);
}
else if (!string.IsNullOrEmpty(subject) && !string.IsNullOrEmpty(date))
{
if (!TryParseDate(date, out var targetDate))
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
TimeSpan? targetTime = TryParseTime(time);
var foldersToSearch = GetFoldersToSearch(ns, folder);
foreach (var mailFolder in foldersToSearch)
{
mail = FindEmail(mailFolder, subject, targetDate, targetTime);
if (mail != null) break;
}
}
else
{
return "Please provide either entryId, or both subject and date.";
}
if (mail != null)
{
var emailSubject = mail.Subject;
mail.Move(junkFolder);
return $"Successfully moved email '{emailSubject}' to Junk folder.";
}
return "Email not found with the specified criteria.";
}
catch (System.Exception ex)
{
return $"Error moving email to junk: {ex.Message}";
}
}
[McpServerTool, Description("Move multiple emails to the Junk folder in a single operation. Pass entry IDs from SearchEmails or ScanForSpam results. Much faster than calling MoveToJunk repeatedly.")]
public static string BatchMoveToJunk(
[Description("Comma-separated entry IDs of emails to move to Junk")] string entryIds)
{
if (string.IsNullOrWhiteSpace(entryIds))
return "No entry IDs provided.";
var ids = entryIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (ids.Length == 0)
return "No valid entry IDs provided.";
try
{
using var outlookApp = new OutlookApp();
var ns = outlookApp.GetNamespace("MAPI");
var junkFolder = ns.GetDefaultFolder(OlDefaultFolders.olFolderJunk);
var moved = new List<string>();
var failed = new List<string>();
foreach (var id in ids)
{
try
{
var mail = FindEmailByEntryId(ns, id);
if (mail != null)
{
var subject = mail.Subject ?? "(No Subject)";
mail.Move(junkFolder);
moved.Add(subject);
}
else
{
failed.Add($"{id[..Math.Min(id.Length, 12)]}... (not found)");
}
}
catch (System.Exception ex)
{
failed.Add($"{id[..Math.Min(id.Length, 12)]}... ({ex.Message})");
}
}
var output = new StringBuilder();
output.AppendLine($"Batch move complete: {moved.Count} moved, {failed.Count} failed.");
if (moved.Count > 0)
{
output.AppendLine();
output.AppendLine("Moved to Junk:");
foreach (var subject in moved)
output.AppendLine($" - {subject}");
}
if (failed.Count > 0)
{
output.AppendLine();
output.AppendLine("Failed:");
foreach (var error in failed)
output.AppendLine($" - {error}");
}
return output.ToString();
}
catch (System.Exception ex)
{
return $"Error during batch move: {ex.Message}";
}
}
[McpServerTool, Description("Read the full body of a specific email. Prefer using entryId (from SearchEmails/ScanForSpam results) for reliable matching. Falls back to subject+date matching.")]
public static string ReadEmail(
[Description("Entry ID of the email (from SearchEmails/ScanForSpam results). Most reliable way to identify an email.")] string? entryId = null,
[Description("Exact or partial subject line to match (used when entryId is not provided)")] string? subject = null,
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy) (used when entryId is not provided)")] string? date = null,
[Description("Outlook folder: Inbox, SentMail, Drafts, DeletedItems, Junk, All, or any custom folder name (default All)")] string folder = "All",
[Description("Optional time to find the closest match when multiple emails share the same subject and date (e.g., '13:46', '1:46 PM')")] string? time = null)
{
try
{
using var outlookApp = new OutlookApp();
var ns = outlookApp.GetNamespace("MAPI");
MailItem? mail = null;
if (!string.IsNullOrEmpty(entryId))
{
mail = FindEmailByEntryId(ns, entryId);
}
else if (!string.IsNullOrEmpty(subject) && !string.IsNullOrEmpty(date))
{
if (!TryParseDate(date, out var targetDate))
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
TimeSpan? targetTime = TryParseTime(time);
var foldersToSearch = GetFoldersToSearch(ns, folder);
foreach (var mailFolder in foldersToSearch)
{
mail = FindEmail(mailFolder, subject, targetDate, targetTime);
if (mail != null) break;
}
}
else
{
return "Please provide either entryId, or both subject and date.";
}
if (mail != null)
return FormatFullEmail(mail);
return "Email not found with the specified criteria.";
}
catch (System.Exception ex)
{
return $"Error reading email: {ex.Message}";
}
}
[McpServerTool, Description("Analyze a specific email for spam indicators. Prefer using entryId (from SearchEmails/ScanForSpam results) for reliable matching. Returns spam score (0.0-1.0), spam likelihood, and detected red flags.")]
public static string AnalyzeSpam(
[Description("Entry ID of the email (from SearchEmails/ScanForSpam results). Most reliable way to identify an email.")] string? entryId = null,
[Description("Exact or partial subject line to match (used when entryId is not provided)")] string? subject = null,
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy) (used when entryId is not provided)")] string? date = null,
[Description("Outlook folder: Inbox, SentMail, Drafts, DeletedItems, Junk, All, or any custom folder name (default All)")] string folder = "All",
[Description("Optional time to find the closest match when multiple emails share the same subject and date (e.g., '13:46', '1:46 PM')")] string? time = null)
{
try
{
using var outlookApp = new OutlookApp();
var ns = outlookApp.GetNamespace("MAPI");
MailItem? mail = null;
if (!string.IsNullOrEmpty(entryId))
{
mail = FindEmailByEntryId(ns, entryId);
}
else if (!string.IsNullOrEmpty(subject) && !string.IsNullOrEmpty(date))
{
if (!TryParseDate(date, out var targetDate))
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
TimeSpan? targetTime = TryParseTime(time);
var foldersToSearch = GetFoldersToSearch(ns, folder);
foreach (var mailFolder in foldersToSearch)
{
mail = FindEmail(mailFolder, subject, targetDate, targetTime);
if (mail != null) break;
}
}
else
{
return "Please provide either entryId, or both subject and date.";
}
if (mail != null)
{
var detector = new SpamDetector();
var result = detector.Analyze(mail);
return FormatSpamAnalysis(mail, result);
}
return "Email not found with the specified criteria.";
}
catch (System.Exception ex)
{
return $"Error analyzing email: {ex.Message}";
}
}
[McpServerTool, Description("Scan recent emails for spam and return a summary with spam scores. Useful for identifying potential spam in your inbox.")]
public static string ScanForSpam(
[Description("Number of days back to scan (default 7)")] int daysBack = 7,
[Description("Maximum number of emails to scan (default 50)")] int maxEmails = 50,
[Description("Minimum spam score to include in results (0.0-1.0, default 0.3)")] double minScore = 0.3,
[Description("Outlook folder to scan: Inbox, SentMail, Drafts, All, or custom folder name (default Inbox)")] string folder = "Inbox")
{
try
{
using var outlookApp = new OutlookApp();
var ns = outlookApp.GetNamespace("MAPI");
var foldersToSearch = GetFoldersToSearch(ns, folder);
var cutoffDate = DateTime.Now.AddDays(-daysBack);
var detector = new SpamDetector();
var results = new List<(EmailResult email, SpamAnalysisResult spam)>();
foreach (var mailFolder in foldersToSearch)
{
try
{
var items = mailFolder.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 >= maxEmails)
break;
if (item is MailItem mail)
{
var spamResult = detector.Analyze(mail);
if (spamResult.FinalScore >= minScore)
{
var emailResult = EmailResult.FromMailItem(mail, mailFolder.Name);
results.Add((emailResult, spamResult));
}
}
}
}
catch { }
if (results.Count >= maxEmails)
break;
}
if (results.Count == 0)
return $"No emails with spam score >= {minScore:P0} found in the last {daysBack} days.";
// Sort by spam score descending
results = results.OrderByDescending(r => r.spam.FinalScore).ToList();
return FormatSpamScanResults(results, daysBack, minScore);
}
catch (System.Exception ex)
{
return $"Error scanning for spam: {ex.Message}";
}
}
private static string FormatSpamAnalysis(MailItem mail, SpamAnalysisResult result)
{
var output = new StringBuilder();
output.AppendLine("=== SPAM ANALYSIS REPORT ===");
output.AppendLine();
output.AppendLine($"Subject: {mail.Subject}");
output.AppendLine($"From: {mail.SenderName} <{mail.SenderEmailAddress}>");
output.AppendLine($"Date: {mail.ReceivedTime:yyyy-MM-dd HH:mm}");
output.AppendLine();
output.AppendLine("--- SPAM SCORE ---");
output.AppendLine($"Score: {result.FinalScore:P0}");
output.AppendLine($"Likelihood: {result.SpamLikelihood}");
output.AppendLine($"Predicted Spam: {(result.PredictedSpam ? "YES" : "No")}");
output.AppendLine();
if (result.RedFlags.Count > 0)
{
output.AppendLine("--- RED FLAGS DETECTED ---");
foreach (var flag in result.RedFlags)
{
output.AppendLine($" - {flag}");
}
output.AppendLine();
}
if (result.Features != null)
{
output.AppendLine("--- SENDER ANALYSIS ---");
output.AppendLine($"Display Name: {result.Features.DisplayName}");
output.AppendLine($"Email Address: {result.Features.FromAddress}");
output.AppendLine($"Domain: {result.Features.FromDomain}");
output.AppendLine($"Free Email Provider: {(result.Features.FreeMailboxDomain ? "Yes" : "No")}");
output.AppendLine($"Known/Trusted Domain: {(!result.Features.UnknownDomain ? "Yes" : "No")}");
output.AppendLine($"Blocklisted: {(result.Features.IsBlocklisted ? "YES" : "No")}");
output.AppendLine();
output.AppendLine("--- AUTHENTICATION ---");
output.AppendLine($"SPF Failed: {(result.Features.SpfFail ? "YES" : "No")}");
output.AppendLine($"DKIM Failed: {(result.Features.DkimFail ? "YES" : "No")}");
output.AppendLine($"DMARC Failed: {(result.Features.DmarcFail ? "YES" : "No")}");
output.AppendLine($"Reply-To Mismatch: {(result.Features.ReplyToDomainMismatch ? "YES" : "No")}");
output.AppendLine();
output.AppendLine("--- CONTENT ANALYSIS ---");
output.AppendLine($"URLs Found: {result.Features.UrlCount}");
output.AppendLine($"Uses URL Shortener: {(result.Features.UsesShortener ? "YES" : "No")}");
output.AppendLine($"IP-based URL: {(result.Features.HasIpLink ? "YES" : "No")}");
output.AppendLine($"Suspicious TLD: {(result.Features.SuspiciousTld ? "YES" : "No")}");
output.AppendLine($"Has Attachments: {(result.Features.HasAttachment ? "Yes" : "No")}");
if (result.Features.HasAttachment)
output.AppendLine($"Attachment Risk: {result.Features.AttachmentRiskScore:P0}");
output.AppendLine($"Keyword Bait: {(result.Features.KeywordBait ? "YES" : "No")}");
output.AppendLine($"Has Tracking Pixel: {(result.Features.HasTrackingPixel ? "Yes" : "No")}");
}
return output.ToString();
}
private static string FormatSpamScanResults(List<(EmailResult email, SpamAnalysisResult spam)> results, int daysBack, double minScore)
{
var output = new StringBuilder();
output.AppendLine($"=== SPAM SCAN RESULTS ===");
output.AppendLine($"Scanned last {daysBack} days, showing {results.Count} email(s) with spam score >= {minScore:P0}");
output.AppendLine();
foreach (var (email, spam) in results)
{
var scoreBar = new string('#', (int)(spam.FinalScore * 10));
var emptyBar = new string('-', 10 - scoreBar.Length);
output.AppendLine($"[{scoreBar}{emptyBar}] {spam.FinalScore:P0} - {spam.SpamLikelihood}");
output.AppendLine($" ID: {email.EntryId}");
output.AppendLine($" Subject: {email.Subject}");
output.AppendLine($" From: {email.Sender}");
output.AppendLine($" Date: {email.ReceivedDate:yyyy-MM-dd HH:mm}");
output.AppendLine($" Folder: {email.Folder}");
if (spam.RedFlags.Count > 0)
{
var topFlags = spam.RedFlags.Take(3);
output.AppendLine($" Flags: {string.Join("; ", topFlags)}");
}
output.AppendLine();
}
var highSpam = results.Count(r => r.spam.FinalScore >= 0.7);
var mediumSpam = results.Count(r => r.spam.FinalScore >= 0.5 && r.spam.FinalScore < 0.7);
output.AppendLine("--- SUMMARY ---");
output.AppendLine($"High likelihood spam (>=70%): {highSpam}");
output.AppendLine($"Medium likelihood spam (50-69%): {mediumSpam}");
output.AppendLine($"Lower likelihood ({minScore:P0}-49%): {results.Count - highSpam - mediumSpam}");
return output.ToString();
}
private static List<MAPIFolder> GetFoldersToSearch(_NameSpace ns, string folder)
{
var folders = new List<MAPIFolder>();
var folderMap = new Dictionary<string, OlDefaultFolders>(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));
// Also search subfolders of Inbox for "All"
var inbox = ns.GetDefaultFolder(OlDefaultFolders.olFolderInbox);
AddSubfolders(inbox, folders);
}
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)
}
}
else
{
// Search for custom folder by name
var customFolder = FindFolderByName(ns, folder);
if (customFolder != null)
{
folders.Add(customFolder);
}
}
return folders;
}
private static MAPIFolder? FindFolderByName(_NameSpace ns, string folderName)
{
// Search through all accounts/stores
foreach (var store in ns.Stores)
{
if (store is Store s)
{
try
{
var rootFolder = s.GetRootFolder() as MAPIFolder;
if (rootFolder != null)
{
var found = SearchFolderRecursive(rootFolder, folderName);
if (found != null)
return found;
}
}
catch
{
// Skip stores that can't be accessed
}
}
}
return null;
}
private static MAPIFolder? SearchFolderRecursive(MAPIFolder parent, string folderName)
{
foreach (var subfolder in parent.Folders)
{
if (subfolder is MAPIFolder folder)
{
if (folder.Name.Equals(folderName, StringComparison.OrdinalIgnoreCase))
return folder;
var found = SearchFolderRecursive(folder, folderName);
if (found != null)
return found;
}
}
return null;
}
private static void AddSubfolders(MAPIFolder parent, List<MAPIFolder> folders)
{
foreach (var subfolder in parent.Folders)
{
if (subfolder is MAPIFolder folder)
{
folders.Add(folder);
AddSubfolders(folder, folders);
}
}
}
private static MailItem? FindEmailByEntryId(_NameSpace ns, string entryId)
{
try
{
var item = ns.GetItemFromID(entryId);
return item as MailItem;
}
catch
{
return null;
}
}
private static MailItem? FindEmail(MAPIFolder folder, string subject, DateTime targetDate, TimeSpan? targetTime = null)
{
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);
if (targetTime.HasValue)
{
// Find the email closest to the specified time
var targetDateTime = targetDate.Date + targetTime.Value;
MailItem? bestMatch = null;
var bestDiff = TimeSpan.MaxValue;
foreach (var item in filteredItems)
{
if (item is MailItem mail &&
mail.Subject != null &&
mail.Subject.Contains(subject, StringComparison.OrdinalIgnoreCase))
{
var diff = (mail.ReceivedTime - targetDateTime).Duration();
if (diff < bestDiff)
{
bestDiff = diff;
bestMatch = mail;
}
}
}
return bestMatch;
}
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<EmailResult> 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<EmailResult> 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($"ID: {email.EntryId}");
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<string> GetAttachmentNames(MailItem mail)
{
var names = new List<string>();
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 TimeSpan? TryParseTime(string? timeString)
{
if (string.IsNullOrWhiteSpace(timeString))
return null;
if (DateTime.TryParse(timeString, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out var parsed))
return parsed.TimeOfDay;
return null;
}
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
};
}
}