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>
This commit is contained in:
@@ -2,6 +2,7 @@ using NetOffice.OutlookApi;
|
|||||||
|
|
||||||
public class EmailResult
|
public class EmailResult
|
||||||
{
|
{
|
||||||
|
public string EntryId { get; set; } = "";
|
||||||
public string Subject { get; set; } = "";
|
public string Subject { get; set; } = "";
|
||||||
public string Sender { get; set; } = "";
|
public string Sender { get; set; } = "";
|
||||||
public string CC { get; set; } = "";
|
public string CC { get; set; } = "";
|
||||||
@@ -22,6 +23,7 @@ public class EmailResult
|
|||||||
|
|
||||||
return new EmailResult
|
return new EmailResult
|
||||||
{
|
{
|
||||||
|
EntryId = mail.EntryID ?? "",
|
||||||
Subject = mail.Subject ?? "(No Subject)",
|
Subject = mail.Subject ?? "(No Subject)",
|
||||||
Sender = $"{mail.SenderName} <{mail.SenderEmailAddress}>",
|
Sender = $"{mail.SenderName} <{mail.SenderEmailAddress}>",
|
||||||
CC = mail.CC ?? "",
|
CC = mail.CC ?? "",
|
||||||
|
|||||||
+161
-29
@@ -76,37 +76,53 @@ public class EmailSearchTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Move an email to the Junk folder by subject and date. Use after SearchEmails to identify the email to move.")]
|
[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(
|
public static string MoveToJunk(
|
||||||
[Description("Exact or partial subject line to match")] string subject,
|
[Description("Entry ID of the email (from SearchEmails/ScanForSpam results). Most reliable way to identify an email.")] string? entryId = null,
|
||||||
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy)")] string date,
|
[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("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)
|
[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
|
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))
|
if (!TryParseDate(date, out var targetDate))
|
||||||
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
||||||
|
|
||||||
TimeSpan? targetTime = TryParseTime(time);
|
TimeSpan? targetTime = TryParseTime(time);
|
||||||
|
|
||||||
using var outlookApp = new OutlookApp();
|
|
||||||
var ns = outlookApp.GetNamespace("MAPI");
|
|
||||||
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
||||||
var junkFolder = ns.GetDefaultFolder(OlDefaultFolders.olFolderJunk);
|
|
||||||
|
|
||||||
foreach (var mailFolder in foldersToSearch)
|
foreach (var mailFolder in foldersToSearch)
|
||||||
{
|
{
|
||||||
var mail = FindEmail(mailFolder, subject, targetDate, targetTime);
|
mail = FindEmail(mailFolder, subject, targetDate, targetTime);
|
||||||
|
if (mail != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "Please provide either entryId, or both subject and date.";
|
||||||
|
}
|
||||||
|
|
||||||
if (mail != null)
|
if (mail != null)
|
||||||
{
|
{
|
||||||
var emailSubject = mail.Subject;
|
var emailSubject = mail.Subject;
|
||||||
mail.Move(junkFolder);
|
mail.Move(junkFolder);
|
||||||
return $"Successfully moved email '{emailSubject}' to Junk folder.";
|
return $"Successfully moved email '{emailSubject}' to Junk folder.";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return "Email not found with the specified subject and date.";
|
return "Email not found with the specified criteria.";
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
@@ -114,32 +130,117 @@ public class EmailSearchTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Read the full body of a specific email by subject and date. Use after SearchEmails to get complete email content.")]
|
[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(
|
public static string ReadEmail(
|
||||||
[Description("Exact or partial subject line to match")] string subject,
|
[Description("Entry ID of the email (from SearchEmails/ScanForSpam results). Most reliable way to identify an email.")] string? entryId = null,
|
||||||
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy)")] string date,
|
[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("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)
|
[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
|
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))
|
if (!TryParseDate(date, out var targetDate))
|
||||||
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
||||||
|
|
||||||
TimeSpan? targetTime = TryParseTime(time);
|
TimeSpan? targetTime = TryParseTime(time);
|
||||||
|
|
||||||
using var outlookApp = new OutlookApp();
|
|
||||||
var ns = outlookApp.GetNamespace("MAPI");
|
|
||||||
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
||||||
|
|
||||||
foreach (var mailFolder in foldersToSearch)
|
foreach (var mailFolder in foldersToSearch)
|
||||||
{
|
{
|
||||||
var mail = FindEmail(mailFolder, subject, targetDate, targetTime);
|
mail = FindEmail(mailFolder, subject, targetDate, targetTime);
|
||||||
if (mail != null)
|
if (mail != null) break;
|
||||||
return FormatFullEmail(mail);
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "Please provide either entryId, or both subject and date.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Email not found with the specified subject and date.";
|
if (mail != null)
|
||||||
|
return FormatFullEmail(mail);
|
||||||
|
|
||||||
|
return "Email not found with the specified criteria.";
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
@@ -147,36 +248,52 @@ public class EmailSearchTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Analyze a specific email for spam indicators. Returns spam score (0.0-1.0), spam likelihood, and detected red flags.")]
|
[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(
|
public static string AnalyzeSpam(
|
||||||
[Description("Exact or partial subject line to match")] string subject,
|
[Description("Entry ID of the email (from SearchEmails/ScanForSpam results). Most reliable way to identify an email.")] string? entryId = null,
|
||||||
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy)")] string date,
|
[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("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)
|
[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
|
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))
|
if (!TryParseDate(date, out var targetDate))
|
||||||
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
||||||
|
|
||||||
TimeSpan? targetTime = TryParseTime(time);
|
TimeSpan? targetTime = TryParseTime(time);
|
||||||
|
|
||||||
using var outlookApp = new OutlookApp();
|
|
||||||
var ns = outlookApp.GetNamespace("MAPI");
|
|
||||||
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
||||||
|
|
||||||
foreach (var mailFolder in foldersToSearch)
|
foreach (var mailFolder in foldersToSearch)
|
||||||
{
|
{
|
||||||
var mail = FindEmail(mailFolder, subject, targetDate, targetTime);
|
mail = FindEmail(mailFolder, subject, targetDate, targetTime);
|
||||||
|
if (mail != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "Please provide either entryId, or both subject and date.";
|
||||||
|
}
|
||||||
|
|
||||||
if (mail != null)
|
if (mail != null)
|
||||||
{
|
{
|
||||||
var detector = new SpamDetector();
|
var detector = new SpamDetector();
|
||||||
var result = detector.Analyze(mail);
|
var result = detector.Analyze(mail);
|
||||||
return FormatSpamAnalysis(mail, result);
|
return FormatSpamAnalysis(mail, result);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return "Email not found with the specified subject and date.";
|
return "Email not found with the specified criteria.";
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
@@ -318,6 +435,7 @@ public class EmailSearchTools
|
|||||||
var emptyBar = new string('-', 10 - scoreBar.Length);
|
var emptyBar = new string('-', 10 - scoreBar.Length);
|
||||||
|
|
||||||
output.AppendLine($"[{scoreBar}{emptyBar}] {spam.FinalScore:P0} - {spam.SpamLikelihood}");
|
output.AppendLine($"[{scoreBar}{emptyBar}] {spam.FinalScore:P0} - {spam.SpamLikelihood}");
|
||||||
|
output.AppendLine($" ID: {email.EntryId}");
|
||||||
output.AppendLine($" Subject: {email.Subject}");
|
output.AppendLine($" Subject: {email.Subject}");
|
||||||
output.AppendLine($" From: {email.Sender}");
|
output.AppendLine($" From: {email.Sender}");
|
||||||
output.AppendLine($" Date: {email.ReceivedDate:yyyy-MM-dd HH:mm}");
|
output.AppendLine($" Date: {email.ReceivedDate:yyyy-MM-dd HH:mm}");
|
||||||
@@ -445,6 +563,19 @@ public class EmailSearchTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private static MailItem? FindEmail(MAPIFolder folder, string subject, DateTime targetDate, TimeSpan? targetTime = null)
|
||||||
{
|
{
|
||||||
var items = folder.Items;
|
var items = folder.Items;
|
||||||
@@ -616,6 +747,7 @@ public class EmailSearchTools
|
|||||||
{
|
{
|
||||||
var email = results[i];
|
var email = results[i];
|
||||||
output.AppendLine($"--- Email {offset + i + 1} ---");
|
output.AppendLine($"--- Email {offset + i + 1} ---");
|
||||||
|
output.AppendLine($"ID: {email.EntryId}");
|
||||||
output.AppendLine($"Subject: {email.Subject}");
|
output.AppendLine($"Subject: {email.Subject}");
|
||||||
output.AppendLine($"From: {email.Sender}");
|
output.AppendLine($"From: {email.Sender}");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user