feat: add MoveToJunk, AnalyzeSpam, and ScanForSpam MCP tools
MoveToJunk moves emails to Junk by subject/date. AnalyzeSpam returns a detailed spam report with score, red flags, and sender/content analysis. ScanForSpam batch-scans recent emails and ranks by spam likelihood. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ using System.Text;
|
|||||||
using OutlookApp = NetOffice.OutlookApi.Application;
|
using OutlookApp = NetOffice.OutlookApi.Application;
|
||||||
using NetOffice.OutlookApi;
|
using NetOffice.OutlookApi;
|
||||||
using NetOffice.OutlookApi.Enums;
|
using NetOffice.OutlookApi.Enums;
|
||||||
|
using EmailSearch.SpamDetection;
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public class EmailSearchTools
|
public class EmailSearchTools
|
||||||
@@ -75,6 +76,41 @@ 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.")]
|
||||||
|
public static string MoveToJunk(
|
||||||
|
[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 to search for the email: Inbox, SentMail, Drafts, All, or any custom folder name (default Inbox)")] string folder = "Inbox")
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
var junkFolder = ns.GetDefaultFolder(OlDefaultFolders.olFolderJunk);
|
||||||
|
|
||||||
|
foreach (var mailFolder in foldersToSearch)
|
||||||
|
{
|
||||||
|
var mail = FindEmail(mailFolder, subject, targetDate);
|
||||||
|
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 subject and date.";
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
return $"Error moving email to junk: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Read the full body of a specific email by subject and date. Use after SearchEmails to get complete email content.")]
|
[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(
|
public static string ReadEmail(
|
||||||
[Description("Exact or partial subject line to match")] string subject,
|
[Description("Exact or partial subject line to match")] string subject,
|
||||||
@@ -105,6 +141,198 @@ 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.")]
|
||||||
|
public static string AnalyzeSpam(
|
||||||
|
[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, All, or any custom folder name (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)
|
||||||
|
{
|
||||||
|
var detector = new SpamDetector();
|
||||||
|
var result = detector.Analyze(mail);
|
||||||
|
return FormatSpamAnalysis(mail, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Email not found with the specified subject and date.";
|
||||||
|
}
|
||||||
|
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($" 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)
|
private static List<MAPIFolder> GetFoldersToSearch(_NameSpace ns, string folder)
|
||||||
{
|
{
|
||||||
var folders = new List<MAPIFolder>();
|
var folders = new List<MAPIFolder>();
|
||||||
|
|||||||
Reference in New Issue
Block a user