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;
///
/// Background agent that watches for new emails and sends notifications
/// when interesting emails arrive (e.g., high-priority, from specific senders, or spam detected).
///
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 WatchedSenders { get; set; } = new(); // Email addresses or names
public List WatchedKeywords { get; set; } = new(); // Subject keywords
}