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