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 }