diff --git a/src/SpamGuard/Services/InboxMonitorService.cs b/src/SpamGuard/Services/InboxMonitorService.cs index 14f5d6b..c876eb5 100644 --- a/src/SpamGuard/Services/InboxMonitorService.cs +++ b/src/SpamGuard/Services/InboxMonitorService.cs @@ -90,62 +90,84 @@ public sealed partial class InboxMonitorService : BackgroundService private async Task PollInboxAsync(CancellationToken ct) { - using var client = await _imapFactory.CreateConnectedClientAsync(_imap, ct); - var inbox = client.Inbox; - await inbox.OpenAsync(FolderAccess.ReadWrite, ct); + var client = await _imapFactory.CreateConnectedClientAsync(_imap, ct); + try + { + var inbox = client.Inbox; + await inbox.OpenAsync(FolderAccess.ReadWrite, ct); - // Build search query: only fetch new messages - IList uids; - if (_lastSeenUid > 0) - { - var range = new UniqueIdRange(new UniqueId(_lastSeenUid + 1), UniqueId.MaxValue); - uids = await inbox.SearchAsync(range, SearchQuery.All, ct); - } - else if (_processedUids.Count > 0) - { - // Resuming from persisted state -- scan recent messages only - uids = await inbox.SearchAsync( - SearchQuery.DeliveredAfter(DateTime.UtcNow.AddDays(-1)), ct); - } - else - { - // First ever run -- initial scan window - uids = await inbox.SearchAsync( - SearchQuery.DeliveredAfter(DateTime.UtcNow.AddDays(-_monitoring.InitialScanDays)), ct); - } - _logger.LogDebug("Found {Count} messages in inbox", uids.Count); - - // Find the spam/junk folder - var spamFolder = await FindSpamFolderAsync(client, ct); - - foreach (var uid in uids) - { - if (_processedUids.Contains(uid.Id)) + // Build search query: only fetch new messages + IList uids; + if (_lastSeenUid > 0) { - if (uid.Id > _lastSeenUid) _lastSeenUid = uid.Id; - continue; + var range = new UniqueIdRange(new UniqueId(_lastSeenUid + 1), UniqueId.MaxValue); + uids = await inbox.SearchAsync(range, SearchQuery.All, ct); } - - try + else if (_processedUids.Count > 0) { - await ProcessMessageAsync(inbox, uid, spamFolder, ct); + // Resuming from persisted state -- scan recent messages only + uids = await inbox.SearchAsync( + SearchQuery.DeliveredAfter(DateTime.UtcNow.AddDays(-1)), ct); } - catch (Exception ex) + else { - _logger.LogError(ex, "Error processing UID={Uid}", uid.Id); - _activityLog.Add(new ActivityEntry + // First ever run -- initial scan window + uids = await inbox.SearchAsync( + SearchQuery.DeliveredAfter(DateTime.UtcNow.AddDays(-_monitoring.InitialScanDays)), ct); + } + _logger.LogDebug("Found {Count} messages in inbox", uids.Count); + + // Find the spam/junk folder + var spamFolder = await FindSpamFolderAsync(client, ct); + + foreach (var uid in uids) + { + if (_processedUids.Contains(uid.Id)) { - Timestamp = DateTime.UtcNow, Sender = "", Subject = $"UID {uid.Id}", - Verdict = Verdict.Error, Reason = ex.Message, - Uid = uid.Id, AccountName = AccountName - }); - _processedUids.Add(uid.Id); + if (uid.Id > _lastSeenUid) _lastSeenUid = uid.Id; + continue; + } + + try + { + await ProcessMessageAsync(inbox, uid, spamFolder, ct); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing UID={Uid}", uid.Id); + _activityLog.Add(new ActivityEntry + { + Timestamp = DateTime.UtcNow, Sender = "", Subject = $"UID {uid.Id}", + Verdict = Verdict.Error, Reason = ex.Message, + Uid = uid.Id, AccountName = AccountName + }); + _processedUids.Add(uid.Id); + + // Reconnect if the server dropped the connection (e.g. unexpected response after MoveToAsync) + if (!client.IsConnected || ex is MailKit.Net.Imap.ImapProtocolException) + { + _logger.LogWarning("IMAP connection lost for {Account}, reconnecting", AccountName); + try { client.Dispose(); } catch { } + client = await _imapFactory.CreateConnectedClientAsync(_imap, ct); + inbox = client.Inbox; + await inbox.OpenAsync(FolderAccess.ReadWrite, ct); + spamFolder = await FindSpamFolderAsync(client, ct); + } + } + + if (uid.Id > _lastSeenUid) _lastSeenUid = uid.Id; } - if (uid.Id > _lastSeenUid) _lastSeenUid = uid.Id; + await client.DisconnectAsync(true, ct); + } + finally + { + client.Dispose(); } - - await client.DisconnectAsync(true, ct); } private async Task ProcessMessageAsync(