强大的守护程序监控收件箱,带OAuth2的邮件工具包

这一切都始于发送给守护程序的电子邮件。托管两个模块(每个模块监视收件箱的自动化)的Windows服务尽职尽责地忽略了IT部门的警告,即 O365的基本身份验证将在几个月内关闭。几个月过去了...尽管微软的公告是在2年前,但当合适的人被告知截止日期只有2个月时。听起来很耳熟?不幸的是,当前的IMAP API不支持OAuth2身份验证,因此必须替换。更糟糕的是,我们浪费了数周时间与Azure管理员一起整理访问权限,尽管我们从第一天起就有分步说明

在调查支持OAuth2的主流IMAPAP时,发现了MailKit,其作者在GithubStackOverflow上非常活跃。我们很快发现各地的开发人员都在解决这个问题,并且关于如何甚至是否可以做到这一点有很多争论(作者本人对此表示怀疑)。值得庆幸的是,经过几周的痛苦,我们在没有用户交互的情况下对守护程序进行身份验证(也称为OAuth2客户端凭据授予流)。

在编写API时,有一个范围从用户抽象和混淆内部工作原理。一方面,编写为与服务器11API不太可用,但可以提供细微的控制和透明度,从而实现更好的调试。此路径需要更多的启动时间,并给用户留下更多的复杂性。另一方面,API承担了一些繁重的工作,旨在提供一个可用、易于使用的界面。一个典型的权衡是内部工作原理是一个黑匣子,可能会在路上咬你的屁股。

与我们的旧API相比,MailKit全心全意地处于前阵营。旧的连接,有一个新的电子邮件事件,然后在服务关闭时断开连接。从删除邮件到搜索新电子邮件,总体上更容易使用。例如,电子邮件UID是电子邮件对象的一部分。使用MailKit时,必须单独查询此信息,因为从技术上讲,这是它在服务器上的存储方式。这为与MailKit交互的整个体验定下了基调。

如上所述,即使使用起来有点困难,看到作者和用户社区的活跃程度也非常令人放心。虽然从旧API移植代码需要大量重写,但有大量的文档、讨论和示例可以回答我们的问题。出乎意料的是,如果不构建完整的IMAP客户端,服务器事件就无法工作,这让我想起了实现Windows消息泵,以在其自己的线程中空闲和处理事件。值得庆幸的是,文档和示例虽然复杂,但可以在此基础上进行构建。

随着序言的结束,我们终于可以谈谈代码了。下面是MailKit APIC#包装器。我们可以以两种不同的方式使用它。您可以简单地实例化它并使用两行代码执行命令。这将自动连接,在IMAP客户端线程上下文中运行IMAP命令,然后断开连接。或者,您可以将其用作长时间运行的连接,它将启动IMAP客户端作为强大的任务,该任务将保持连接直到停止。这允许使用包装器公开的事件来处理新消息。还有一个命令队列,以便代码可以排队以在IMAP客户端线程上下文中运行。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Security;
using Microsoft.Identity.Client;

namespace Codinglifestyle
{
    /// <summary>
    /// IMAP client instance capable of receiving events and executing IMAP commands
    /// </summary>
    /// <seealso cref="System.IDisposable" />
    public class ImapClientEx : IDisposable
    {
        #region Member variables
        ImapClient                   _imapClient;
        IMailFolder                  _imapFolder;
        int                          _numMessages;
        CancellationTokenSource      _tokenCancel;
        CancellationTokenSource      _tokenDone;

        Queue<OnImapCommand>         _queueCommand;
        bool                         _messagesArrived;

        readonly string              _imapServer;
        readonly string              _imapUser;
        readonly string              _authAppID;
        readonly string              _authAppSecret;
        readonly string              _authTenantID;
        readonly SecureSocketOptions _sslOptions;
        readonly int                 _port;
        readonly FolderAccess        _folderAccess;
        
        protected DateTime           _dtLastConnection;

        readonly object              _lock;
        #endregion

        #region Ctor
        /// <summary>
        /// Initializes a new instance of the <see cref="ImapClientEx"/> class.
        /// </summary>
        /// <param name="userEmail">The user email account.</param>
        public ImapClientEx(string userEmail)
        {
            _queueCommand  = new Queue<OnImapCommand>();
            _numMessages   = 0;
            _lock          = new object();

            Config config  = new Config("O365 Settings");
            _authAppID     = config["App ID"];
            _authAppSecret = config.Decrypt("App Secret");
            _authTenantID  = config["Tenant ID"];
            config         = new Config("Mail Settings");
            _imapServer    = config["IMAP Server"];
            _imapUser      = userEmail;
            _sslOptions    = SecureSocketOptions.Auto;
            _port          = 993;
            _folderAccess  = FolderAccess.ReadWrite;
        }
        #endregion

        #region Public Events
        /// <summary>
        /// IMAP command delegate to be queued and executed by the IMAP thread instance.
        /// </summary>
        /// <param name="imapClient">The IMAP client.</param>
        /// <param name="imapFolder">The IMAP folder.</param>
        public delegate void OnImapCommand(ImapClient imapClient, IMailFolder imapFolder);
        /// <summary>
        /// Event indicates the IMAP client folder has received a new message.
        /// </summary>
        /// <remarks>
        /// The event is called by the IMAP thread instance.
        /// </remarks>
        public event OnImapCommand NewMessage;
        /// <summary>
        /// Fires the new message event.
        /// </summary>
        private void OnNewMessageEvent(ImapClient imapClient, IMailFolder imapFolder)
        {
            if (NewMessage != null)
                NewMessage(_imapClient, _imapFolder);
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Runs an IMAP client asynchronously.
        /// </summary>
        public async Task RunAsync()
        {
            try
            {
                //
                //Queue first-run event to load new messages since last connection (the consumer must track this)
                //
                QueueCommand(OnNewMessageEvent);
                //
                //Run command in robustness pattern asynchronously to let this thread go...
                //
                await DoCommandAsync((_imapClient, _imapFolder) =>
                {
                    //
                    //Run IMAP client async in IDLE to listen to events until Stop() is called
                    //
                    IdleAsync().Wait();
                });

                Log.Debug(Identifier + "IMAP client exiting normally.");
            }
            catch (OperationCanceledException)
            {
                //Token is cancelled so exit
                Log.Debug(Identifier + "IMAP operation cancelled...");
            }
            catch (Exception ex)
            {
                Log.Err(ex, Identifier + "RunAsync");
            }
            finally
            {
                //
                //Disconnect and close IMAP client
                //
                Dispose();
            }
        }

        /// <summary>
        /// Gets a value indicating whether this IMAP client instance is connected.
        /// </summary>
        public bool IsConnected => _imapClient?.IsConnected == true && _imapFolder?.IsOpen == true;

        /// <summary>
        /// Identifiers this instance for logging.
        /// </summary>
        public string Identifier => string.Format("IMAP {0} [{1}]: ", _imapUser, Thread.CurrentThread.ManagedThreadId);

        /// <summary>
        /// Stops this IMAP client instance.
        /// </summary>
        public void Stop()
        {
            //Cancel the tokens releasing the IMAP client thread
            _tokenDone?.Cancel();
            _tokenCancel?.Cancel();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <remarks>This is safe to call and then carry on using this instance as all the resources will be automatically recreated by error handling</remarks>
        public void Dispose()
        {
            //Cancel tokens
            Stop();
            //Release connection
            DisconnectAsync().Wait();
            //Release resources
            if (_imapFolder != null)
            {
                _imapFolder.MessageExpunged -= OnMessageExpunged;
                _imapFolder.CountChanged    -= OnCountChanged;
            }
            _imapFolder  = null;
            _imapClient?.Dispose();
            _imapClient  = null;
            _tokenCancel?.Dispose();
            _tokenCancel = null;
            _tokenDone?.Dispose();
            _tokenDone   = null;
        }
        #endregion

        #region IMAP Connect / Idle
        /// <summary>
        /// Connects IMAP client, authenticated with OAUTH2, and opens the Inbox folder asynchronously.
        /// </summary>
        private async Task ConnectAsync()
        {
            //Dispose of existing instance, if any.
            if (_imapClient != null)
                Dispose();
            //
            //Create IMAP client
            //
            _imapClient  = new ImapClient();
            //
            //Create a new cancellation token
            //
            _tokenCancel = new CancellationTokenSource();
            //
            //Connect to the server
            //
            Log.Debug(Identifier + "Connecting to IMAP server: " + _imapServer);
            if (!_imapClient.IsConnected)
                await _imapClient.ConnectAsync(_imapServer, _port, _sslOptions, _tokenCancel.Token);
            //
            //Authenticate
            //
            if (!_imapClient.IsAuthenticated)
            {
                //
                //Create the client application
                //
                var app       = ConfidentialClientApplicationBuilder
                                    .Create(_authAppID)
                                    .WithClientSecret(_authAppSecret)
                                    .WithAuthority(new System.Uri($"https://login.microsoftonline.com/{_authTenantID}"))
                                    .Build();
                //
                //Get the OAUTH2 token
                //
                var scopes    = new string[] { "https://outlook.office365.com/.default" };
                var authToken = await app.AcquireTokenForClient(scopes).ExecuteAsync();
                Log.Debug(Identifier + "Creating OAUTH2 tokent for {0}: {1}", _imapUser, authToken.AccessToken);
                var oauth2    = new SaslMechanismOAuth2(_imapUser, authToken.AccessToken);
                //
                //Authenticate
                //
                Log.Debug(Identifier + "Authenticating user: " + _imapUser);
                await _imapClient.AuthenticateAsync(oauth2, _tokenCancel.Token);
            }
            //
            //Open inbox
            //
            if (!_imapClient.Inbox.IsOpen)
                await _imapClient.Inbox.OpenAsync(_folderAccess, _tokenCancel.Token);
            // 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.
            _imapFolder                  = _imapClient.Inbox;
            //
            //Track changes to the number of messages in the folder (this is how we'll tell if new messages have arrived).
            _imapFolder.CountChanged    += OnCountChanged;
            //Track of messages being expunged to track messages removed to work in combination with the above event.
            _imapFolder.MessageExpunged += OnMessageExpunged;
            //Track the message count to determine when we have new messages.
            _numMessages                 = _imapFolder.Count;
        }

        /// <summary>
        /// Closes the folder and disconnects IMAP client asynchronously.
        /// </summary>
        private async Task DisconnectAsync()
        {
            try
            {
                //Disconnect IMAP client
                if (_imapClient?.IsConnected == true)
                    await _imapClient.DisconnectAsync(true);
                Log.Debug(Identifier + "Disconnected.");
            }
            catch (Exception)
            {
            }
        }

        /// <summary>
        /// Idles waiting for events or commands to execute asynchronously.
        /// </summary>
        private async Task IdleAsync()
        {
            do
            {
                try
                {
                    //
                    //Run all queued IMAP commands
                    //
                    await DoCommandsAsync();
                    //
                    //Idle and listen for messages
                    //
                    await WaitForNewMessages();
                    //
                    if (_messagesArrived)
                    {
                        Log.Debug(Identifier + "New message arrived.  Queueing new message event...");
                        //
                        QueueCommand(OnNewMessageEvent);
                        //
                        _messagesArrived = false;
                    }
                }
                catch (OperationCanceledException)
                {
                    //Token is cancelled so exit
                    Log.Debug(Identifier + "IMAP Idle stopping...");
                    break;
                }
            } while (_tokenCancel != null && !_tokenCancel.IsCancellationRequested);
        }

        /// <summary>
        /// Waits for server events or cancellation tokens asynchronously.
        /// </summary>
        private async Task WaitForNewMessages()
        {
            try
            {
                Log.Debug(Identifier + "IMAP idle for 1 minute.  Connection age: {0}", DateTime.Now - _dtLastConnection);
                if (_imapClient.Capabilities.HasFlag(ImapCapabilities.Idle))
                {
                    //Done token will self-desrtruct in specified time (1 min)
                    _tokenDone = new CancellationTokenSource(new TimeSpan(0, 1, 0));
                    //
                    //Idle waiting for new events...
                    //Note: My observation was that the events fired but only after the 1 min token expired
                    //
                    await _imapClient.IdleAsync(_tokenDone.Token, _tokenCancel.Token);
                }
                else
                {
                    //Wait for 1 min
                    await Task.Delay(new TimeSpan(0, 1, 0), _tokenCancel.Token);
                    //Ping the IMAP server to keep the connection alive
                    await _imapClient.NoOpAsync(_tokenCancel.Token);
                }
            }
            catch (OperationCanceledException)
            {
                Log.Debug(Identifier + "WaitForNewMessages Idle cancelled...");
                throw;
            }
            catch (Exception ex)
            {
                Log.Warn(ex, Identifier + "WaitForNewMessages errored out...");
                throw;
            }
            finally
            {
                _tokenDone?.Dispose();
                _tokenDone = null;
            }
        }
        #endregion

        #region Command Queue
        /// <summary>
        /// Connects and performs IMAP command asynchronously.
        /// </summary>
        /// <param name="command">The IMAP comannd to execute.</param>
        /// <param name="retries">The number of times to retry executing the command.</param>
        /// <returns>Return true if the command succesfully updated</returns>
        /// <exception cref="MailKit.ServiceNotConnectedException">Will enter robustness pattern if not connected and retry later</exception>
        public async Task<bool> DoCommandAsync(OnImapCommand command, int retries = -1)
        {
            int attempts                  = 1;
            int errors                    = 0;
            int connections               = 0;
            _dtLastConnection             = DateTime.Now;
            DateTime errorStart           = DateTime.Now;
            bool bReturn                  = false;

            //Enter robustness pattern do/while loop...
            do
            {
                try
                {
                    //
                    //Connect, if not already connected
                    //
                    if (!IsConnected)
                    {
                        Log.Debug(Identifier + "Connection attempt #{0}; retries: {1}; errors: {2}; conns: {3}; total age: {4})",
                                  attempts++,
                                  (retries-- < 0) ? "infinite" : retries.ToString(),
                                  errors,
                                  connections,
                                  DateTime.Now - _dtLastConnection);
                        //
                        //Connect to IMAP
                        //
                        await ConnectAsync();
                        //Test IMAP connection
                        if (!IsConnected)
                            throw new ServiceNotConnectedException();
                        Log.Debug($"{Identifier}Server Connection: {IsConnected}");
                        //Reset connection stats
                        attempts          = 1;
                        errors            = 0; 
                        _dtLastConnection = DateTime.Now;
                        connections++;
                    }
                    //
                    //Perform command
                    //
                    Log.Debug("{0}Run IMAP command: {1}", Identifier, command.Method);
                    await Task.Run(() => command(_imapClient, _imapFolder), _tokenCancel.Token);
                    //
                    //Success: break the do/while loop and exit
                    //
                    Log.Debug(Identifier + "Command completed successfully.");
                    bReturn = true;
                    break;
                }
                catch (OperationCanceledException)
                {
                    //Token is cancelled so break the do/while loop and exit
                    Log.Debug(Identifier + "Command operation cancelled...");
                    break;
                }
                catch (Exception ex)
                {
                    //If no reries left log the error
                    if (retries == 0 && IsConnected)
                        Log.Err(ex, "{0}Error IMAP command: {1}", Identifier, command.Method);
                    //If first error since connected...
                    if (errors++ == 0)
                    {
                        //Track time since first error
                        errorStart = DateTime.Now;
                        //Reset the IMAP connection
                        Log.Debug(Identifier + "Error detected - attempt immediate reconnection.");
                        await DisconnectAsync();
                    }
                    else
                    {
                        TimeSpan errorAge = (DateTime.Now - errorStart);
                        Log.Debug(Identifier + "Connect failure (attempting connection for {0})", errorAge);

                        //Wait and try to reconnect
                        if (errorAge.TotalMinutes < 10)
                        {
                            Log.Debug(Identifier + "Cannot connect.  Retry in 1 minute.");
                            await Task.Delay(new TimeSpan(0, 1, 0), _tokenCancel.Token);
                        }
                        else if (errorAge.TotalMinutes < 60)
                        {
                            Log.Info(Identifier + "Cannot connect.  Retry in 10 minutes.");
                            await Task.Delay(new TimeSpan(0, 10, 0), _tokenCancel.Token);
                        }
                        else
                        {
                            Log.Err(ex, Identifier + "Cannot connect.  Retry in 1 hour (total errors: {0}).", errors);
                            await Task.Delay(new TimeSpan(1, 0, 0), _tokenCancel.Token);
                        }
                    }
                }
            } while (retries != 0 && _tokenCancel != null && !_tokenCancel.IsCancellationRequested);
            //
            //Return true if the command succesfully updated
            //
            return bReturn;
        }

        /// <summary>
        /// Execute the IMAP commands in the queue asynchronously.
        /// </summary>
        /// <param name="retries">The number of times to retry executing the command.</param>
        /// <returns>True if all commands in the queue are executed successfully.</returns>
        /// <remarks>Command retries do not apply to the queue which will run idefinitely until empty or cancelled</remarks>
        public async Task<bool> DoCommandsAsync(int retries = -1)
        {
            while (_queueCommand.Count > 0 && _tokenCancel != null && !_tokenCancel.IsCancellationRequested)
            {
                try
                {
                    //Peek in the command queue for the next command
                    var command = _queueCommand.Peek();
                    //
                    //Execute the Imap command
                    //
                    if (await DoCommandAsync(command, retries))
                    {
                        //If successful, dequeue and discard the command
                        lock (_lock)
                            _queueCommand.Dequeue();
                    }
                    //Reset if the command affects folder state
                    if (_imapClient.IsConnected && !_imapFolder.IsOpen)
                        _imapFolder.Open(_folderAccess);
                }
                catch (Exception ex)
                {
                    //We may be disconnected, throw to try again
                    Log.Warn(ex, Identifier + "DoCommands errored out...");
                    throw;
                }
            }

            return _queueCommand.Count == 0;
        }

        /// <summary>
        /// Queues a command to be executed by the IMAP client instance.
        /// </summary>
        /// <param name="command">The command to execute in the IMAP thread.</param>
        public void QueueCommand(OnImapCommand command)
        {
            lock (_lock)
                _queueCommand.Enqueue(command);
            //If idling, wake up and process the command queue
            _tokenDone?.Cancel();
        }
        #endregion

        #region IMAP Events
        /// <summary>
        /// Called when folder message count changes.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        /// <remarks>CountChanged event will fire when new messages arrive in the folder and/or when messages are expunged.</remarks>
        private void OnCountChanged(object sender, EventArgs e)
        {
            var folder = (ImapFolder)sender;
            Log.Debug(Identifier + "{0} message count has changed from {1} to {2}.", folder, _numMessages, folder.Count);

            //If the folder count is more than our tracked number of messages flag and cancel IDLE
            if (folder.Count > _numMessages)
            {
                Log.Debug(Identifier + "{0} new messages have arrived.", folder.Count - _numMessages);
                // Note: This event is called by the ImapFolder (the ImapFolder is not re-entrant).
                //       IMAP commands cannot be performed here so instead flag new messages and
                //       cancel the `done` token to handle new messages in IdleAsync.
                _messagesArrived = true;
                _tokenDone?.Cancel();
            }
            //
            //Track the message count to determine when we have new messages.
            //
            _numMessages = folder.Count;
        }

        /// <summary>
        /// Called when a message is expunged (deleted or moved).
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="MessageEventArgs"/> instance containing the event data.</param>
        private void OnMessageExpunged(object sender, MessageEventArgs e)
        {
            var folder = (ImapFolder)sender;
            Log.Debug(Identifier + "{0} message #{1} has been expunged.", folder, e.Index);
            //
            //Track the message count to determine when we have new messages.
            //
            _numMessages = folder.Count;
        }
        #endregion
    }
}

DoCommandAsync代码中的健壮性模式值得研究。如果你自己的代码写得很好,并且使用错误处理,引发异常的最可能原因是由于服务器连接的问题。此模式旨在允许守护程序重新建立连接,即使这样做需要数小时。这个想法是,在第一个错误时,它将立即重新连接并重试。如果仍然存在连接问题,它将在两次重试之间等待1分钟,然后等待10分钟,然后最终等待一个小时,然后再尝试重新连接并运行命令。还有一种方法可以无限期重试或重试指定的重试次数。

还应该注意的是,与作者的示例一样,使用了两个取消令牌。可以通过调用包装器StopDispose包装器实例访问这些内容。当命令排队时,如果空闲,我们将唤醒。当收到服务器事件时,我们也应该这样做。

首先,让我们演示连接和运行IMAP命令的简单案例(例如删除电子邮件、搜索或获取详细信息或移动邮件等)。

//Connect, perform command, and disconnect synchronously
using (var imapClient = new ImapClientEx(_imapUser))
{
    //The IMAP client will run the command async so we must Wait to ensure the connection does not close before the command is run
    imapClient.DoCommandAsync(MoveEmail, 5).Wait();
}

请注意用于限定ImapClientEx包装器范围的using语句。此代码由其自己的线程执行,当命令运行时,此操作在另一个线程中完成,要运行的函数的指针从线程分流到IMAP线程并由IMAP客户端执行。它将在运行命令之前自动连接。虽然在这种情况下支持异步,但我们将等待,否则我们将过早处理我们的IMAP连接。

private void MoveEmail(ImapClient imapClient, IMailFolder imapFolder)
{
    //Perform an action with the connected imapClient or the opened imapFolder
}

命令队列采用具有MailKit客户端和文件夹参数参数的委托。它由IMAP客户端包装器线程运行,但它是对象的实例,因此您可以完全访问成员变量等。同样,这是一个简单的用例,但显示了客户端连接和运行代码的难易程度。

现在,让我们转到您希望与收件箱建立长期连接以监视新邮件的用例。这需要异步启动和存储IMAP客户端包装器。当客户端运行时,它将保持连接并按照作者的示例监视两个事件:inbox.CountChangedinbox.MessageExpunged。通过监视这一点,我们可以在包装器中公开我们的单个事件:NewMessage。在IMAP客户端运行时,我们所要做的就是将实例保留在成员变量中,以对其他IMAP命令进行排队、接收NewMessage事件或在完成后停止客户端。

protected void ImapConnect()
{
    // Dispose of existing instance, if any.
    if (_imapClient != null)
    {
        _imapClient.NewMessage -= IMAPProcessMessages;
        _imapClient.Stop();
        _imapClient = null;
    }

    _imapClient             = new ImapClientEx(_imapUser);
    _imapClient.NewMessage += IMAPProcessMessages;
    var idleTask            = _imapClient.RunAsync();
    _dtLastConnection       = DateTime.Now;
}

现在应该注意的是,一旦IMAP客户端在启动时连接,就会触发NewMessage事件。这是因为我们的守护程序需要能够关闭,因此必须跟踪最后处理的消息。执行此操作的最佳方法是跟踪上次处理的UID。这样,每当触发事件时,您只需搜索自上次跟踪的UID以来的新UID

private void IMAPProcessMessages(ImapClient imapClient, IMailFolder imapFolder)
{
    LogSvc.Debug(this, "IMAP: Checking emails...");
    _dtLastConnection = DateTime.Now;
    //
    //Retrieve last index from DB
    //
    if (_currentUid  == 0)
        _currentUid  = (uint)TaskEmailData.FetchLastUID(_taskType);
    LogSvc.Debug(this, "IMAP: Last email index from DB: " + _currentUid.ToString());
    //
    //Process messages since last processed UID
    //
    int currentIndex = imapFolder.Count - 1;
    if (currentIndex >= 0)
    {
        //
        //Create range from the current UID to the max
        //
        var range    = new UniqueIdRange(new UniqueId((uint)_currentUid + 1), UniqueId.MaxValue);
        //
        //Get the UIDs newer than the current UID
        //
        var uids     = imapFolder.Search(range, SearchQuery.All);
        //
        if (uids.Count > 0)
        {
            LogSvc.Info(this, "IMAP: Processing {0} missed emails.", uids.Count);
            foreach (var uid in uids)
            {
                //
                //Get the email
                //
                var email = imapFolder.GetMessage(uid);
                //
                //Process and enqueue new message
                //
                ImapProcessMessage(imapClient, imapFolder, uid, email);
            }
            //
            //Pulse the lock to process new tasks...
            //
            Pulse();
        }
        else
        {
            LogSvc.Debug(this, "IMAP: No missed emails.");
        }
    }
    else
    {
        LogSvc.Debug(this, "IMAP: No missed emails.");
    }
}

我不会向您展示,但是,可以说,我的守护程序中有一个额外的冗余级别,它跟踪连接年龄并在指定的非活动时间后简单地回收连接。这样做是因为,虽然它更有用,但我们旧的IMAP API经常断开连接,尽管它错误地报告它仍然处于连接状态。

最后,当守护程序因任何原因被关闭时,我们需要StopDispose断开并清理IMAP连接。Stop将触发取消令牌,以便IMAP任务线程在其自己的时间内关闭。直接调用Dispose将同步执行相同的操作。此外,可以在包装器实例上重复调用Dispose,并且仍然可以安全使用,因为它将根据需要重新连接。

_imapClient?.Dispose();

这花了几周的时间来编写和测试。我的老板对它被分享很酷,所以我希望省去其他人从头开始写这篇文章的痛苦。虽然MailKit可能处于频谱的基本端,但我们构建了一个非常强大的解决方案,并且无疑将具有比以前更好的正常运行时间指标。非常感谢作者和MailKit用户社区提供撰写本文所需的所有见解和知识。

本文最初发表于 Robust Daemon Monitoring Inbox with MailKit w/ OAuth2 | Coding Lifestyle

https://www.codeproject.com/Articles/5342831/Robust-Daemon-Monitoring-Inbox-with-MailKit-w-OAut

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值