EmailWatcherAgent monitors Outlook for new emails and sends Gotify notifications based on configurable rules (importance, sender, keywords, spam score). AIEmailAgent uses Claude API in an agentic loop to evaluate emails and decide actions (notify, move, flag, analyze spam). Program.cs updated to support --agent CLI mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
305 lines
9.4 KiB
C#
305 lines
9.4 KiB
C#
using Microsoft.Extensions.Hosting;
|
|
using System.Net.Http.Json;
|
|
using NetOffice.OutlookApi;
|
|
using NetOffice.OutlookApi.Enums;
|
|
using EmailSearch.SpamDetection;
|
|
using OutlookApp = NetOffice.OutlookApi.Application;
|
|
using Exception = System.Exception;
|
|
|
|
namespace EmailSearch.Agents;
|
|
|
|
/// <summary>
|
|
/// Background agent that watches for new emails and sends notifications
|
|
/// when interesting emails arrive (e.g., high-priority, from specific senders, or spam detected).
|
|
/// </summary>
|
|
public class EmailWatcherAgent : BackgroundService
|
|
{
|
|
private readonly EmailWatcherConfig _config;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly SpamDetector _spamDetector;
|
|
private OutlookApp? _outlookApp;
|
|
private _NameSpace? _namespace;
|
|
private MAPIFolder? _watchFolder;
|
|
private Items? _items;
|
|
|
|
public EmailWatcherAgent(EmailWatcherConfig config)
|
|
{
|
|
_config = config;
|
|
_httpClient = new HttpClient();
|
|
_spamDetector = new SpamDetector();
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
Console.WriteLine($"[EmailWatcher] Starting... watching folder: {_config.WatchFolder}");
|
|
|
|
try
|
|
{
|
|
InitializeOutlook();
|
|
Console.WriteLine("[EmailWatcher] Connected to Outlook. Waiting for new emails...");
|
|
|
|
// Keep service alive
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[EmailWatcher] Error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void InitializeOutlook()
|
|
{
|
|
_outlookApp = new OutlookApp();
|
|
_namespace = _outlookApp.GetNamespace("MAPI");
|
|
|
|
// Get the folder to watch
|
|
_watchFolder = _config.WatchFolder.Equals("Inbox", StringComparison.OrdinalIgnoreCase)
|
|
? _namespace.GetDefaultFolder(OlDefaultFolders.olFolderInbox)
|
|
: FindFolder(_namespace, _config.WatchFolder);
|
|
|
|
if (_watchFolder == null)
|
|
throw new Exception($"Folder '{_config.WatchFolder}' not found");
|
|
|
|
_items = (Items)_watchFolder.Items;
|
|
|
|
// Subscribe to new email events
|
|
_items.ItemAddEvent += OnNewEmailReceived;
|
|
|
|
Console.WriteLine($"[EmailWatcher] Watching folder: {_watchFolder.Name}");
|
|
}
|
|
|
|
private void OnNewEmailReceived(object item)
|
|
{
|
|
if (item is not MailItem mail)
|
|
return;
|
|
|
|
try
|
|
{
|
|
Console.WriteLine($"[EmailWatcher] New email: {mail.Subject}");
|
|
|
|
// Check if this email is "interesting" based on configured rules
|
|
var notification = EvaluateEmail(mail);
|
|
|
|
if (notification != null)
|
|
{
|
|
// Fire and forget the notification
|
|
_ = Task.Run(() => SendNotificationAsync(notification));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[EmailWatcher] Error processing email: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private NotificationPayload? EvaluateEmail(MailItem mail)
|
|
{
|
|
var subject = mail.Subject ?? "(No Subject)";
|
|
var sender = mail.SenderName ?? mail.SenderEmailAddress ?? "Unknown";
|
|
var senderEmail = mail.SenderEmailAddress ?? "";
|
|
|
|
// Rule 1: High importance emails
|
|
if (_config.NotifyOnHighImportance && mail.Importance == OlImportance.olImportanceHigh)
|
|
{
|
|
return new NotificationPayload
|
|
{
|
|
Title = "High Priority Email",
|
|
Message = $"From: {sender}\n{subject}",
|
|
Priority = 8
|
|
};
|
|
}
|
|
|
|
// Rule 2: Specific senders
|
|
if (_config.WatchedSenders.Any(s =>
|
|
senderEmail.Contains(s, StringComparison.OrdinalIgnoreCase) ||
|
|
sender.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return new NotificationPayload
|
|
{
|
|
Title = $"Email from {sender}",
|
|
Message = subject,
|
|
Priority = 7
|
|
};
|
|
}
|
|
|
|
// Rule 3: Keyword matches in subject
|
|
var matchedKeyword = _config.WatchedKeywords
|
|
.FirstOrDefault(k => subject.Contains(k, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (matchedKeyword != null)
|
|
{
|
|
return new NotificationPayload
|
|
{
|
|
Title = $"Email matching '{matchedKeyword}'",
|
|
Message = $"From: {sender}\n{subject}",
|
|
Priority = 6
|
|
};
|
|
}
|
|
|
|
// Rule 4: Spam detection alert
|
|
if (_config.NotifyOnSpam)
|
|
{
|
|
var spamResult = _spamDetector.Analyze(mail);
|
|
if (spamResult.FinalScore >= _config.SpamThreshold)
|
|
{
|
|
return new NotificationPayload
|
|
{
|
|
Title = $"Spam Detected ({spamResult.FinalScore:P0})",
|
|
Message = $"From: {sender}\n{subject}\nFlags: {string.Join(", ", spamResult.RedFlags.Take(3))}",
|
|
Priority = 5
|
|
};
|
|
}
|
|
}
|
|
|
|
// Rule 5: Notify on all emails (if enabled)
|
|
if (_config.NotifyOnAll)
|
|
{
|
|
return new NotificationPayload
|
|
{
|
|
Title = "New Email",
|
|
Message = $"From: {sender}\n{subject}",
|
|
Priority = 3
|
|
};
|
|
}
|
|
|
|
return null; // Not interesting
|
|
}
|
|
|
|
private async Task SendNotificationAsync(NotificationPayload notification)
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(_config.GotifyUrl) && !string.IsNullOrEmpty(_config.GotifyToken))
|
|
{
|
|
await SendGotifyAsync(notification);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(_config.WebhookUrl))
|
|
{
|
|
await SendWebhookAsync(notification);
|
|
}
|
|
|
|
// Console output as fallback
|
|
Console.WriteLine($"[NOTIFY] {notification.Title}: {notification.Message}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[EmailWatcher] Failed to send notification: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task SendGotifyAsync(NotificationPayload notification)
|
|
{
|
|
var url = $"{_config.GotifyUrl.TrimEnd('/')}/message?token={_config.GotifyToken}";
|
|
|
|
var payload = new
|
|
{
|
|
title = notification.Title,
|
|
message = notification.Message,
|
|
priority = notification.Priority
|
|
};
|
|
|
|
var response = await _httpClient.PostAsJsonAsync(url, payload);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
Console.WriteLine($"[Gotify] Failed: {response.StatusCode}");
|
|
}
|
|
}
|
|
|
|
private async Task SendWebhookAsync(NotificationPayload notification)
|
|
{
|
|
var payload = new
|
|
{
|
|
title = notification.Title,
|
|
message = notification.Message,
|
|
priority = notification.Priority,
|
|
timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
await _httpClient.PostAsJsonAsync(_config.WebhookUrl!, payload);
|
|
}
|
|
|
|
private static MAPIFolder? FindFolder(_NameSpace ns, string folderName)
|
|
{
|
|
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 { }
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
Console.WriteLine("[EmailWatcher] Shutting down...");
|
|
|
|
_items?.Dispose();
|
|
_watchFolder?.Dispose();
|
|
_namespace?.Dispose();
|
|
_outlookApp?.Dispose();
|
|
_httpClient.Dispose();
|
|
|
|
base.Dispose();
|
|
}
|
|
}
|
|
|
|
public class NotificationPayload
|
|
{
|
|
public string Title { get; set; } = "";
|
|
public string Message { get; set; } = "";
|
|
public int Priority { get; set; } = 5; // Gotify: 1-10
|
|
}
|
|
|
|
public class EmailWatcherConfig
|
|
{
|
|
public string WatchFolder { get; set; } = "Inbox";
|
|
|
|
// Notification destinations
|
|
public string? GotifyUrl { get; set; } // e.g., "http://gotify.local:8080"
|
|
public string? GotifyToken { get; set; } // App token from Gotify
|
|
public string? WebhookUrl { get; set; } // Generic webhook endpoint
|
|
|
|
// What to notify on
|
|
public bool NotifyOnHighImportance { get; set; } = true;
|
|
public bool NotifyOnSpam { get; set; } = true;
|
|
public double SpamThreshold { get; set; } = 0.7;
|
|
public bool NotifyOnAll { get; set; } = false;
|
|
|
|
// Watch lists
|
|
public List<string> WatchedSenders { get; set; } = new(); // Email addresses or names
|
|
public List<string> WatchedKeywords { get; set; } = new(); // Subject keywords
|
|
}
|