Created
November 24, 2020 06:43
-
-
Save bluewalk/07d7c717d99d5511bd07731b2da5c4f4 to your computer and use it in GitHub Desktop.
Imap IDLE client (using MailKit)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class IdleClient : IDisposable | |
{ | |
private readonly string _host, _username, _password; | |
private readonly SecureSocketOptions _sslOptions; | |
private readonly int _port; | |
private readonly CancellationTokenSource _cancel; | |
private CancellationTokenSource _done; | |
private bool _messagesArrived; | |
private readonly ImapClient _client; | |
private readonly bool _deleteOnProcessed; | |
public EventHandler<MimeMessage> OnMessageReceived { get; set; } | |
public IdleClient(string host, int port, SecureSocketOptions sslOptions, string username, string password, | |
bool deleteOnProcessed = false) | |
{ | |
_client = new ImapClient(new ProtocolLogger(Console.OpenStandardError())) | |
{ | |
ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true | |
}; | |
_cancel = new CancellationTokenSource(); | |
_sslOptions = sslOptions; | |
_username = username; | |
_password = password; | |
_host = host; | |
_port = port; | |
_deleteOnProcessed = deleteOnProcessed; | |
} | |
private async Task ReconnectAsync() | |
{ | |
try | |
{ | |
if (!_client.IsConnected) | |
await _client.ConnectAsync(_host, _port, _sslOptions, _cancel.Token); | |
if (!_client.IsAuthenticated) | |
{ | |
await _client.AuthenticateAsync(_username, _password, _cancel.Token); | |
await _client.Inbox.OpenAsync(FolderAccess.ReadOnly, _cancel.Token); | |
} | |
} | |
catch | |
{ | |
if (_client.IsConnected) | |
await _client.DisconnectAsync(true, _cancel.Token); | |
if (!_cancel.IsCancellationRequested) | |
await ReconnectAsync(); | |
} | |
} | |
private async Task FetchMessagesAsync(bool print) | |
{ | |
var messages = new List<MimeMessage>(); | |
do | |
{ | |
try | |
{ | |
var inbox = _client.Inbox; | |
await inbox.OpenAsync(FolderAccess.ReadWrite); | |
var fetched = await inbox.FetchAsync(0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId, | |
_cancel.Token); | |
for (var i = 0; i < inbox.Count; i++) | |
{ | |
if (fetched[i].Flags != MessageFlags.None) continue; | |
var message = await inbox.GetMessageAsync(i, _cancel.Token); | |
messages.Add(message); | |
await inbox.AddFlagsAsync(new List<int> {i}, | |
_deleteOnProcessed ? MessageFlags.Deleted : MessageFlags.Seen, | |
true); | |
if (_deleteOnProcessed) | |
await inbox.ExpungeAsync(); | |
} | |
break; | |
} | |
catch (ImapProtocolException) | |
{ | |
// protocol exceptions often result in the client getting disconnected | |
await ReconnectAsync(); | |
} | |
catch (IOException) | |
{ | |
// I/O exceptions always result in the client getting disconnected | |
await ReconnectAsync(); | |
} | |
} while (true); | |
foreach (var message in messages) | |
OnMessageReceived?.Invoke(this, message); | |
} | |
private async Task WaitForNewMessagesAsync() | |
{ | |
do | |
{ | |
try | |
{ | |
if (_client.Capabilities.HasFlag(ImapCapabilities.Idle)) | |
{ | |
// Note: IMAP servers are only supposed to drop the connection after 30 minutes, so normally | |
// we'd IDLE for a max of, say, ~29 minutes... but GMail seems to drop idle connections after | |
// about 10 minutes, so we'll only idle for 9 minutes. | |
_done = new CancellationTokenSource(new TimeSpan(0, 9, 0)); | |
try | |
{ | |
await _client.IdleAsync(_done.Token, _cancel.Token); | |
} | |
finally | |
{ | |
_done.Dispose(); | |
_done = null; | |
} | |
} | |
else | |
{ | |
// Note: we don't want to spam the IMAP server with NOOP commands, so lets wait a minute | |
// between each NOOP command. | |
await Task.Delay(new TimeSpan(0, 1, 0), _cancel.Token); | |
await _client.NoOpAsync(_cancel.Token); | |
} | |
break; | |
} | |
catch (ImapProtocolException) | |
{ | |
// protocol exceptions often result in the client getting disconnected | |
await ReconnectAsync(); | |
} | |
catch (IOException) | |
{ | |
// I/O exceptions always result in the client getting disconnected | |
await ReconnectAsync(); | |
} | |
} while (true); | |
} | |
private async Task IdleAsync() | |
{ | |
do | |
{ | |
try | |
{ | |
await WaitForNewMessagesAsync(); | |
if (!_messagesArrived) continue; | |
await FetchMessagesAsync(true); | |
_messagesArrived = false; | |
} | |
catch (OperationCanceledException) | |
{ | |
break; | |
} | |
} while (!_cancel.IsCancellationRequested); | |
} | |
public async Task RunAsync() | |
{ | |
// connect to the IMAP server and get our initial list of messages | |
try | |
{ | |
await ReconnectAsync(); | |
await FetchMessagesAsync(false); | |
} | |
catch (OperationCanceledException) | |
{ | |
if (_client.IsConnected) | |
await _client.DisconnectAsync(true); | |
return; | |
} | |
// Note: We capture client.Inbox here because cancelling IdleAsync() *may* require | |
// disconnecting the IMAP client connection, and, if it does, the `client.Inbox` | |
// property will no longer be accessible which means we won't be able to disconnect | |
// our event handlers. | |
var inbox = _client.Inbox; | |
// keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived). | |
inbox.CountChanged += OnCountChanged; | |
await IdleAsync(); | |
inbox.CountChanged -= OnCountChanged; | |
if (_client.IsConnected) | |
await _client.DisconnectAsync(true); | |
} | |
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged. | |
private void OnCountChanged(object sender, EventArgs e) | |
{ | |
_messagesArrived = true; | |
try | |
{ | |
_done?.Cancel(); | |
} | |
catch (ObjectDisposedException) | |
{ | |
// ignored | |
} | |
} | |
public void Exit() | |
{ | |
_cancel.Cancel(); | |
} | |
public void Dispose() | |
{ | |
_client.Dispose(); | |
_cancel.Dispose(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment