ImapIdleSession Class

A session that waits for changes to be pushed by an IMAP server.

Definition

Namespace: MailKit.Net.Imap
Assembly: MailKit (in MailKit.dll) Version: 5.0.0
C#
public sealed class ImapIdleSession
Inheritance
Object    ImapIdleSession

Remarks

An ImapIdleSession takes care of the details of waiting for changes:

  • It uses the IDLE command when the server supports it and periodically restarts it (see RefreshInterval) so that the connection isn't dropped for inactivity.
  • It falls back to polling with the NOOP command when the server does not support IDLE (see PollInterval).
  • It collects bursts of notifications into a single ImapIdleChanges batch (see CoalesceDelay).
  • It ends the IDLE command before returning the changes, so the caller is free to issue other commands (such as fetching the new messages) while processing them.

Only changes that are received while WaitForChanges(CancellationToken) (or one of its variants) is running are recorded. Changes that the server reports while other commands are running are still raised as events on the folders (such as CountChanged) and are reflected in the folder properties, but they are not included in the next batch.

Cancelling the cancellation token passed to the wait methods ends the IDLE command gracefully, which means that the connection remains usable afterward. To abort a wait that the server never responds to, disconnect the client.

An ImapIdleSession does not hold any resources between waits, so it does not need to be disposed.

Example

C#
                    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 ImapIdleSessionExample {
    class Program
    {
        // Connection-related properties
        const SecureSocketOptions SslOptions = SecureSocketOptions.Auto;
        const string Host = "imap.gmail.com";
        const int Port = 993;

        // Authentication-related properties
        const string Username = "username@gmail.com";
        const string Password = "password";

        public static void Main (string[] args)
        {
            using (var client = new IdleClient (Host, Port, SslOptions, Username, Password)) {
                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
    {
        readonly string host, username, password;
        readonly SecureSocketOptions sslOptions;
        readonly int port;
        readonly List<IMessageSummary> messages;
        readonly CancellationTokenSource cancel;
        readonly FetchRequest request;
        readonly ImapClient client;

        public IdleClient (string host, int port, SecureSocketOptions sslOptions, string username, string password)
        {
            this.client = new ImapClient (new ProtocolLogger (Console.OpenStandardError ()));
            this.request = new FetchRequest (MessageSummaryItems.Full | MessageSummaryItems.UniqueId);
            this.messages = new List<IMessageSummary> ();
            this.cancel = new CancellationTokenSource ();
            this.sslOptions = sslOptions;
            this.username = username;
            this.password = password;
            this.host = host;
            this.port = port;
        }

        async Task ReconnectAsync ()
        {
            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);
            }
        }

        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 = await client.Inbox.FetchAsync (startIndex, -1, request, 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<ImapIdleChanges> WaitForChangesAsync (ImapIdleSession session)
        {
            do {
                try {
                    // Note: The session uses IDLE when the server supports it (refreshing it periodically
                    // so that the server doesn't drop the connection) and falls back to polling with NOOP
                    // when it doesn't.
                    return await session.WaitForChangesAsync (cancel.Token);
                } 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);
        }

        void ProcessChanges (ImapIdleChanges changes)
        {
            // Note: Events are reported in the order that the server sent them, so applying
            // them in order keeps our 'messages' cache consistent with the folder.
            foreach (var e in changes.Events) {
                switch (e) {
                case CountChangedIdleEvent countChanged:
                    Console.WriteLine ("{0}: message count changed from {1} to {2}.", e.Folder, countChanged.PreviousCount, countChanged.Count);
                    break;
                case MessageExpungedIdleEvent expunged:
                    if (expunged.Index < messages.Count) {
                        var message = messages[expunged.Index];

                        Console.WriteLine ("{0}: message #{1} has been expunged: {2}", e.Folder, expunged.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 expunged.Index.
                        messages.RemoveAt (expunged.Index);
                    } else {
                        Console.WriteLine ("{0}: message #{1} has been expunged.", e.Folder, expunged.Index);
                    }
                    break;
                case MessagesVanishedIdleEvent vanished:
                    // Note: This is only reported by servers when QRESYNC has been enabled.
                    Console.WriteLine ("{0}: {1} messages have vanished.", e.Folder, vanished.UniqueIds.Count);
                    break;
                case MessageChangedIdleEvent changed:
                    if (changed.Message.Flags.HasValue)
                        Console.WriteLine ("{0}: flags have changed for message #{1} ({2}).", e.Folder, changed.Message.Index, changed.Message.Flags.Value);
                    break;
                case FolderStatusChangedIdleEvent statusChanged:
                    Console.WriteLine ("{0}: folder status changed ({1}).", e.Folder, statusChanged.Item);
                    break;
                }
            }
        }

        async Task IdleAsync ()
        {
            // Note: The default options are suitable for most servers (including GMail), but they
            // can be tweaked by passing an ImapIdleOptions to CreateIdleSession().
            var session = client.CreateIdleSession ();

            // Note: If you don't need to handle reconnecting, this loop can be simplified to:
            //
            // await foreach (var changes in session.ReadChangesAsync (cancel.Token)) { ... }
            try {
                do {
                    var changes = await WaitForChangesAsync (session);

                    ProcessChanges (changes);

                    // Note: Unlike when handling the folder events (such as CountChanged) while
                    // idling, it is safe to send new commands here because the IDLE command has
                    // already been ended by the time WaitForChangesAsync() returns.
                    if (changes.MessagesArrived)
                        await FetchMessageSummariesAsync (true);
                } while (true);
            } catch (OperationCanceledException) {
                // Exit() was called. The IDLE command was ended gracefully, so the client is
                // still connected and usable.
            }
        }

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

            await IdleAsync ();

            await client.DisconnectAsync (true);
        }

        public void Exit ()
        {
            cancel.Cancel ();
        }

        public void Dispose ()
        {
            client.Dispose ();
            cancel.Dispose ();
        }
    }
}
                  

Properties

Client Get the client that the session belongs to.

Methods

Equals Determines whether the specified object is equal to the current object.
(Inherited from Object)
GetHashCode Serves as the default hash function.
(Inherited from Object)
GetType Gets the Type of the current instance.
(Inherited from Object)
ReadChangesAsync Asynchronously read batches of changes as they are reported by the server.
ToString Returns a string that represents the current object.
(Inherited from Object)
WaitForChanges Wait for the server to report changes.
WaitForChangesAsync Asynchronously wait for the server to report changes.

See Also