Skip to content

Instantly share code, notes, and snippets.

@bluewalk
Created November 24, 2020 06:43
Show Gist options
  • Save bluewalk/07d7c717d99d5511bd07731b2da5c4f4 to your computer and use it in GitHub Desktop.
Save bluewalk/07d7c717d99d5511bd07731b2da5c4f4 to your computer and use it in GitHub Desktop.
Imap IDLE client (using MailKit)
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