MailKit IMAP Idle - 如何在 CountChanged 事件中访问 'done' CancellationTokenSource
MailKit IMAP Idle - How to access 'done' CancellationTokenSource in CountChanged Event
我正在使用找到的 IMAP 空闲示例代码 here
示例需要 Console.ReadKey 来取消 CancellationTokenSource,但建议在新邮件到达时可以在 CountChanged 事件中取消它,只要该事件可以访问 CancellationTokenSource。
如何访问 CountChanged 事件中的 CancellationTokenSource?
这是上面的代码片段 link...
// Keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
client.Inbox.CountChanged += (sender, e) => {
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
var folder = (ImapFolder)sender;
Console.WriteLine("The number of messages in {0} has changed.", folder);
// Note: because we are keeping track of the MessageExpunged event and updating our
// 'messages' list, we know that if we get a CountChanged event and folder.Count is
// larger than messages.Count, then it means that new messages have arrived.
if (folder.Count > messages.Count) {
Console.WriteLine("{0} new messages have arrived.", folder.Count - messages.Count);
// Note: your first instict may be to fetch these new messages now, but you cannot do
// that in an event handler (the ImapFolder is not re-entrant).
//
// If this code had access to the 'done' CancellationTokenSource (see below), it could
// cancel that to cause the IDLE loop to end.
// HOW DO I DO THIS??
}
};
Console.WriteLine("Hit any key to end the IDLE loop.");
using (var done = new CancellationTokenSource()) {
// Note: when the 'done' CancellationTokenSource is cancelled, it ends to IDLE loop.
var thread = new Thread(IdleLoop);
thread.Start(new IdleState(client, done.Token));
Console.ReadKey();
done.Cancel();
thread.Join();
}
您需要做的就是稍微重新安排代码,以便您的事件处理程序可以访问 done
令牌。
这是一个如何执行此操作的示例:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Security;
namespace ImapIdle
{
class Program
{
// Connection-related properties
public const SecureSocketOptions SslOptions = SecureSocketOptions.Auto;
public const string Host = "imap.gmail.com";
public const int Port = 993;
// Authentication-related properties
public const string Username = "username@gmail.com";
public const string Password = "password";
public static void Main (string[] args)
{
using (var client = new IdleClient ()) {
Console.WriteLine ("Hit any key to end the demo.");
var idleTask = client.RunAsync ();
Task.Run (() => {
Console.ReadKey (true);
}).Wait ();
client.Exit ();
idleTask.GetAwaiter ().GetResult ();
}
}
}
class IdleClient : IDisposable
{
List<IMessageSummary> messages;
CancellationTokenSource cancel;
CancellationTokenSource done;
bool messagesArrived;
ImapClient client;
public IdleClient ()
{
client = new ImapClient (new ProtocolLogger (Console.OpenStandardError ()));
messages = new List<IMessageSummary> ();
cancel = new CancellationTokenSource ();
}
async Task ReconnectAsync ()
{
if (!client.IsConnected)
await client.ConnectAsync (Program.Host, Program.Port, Program.SslOptions, cancel.Token);
if (!client.IsAuthenticated) {
await client.AuthenticateAsync (Program.Username, Program.Password, cancel.Token);
await client.Inbox.OpenAsync (FolderAccess.ReadOnly, cancel.Token);
}
}
async Task FetchMessageSummariesAsync (bool print)
{
IList<IMessageSummary> fetched;
do {
try {
// fetch summary information for messages that we don't already have
int startIndex = messages.Count;
fetched = client.Inbox.Fetch (startIndex, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId, 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);
foreach (var message in fetched) {
if (print)
Console.WriteLine ("{0}: new message: {1}", client.Inbox, message.Envelope.Subject);
messages.Add (message);
}
}
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.
using (done = new CancellationTokenSource (new TimeSpan (0, 9, 0))) {
using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancel.Token, done.Token)) {
await client.IdleAsync (linked.Token);
// throw OperationCanceledException if the cancel token has been canceled.
cancel.Token.ThrowIfCancellationRequested ();
}
}
} 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);
}
async Task IdleAsync ()
{
do {
try {
await WaitForNewMessagesAsync ();
if (messagesArrived) {
await FetchMessageSummariesAsync (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 FetchMessageSummariesAsync (false);
} catch (OperationCanceledException) {
await client.DisconnectAsync (true);
return;
}
// keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
client.Inbox.CountChanged += OnCountChanged;
// keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's
// because new messages have arrived vs messages being removed (or some combination of the two).
client.Inbox.MessageExpunged += OnMessageExpunged;
// keep track of flag changes
client.Inbox.MessageFlagsChanged += OnMessageFlagsChanged;
await IdleAsync ();
client.Inbox.MessageFlagsChanged -= OnMessageFlagsChanged;
client.Inbox.MessageExpunged -= OnMessageExpunged;
client.Inbox.CountChanged -= OnCountChanged;
await client.DisconnectAsync (true);
}
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
void OnCountChanged (object sender, EventArgs e)
{
var folder = (ImapFolder) sender;
// Note: because we are keeping track of the MessageExpunged event and updating our
// 'messages' list, we know that if we get a CountChanged event and folder.Count is
// larger than messages.Count, then it means that new messages have arrived.
if (folder.Count > messages.Count) {
int arrived = folder.Count - messages.Count;
if (arrived > 1)
Console.WriteLine ("\t{0} new messages have arrived.", arrived);
else
Console.WriteLine ("\t1 new message has arrived.");
// Note: your first instict may be to fetch these new messages now, but you cannot do
// that in this event handler (the ImapFolder is not re-entrant).
//
// Instead, cancel the `done` token and update our state so that we know new messages
// have arrived. We'll fetch the summaries for these new messages later...
messagesArrived = true;
done?.Cancel ();
}
}
void OnMessageExpunged (object sender, MessageEventArgs e)
{
var folder = (ImapFolder) sender;
if (e.Index < messages.Count) {
var message = messages[e.Index];
Console.WriteLine ("{0}: message #{1} has been expunged: {2}", folder, e.Index, message.Envelope.Subject);
// Note: If you are keeping a local cache of message information
// (e.g. MessageSummary data) for the folder, then you'll need
// to remove the message at e.Index.
messages.RemoveAt (e.Index);
} else {
Console.WriteLine ("{0}: message #{1} has been expunged.", folder, e.Index);
}
}
void OnMessageFlagsChanged (object sender, MessageFlagsChangedEventArgs e)
{
var folder = (ImapFolder) sender;
Console.WriteLine ("{0}: flags have changed for message #{1} ({2}).", folder, e.Index, e.Flags);
}
public void Exit ()
{
cancel.Cancel ();
}
public void Dispose ()
{
client.Dispose ();
cancel.Dispose ();
done?.Dispose ();
}
}
}
继之前的 之后,我将其作为 后台服务 用于 Api
public class InboxMessageSubscriptionService : BackgroundService
{
public IServiceProvider Services { get; }
public IConfiguration Configuration { get; }
private IdleClient client;
public InboxMessageSubscriptionService(IServiceProvider services, IConfiguration configuration)
{
/*
* Outlook Ports and info : https://support.microsoft.com/en-us/office/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040
*
*
* Outlook Imap Server Name: "outlook.office365.com"
* Outlook IMAP Port: 993
*
*
* Following example:
*
*/
Services = services;
Configuration = configuration;
}
public override Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = Services.CreateScope())
{
var settings = Configuration.GetSection(SettingsOptions.SettingsPath).Get<SettingsOptions>();
client = new IdleClient("outlook.office365.com", 993, settings.Emails.Sender, settings.Emails.Password);
}
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await client.RunAsync();
}
public override Task StopAsync(CancellationToken cancellationToken)
{
try
{
client.Exit();
client.Dispose();
}catch (Exception ex)
{
Console.Error.WriteLine("Error when disposing the IdleClient");
}
return base.StopAsync(cancellationToken);
}
}
class IdleClient : IDisposable
{
List<IMessageSummary> messages;
CancellationTokenSource cancel;
CancellationTokenSource done;
bool messagesArrived;
ImapClient client;
private readonly string host;
private readonly int port;
private readonly string email;
private readonly string password;
public IdleClient(string host, int port, string email, string password)
{
this.host = host;
this.port = port;
this.email = email;
this.password = password;
client = new ImapClient();
messages = new List<IMessageSummary>();
cancel = new CancellationTokenSource();
}
async Task ReconnectAsync()
{
if (!client.IsConnected)
await client.ConnectAsync(host, port, true, cancel.Token);
if (!client.IsAuthenticated)
{
await client.AuthenticateAsync(email, password, cancel.Token);
await client.Inbox.OpenAsync(FolderAccess.ReadOnly, cancel.Token);
}
}
async Task FetchMessageSummariesAsync(bool print)
{
IList<IMessageSummary> fetched;
do
{
try
{
// fetch summary information for messages that we don't already have
int startIndex = messages.Count;
fetched = client.Inbox.Fetch(startIndex, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId, 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);
foreach (var message in fetched)
{
if (print)
Console.WriteLine("{0}: new message: {1}", client.Inbox, message.Envelope.Subject);
messages.Add(message);
}
}
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.
using (done = new CancellationTokenSource(new TimeSpan(0, 9, 0)))
{
using (var linked = CancellationTokenSource.CreateLinkedTokenSource(cancel.Token, done.Token))
{
await client.IdleAsync(linked.Token);
// throw OperationCanceledException if the cancel token has been canceled.
cancel.Token.ThrowIfCancellationRequested();
}
}
}
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);
}
async Task IdleAsync()
{
do
{
try
{
await WaitForNewMessagesAsync();
if (messagesArrived)
{
await FetchMessageSummariesAsync(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 FetchMessageSummariesAsync(false);
}
catch (OperationCanceledException)
{
await client.DisconnectAsync(true);
return;
}
// keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
client.Inbox.CountChanged += OnCountChanged;
// keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's
// because new messages have arrived vs messages being removed (or some combination of the two).
client.Inbox.MessageExpunged += OnMessageExpunged;
// keep track of flag changes
client.Inbox.MessageFlagsChanged += OnMessageFlagsChanged;
await IdleAsync();
client.Inbox.MessageFlagsChanged -= OnMessageFlagsChanged;
client.Inbox.MessageExpunged -= OnMessageExpunged;
client.Inbox.CountChanged -= OnCountChanged;
await client.DisconnectAsync(true);
}
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
void OnCountChanged(object sender, EventArgs e)
{
var folder = (ImapFolder)sender;
// Note: because we are keeping track of the MessageExpunged event and updating our
// 'messages' list, we know that if we get a CountChanged event and folder.Count is
// larger than messages.Count, then it means that new messages have arrived.
if (folder.Count > messages.Count)
{
int arrived = folder.Count - messages.Count;
if (arrived > 1)
Console.WriteLine("\t{0} new messages have arrived.", arrived);
else
Console.WriteLine("\t1 new message has arrived.");
// Note: your first instict may be to fetch these new messages now, but you cannot do
// that in this event handler (the ImapFolder is not re-entrant).
//
// Instead, cancel the `done` token and update our state so that we know new messages
// have arrived. We'll fetch the summaries for these new messages later...
messagesArrived = true;
done?.Cancel();
}
}
void OnMessageExpunged(object sender, MessageEventArgs e)
{
var folder = (ImapFolder)sender;
if (e.Index < messages.Count)
{
var message = messages[e.Index];
Console.WriteLine("{0}: message #{1} has been expunged: {2}", folder, e.Index, message.Envelope.Subject);
// Note: If you are keeping a local cache of message information
// (e.g. MessageSummary data) for the folder, then you'll need
// to remove the message at e.Index.
messages.RemoveAt(e.Index);
}
else
{
Console.WriteLine("{0}: message #{1} has been expunged.", folder, e.Index);
}
}
void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs e)
{
var folder = (ImapFolder)sender;
Console.WriteLine("{0}: flags have changed for message #{1} ({2}).", folder, e.Index, e.Flags);
}
public void Exit()
{
cancel.Cancel();
}
public void Dispose()
{
client.Dispose();
cancel.Dispose();
done?.Dispose();
}
}
我正在使用找到的 IMAP 空闲示例代码 here
示例需要 Console.ReadKey 来取消 CancellationTokenSource,但建议在新邮件到达时可以在 CountChanged 事件中取消它,只要该事件可以访问 CancellationTokenSource。
如何访问 CountChanged 事件中的 CancellationTokenSource?
这是上面的代码片段 link...
// Keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
client.Inbox.CountChanged += (sender, e) => {
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
var folder = (ImapFolder)sender;
Console.WriteLine("The number of messages in {0} has changed.", folder);
// Note: because we are keeping track of the MessageExpunged event and updating our
// 'messages' list, we know that if we get a CountChanged event and folder.Count is
// larger than messages.Count, then it means that new messages have arrived.
if (folder.Count > messages.Count) {
Console.WriteLine("{0} new messages have arrived.", folder.Count - messages.Count);
// Note: your first instict may be to fetch these new messages now, but you cannot do
// that in an event handler (the ImapFolder is not re-entrant).
//
// If this code had access to the 'done' CancellationTokenSource (see below), it could
// cancel that to cause the IDLE loop to end.
// HOW DO I DO THIS??
}
};
Console.WriteLine("Hit any key to end the IDLE loop.");
using (var done = new CancellationTokenSource()) {
// Note: when the 'done' CancellationTokenSource is cancelled, it ends to IDLE loop.
var thread = new Thread(IdleLoop);
thread.Start(new IdleState(client, done.Token));
Console.ReadKey();
done.Cancel();
thread.Join();
}
您需要做的就是稍微重新安排代码,以便您的事件处理程序可以访问 done
令牌。
这是一个如何执行此操作的示例:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Security;
namespace ImapIdle
{
class Program
{
// Connection-related properties
public const SecureSocketOptions SslOptions = SecureSocketOptions.Auto;
public const string Host = "imap.gmail.com";
public const int Port = 993;
// Authentication-related properties
public const string Username = "username@gmail.com";
public const string Password = "password";
public static void Main (string[] args)
{
using (var client = new IdleClient ()) {
Console.WriteLine ("Hit any key to end the demo.");
var idleTask = client.RunAsync ();
Task.Run (() => {
Console.ReadKey (true);
}).Wait ();
client.Exit ();
idleTask.GetAwaiter ().GetResult ();
}
}
}
class IdleClient : IDisposable
{
List<IMessageSummary> messages;
CancellationTokenSource cancel;
CancellationTokenSource done;
bool messagesArrived;
ImapClient client;
public IdleClient ()
{
client = new ImapClient (new ProtocolLogger (Console.OpenStandardError ()));
messages = new List<IMessageSummary> ();
cancel = new CancellationTokenSource ();
}
async Task ReconnectAsync ()
{
if (!client.IsConnected)
await client.ConnectAsync (Program.Host, Program.Port, Program.SslOptions, cancel.Token);
if (!client.IsAuthenticated) {
await client.AuthenticateAsync (Program.Username, Program.Password, cancel.Token);
await client.Inbox.OpenAsync (FolderAccess.ReadOnly, cancel.Token);
}
}
async Task FetchMessageSummariesAsync (bool print)
{
IList<IMessageSummary> fetched;
do {
try {
// fetch summary information for messages that we don't already have
int startIndex = messages.Count;
fetched = client.Inbox.Fetch (startIndex, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId, 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);
foreach (var message in fetched) {
if (print)
Console.WriteLine ("{0}: new message: {1}", client.Inbox, message.Envelope.Subject);
messages.Add (message);
}
}
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.
using (done = new CancellationTokenSource (new TimeSpan (0, 9, 0))) {
using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancel.Token, done.Token)) {
await client.IdleAsync (linked.Token);
// throw OperationCanceledException if the cancel token has been canceled.
cancel.Token.ThrowIfCancellationRequested ();
}
}
} 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);
}
async Task IdleAsync ()
{
do {
try {
await WaitForNewMessagesAsync ();
if (messagesArrived) {
await FetchMessageSummariesAsync (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 FetchMessageSummariesAsync (false);
} catch (OperationCanceledException) {
await client.DisconnectAsync (true);
return;
}
// keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
client.Inbox.CountChanged += OnCountChanged;
// keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's
// because new messages have arrived vs messages being removed (or some combination of the two).
client.Inbox.MessageExpunged += OnMessageExpunged;
// keep track of flag changes
client.Inbox.MessageFlagsChanged += OnMessageFlagsChanged;
await IdleAsync ();
client.Inbox.MessageFlagsChanged -= OnMessageFlagsChanged;
client.Inbox.MessageExpunged -= OnMessageExpunged;
client.Inbox.CountChanged -= OnCountChanged;
await client.DisconnectAsync (true);
}
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
void OnCountChanged (object sender, EventArgs e)
{
var folder = (ImapFolder) sender;
// Note: because we are keeping track of the MessageExpunged event and updating our
// 'messages' list, we know that if we get a CountChanged event and folder.Count is
// larger than messages.Count, then it means that new messages have arrived.
if (folder.Count > messages.Count) {
int arrived = folder.Count - messages.Count;
if (arrived > 1)
Console.WriteLine ("\t{0} new messages have arrived.", arrived);
else
Console.WriteLine ("\t1 new message has arrived.");
// Note: your first instict may be to fetch these new messages now, but you cannot do
// that in this event handler (the ImapFolder is not re-entrant).
//
// Instead, cancel the `done` token and update our state so that we know new messages
// have arrived. We'll fetch the summaries for these new messages later...
messagesArrived = true;
done?.Cancel ();
}
}
void OnMessageExpunged (object sender, MessageEventArgs e)
{
var folder = (ImapFolder) sender;
if (e.Index < messages.Count) {
var message = messages[e.Index];
Console.WriteLine ("{0}: message #{1} has been expunged: {2}", folder, e.Index, message.Envelope.Subject);
// Note: If you are keeping a local cache of message information
// (e.g. MessageSummary data) for the folder, then you'll need
// to remove the message at e.Index.
messages.RemoveAt (e.Index);
} else {
Console.WriteLine ("{0}: message #{1} has been expunged.", folder, e.Index);
}
}
void OnMessageFlagsChanged (object sender, MessageFlagsChangedEventArgs e)
{
var folder = (ImapFolder) sender;
Console.WriteLine ("{0}: flags have changed for message #{1} ({2}).", folder, e.Index, e.Flags);
}
public void Exit ()
{
cancel.Cancel ();
}
public void Dispose ()
{
client.Dispose ();
cancel.Dispose ();
done?.Dispose ();
}
}
}
继之前的
public class InboxMessageSubscriptionService : BackgroundService
{
public IServiceProvider Services { get; }
public IConfiguration Configuration { get; }
private IdleClient client;
public InboxMessageSubscriptionService(IServiceProvider services, IConfiguration configuration)
{
/*
* Outlook Ports and info : https://support.microsoft.com/en-us/office/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040
*
*
* Outlook Imap Server Name: "outlook.office365.com"
* Outlook IMAP Port: 993
*
*
* Following example:
*
*/
Services = services;
Configuration = configuration;
}
public override Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = Services.CreateScope())
{
var settings = Configuration.GetSection(SettingsOptions.SettingsPath).Get<SettingsOptions>();
client = new IdleClient("outlook.office365.com", 993, settings.Emails.Sender, settings.Emails.Password);
}
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await client.RunAsync();
}
public override Task StopAsync(CancellationToken cancellationToken)
{
try
{
client.Exit();
client.Dispose();
}catch (Exception ex)
{
Console.Error.WriteLine("Error when disposing the IdleClient");
}
return base.StopAsync(cancellationToken);
}
}
class IdleClient : IDisposable
{
List<IMessageSummary> messages;
CancellationTokenSource cancel;
CancellationTokenSource done;
bool messagesArrived;
ImapClient client;
private readonly string host;
private readonly int port;
private readonly string email;
private readonly string password;
public IdleClient(string host, int port, string email, string password)
{
this.host = host;
this.port = port;
this.email = email;
this.password = password;
client = new ImapClient();
messages = new List<IMessageSummary>();
cancel = new CancellationTokenSource();
}
async Task ReconnectAsync()
{
if (!client.IsConnected)
await client.ConnectAsync(host, port, true, cancel.Token);
if (!client.IsAuthenticated)
{
await client.AuthenticateAsync(email, password, cancel.Token);
await client.Inbox.OpenAsync(FolderAccess.ReadOnly, cancel.Token);
}
}
async Task FetchMessageSummariesAsync(bool print)
{
IList<IMessageSummary> fetched;
do
{
try
{
// fetch summary information for messages that we don't already have
int startIndex = messages.Count;
fetched = client.Inbox.Fetch(startIndex, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId, 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);
foreach (var message in fetched)
{
if (print)
Console.WriteLine("{0}: new message: {1}", client.Inbox, message.Envelope.Subject);
messages.Add(message);
}
}
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.
using (done = new CancellationTokenSource(new TimeSpan(0, 9, 0)))
{
using (var linked = CancellationTokenSource.CreateLinkedTokenSource(cancel.Token, done.Token))
{
await client.IdleAsync(linked.Token);
// throw OperationCanceledException if the cancel token has been canceled.
cancel.Token.ThrowIfCancellationRequested();
}
}
}
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);
}
async Task IdleAsync()
{
do
{
try
{
await WaitForNewMessagesAsync();
if (messagesArrived)
{
await FetchMessageSummariesAsync(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 FetchMessageSummariesAsync(false);
}
catch (OperationCanceledException)
{
await client.DisconnectAsync(true);
return;
}
// keep track of changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
client.Inbox.CountChanged += OnCountChanged;
// keep track of messages being expunged so that when the CountChanged event fires, we can tell if it's
// because new messages have arrived vs messages being removed (or some combination of the two).
client.Inbox.MessageExpunged += OnMessageExpunged;
// keep track of flag changes
client.Inbox.MessageFlagsChanged += OnMessageFlagsChanged;
await IdleAsync();
client.Inbox.MessageFlagsChanged -= OnMessageFlagsChanged;
client.Inbox.MessageExpunged -= OnMessageExpunged;
client.Inbox.CountChanged -= OnCountChanged;
await client.DisconnectAsync(true);
}
// Note: the CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.
void OnCountChanged(object sender, EventArgs e)
{
var folder = (ImapFolder)sender;
// Note: because we are keeping track of the MessageExpunged event and updating our
// 'messages' list, we know that if we get a CountChanged event and folder.Count is
// larger than messages.Count, then it means that new messages have arrived.
if (folder.Count > messages.Count)
{
int arrived = folder.Count - messages.Count;
if (arrived > 1)
Console.WriteLine("\t{0} new messages have arrived.", arrived);
else
Console.WriteLine("\t1 new message has arrived.");
// Note: your first instict may be to fetch these new messages now, but you cannot do
// that in this event handler (the ImapFolder is not re-entrant).
//
// Instead, cancel the `done` token and update our state so that we know new messages
// have arrived. We'll fetch the summaries for these new messages later...
messagesArrived = true;
done?.Cancel();
}
}
void OnMessageExpunged(object sender, MessageEventArgs e)
{
var folder = (ImapFolder)sender;
if (e.Index < messages.Count)
{
var message = messages[e.Index];
Console.WriteLine("{0}: message #{1} has been expunged: {2}", folder, e.Index, message.Envelope.Subject);
// Note: If you are keeping a local cache of message information
// (e.g. MessageSummary data) for the folder, then you'll need
// to remove the message at e.Index.
messages.RemoveAt(e.Index);
}
else
{
Console.WriteLine("{0}: message #{1} has been expunged.", folder, e.Index);
}
}
void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs e)
{
var folder = (ImapFolder)sender;
Console.WriteLine("{0}: flags have changed for message #{1} ({2}).", folder, e.Index, e.Flags);
}
public void Exit()
{
cancel.Cancel();
}
public void Dispose()
{
client.Dispose();
cancel.Dispose();
done?.Dispose();
}
}