首页 > 解决方案 > MailKit IMAP Idle - How to access 'done' CancellationTokenSource in CountChanged Event

问题描述

I am working with the IMAP Idle sample code found here

The sample requires a Console.ReadKey to cancel the CancellationTokenSource but makes a suggestion that it could be cancelled in the CountChanged event at the time new mail arrives so long as the event has access to the CancellationTokenSource.

How do I get access to the CancellationTokenSource in the CountChanged event?

Here is the snippit of code from the above 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();
}

标签: c#imapmailkit

解决方案


All you need to do is rearrange the code a bit so that your event handlers have access to the done token.

Here's an example of how you could do this:

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 ();
        }
    }
}

推荐阅读