Improve IMAP reconnection and error handling in polling
Refactored PollInboxAsync to better handle IMAP connection drops and protocol errors by reconnecting and resuming processing as needed. Switched from using a using statement to explicit disposal in a finally block. Now logs and recovers from transient IMAP issues, and ensures proper cancellation handling by rethrowing OperationCanceledException. This increases service robustness and reliability.
This commit is contained in:
@@ -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<UniqueId> 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<UniqueId> 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(
|
||||
|
||||
Reference in New Issue
Block a user