0
votes

when new email is received, the idle stop method is ran and there it says Imap client is currently busy processing in another thread. i assume its because idle command is still running in the background thread ? even tho i called thread.Join() method, it wont end. i am sturck here for quite some time, and the example demo on MailKit github only shows how to handle it with help from manual user input such as Console.ReadKey(). i am pretty sure i am missing some major point or the code have major flaws but i have searched many times for an answer and there doesnt seem to be any major results other than the github example

protocol logger when idle start and message recevied until the exception occurs

S: * OK [UIDNEXT 21641] Predicted next UID.
S: * OK [HIGHESTMODSEQ 881089]
S: A00000006 OK [READ-WRITE] INBOX selected. (Success)
C: A00000007 IDLE
S: + idling
S: * 21512 EXISTS
C: DONE

method which starts idle

        IdleClient.Inbox.MessageExpunged += OnMessageExpunged;
        IdleClient.Inbox.CountChanged += OnInboxCountChanged;

        ImapToken = new CancellationTokenSource();
        SetTokenValues(ImapToken.Token);

        ImapToken.Token.ThrowIfCancellationRequested();
        ImapThreadInfo = Helpers.InBackgroundThread(ImapIdleLoop, UniqueAccountId, true);

declarations related to idle

    private (int, Thread) ImapThreadInfo;
    private CancellationToken CancellationToken { get; set; }
    private CancellationToken DoneToken { get; set; }
    private CancellationTokenSource ImapToken { get; set; }
    private CancellationTokenSource Timeout { get; set; }


    private bool IsCancellationRequested => CancellationToken.IsCancellationRequested || DoneToken.IsCancellationRequested;
    private readonly object Mutex = new object();

    private void CancelTimeout() {
        lock (Mutex) {
            Timeout?.Cancel();
        }
    }

    private void SetTimeoutSource(CancellationTokenSource source) {
        lock (Mutex) {
            Timeout = source;

            if (Timeout != null && IsCancellationRequested) {
                Timeout.Cancel();
            }
        }
    }

    private void SetTokenValues(CancellationToken doneToken, CancellationToken cancellationToken = default) {
        CancellationToken = cancellationToken;
        DoneToken = doneToken;
        doneToken.Register(CancelTimeout);
    }

stop idle method

public void StopImapIdle(bool clientDisconnect) {
        ImapToken.Cancel();
        try {
            Task.Factory.StartNew(() => {
                ImapThreadInfo.Item2?.Join();
            });
            ImapToken.Dispose();

            if (!clientDisconnect) {
                return;
            }

            if (IdleClient.IsConnected && IdleClient.IsIdle) {
                while (true) {
                    if (!IdleClient.IsIdle) {
                        BotLogger.Log("Idling has been stopped.", LogLevels.Trace);
                        break;
                    }
                    BotLogger.Log("Waiting for idle client to stop idling...", LogLevels.Trace);
                }
            }

            lock (IdleClient.SyncRoot) {
                //Error here
                IdleClient.Disconnect(true);
                BotLogger.Log("Imap client has been disconnected.", LogLevels.Trace);
            }
        }
        catch (NullReferenceException) {
            BotLogger.Log("There is no thread with the specified uniqueID", LogLevels.Warn);
        }
        IsAccountLoaded = false;
    }

idle loop method

private void ImapIdleLoop() {
        while (!IsCancellationRequested) {
            Timeout = new CancellationTokenSource(new TimeSpan(0, 9, 0));

            try {
                SetTimeoutSource(Timeout);
                if (IdleClient.Capabilities.HasFlag(ImapCapabilities.Idle)) {
                    lock (IdleClient.SyncRoot) {
                        IdleClient.Idle(Timeout.Token, CancellationToken);
                    }
                }
                else {
                    lock (IdleClient.SyncRoot) {
                        IdleClient.NoOp(CancellationToken);
                    }
                    WaitHandle.WaitAny(new[] { Timeout.Token.WaitHandle, CancellationToken.WaitHandle });
                }
            }
            catch (OperationCanceledException) {
                // This means that idle.CancellationToken was cancelled, not the DoneToken nor the timeout.
                break;
            }
            catch (ImapProtocolException) {
                // The IMAP server sent garbage in a response and the ImapClient was unable to deal with it.
                // This should never happen in practice, but it's probably still a good idea to handle it.
                // 
                // Note: an ImapProtocolException almost always results in the ImapClient getting disconnected.
                IsAccountLoaded = false;
                break;
            }
            catch (ImapCommandException) {
                // The IMAP server responded with "NO" or "BAD" to either the IDLE command or the NOOP command.
                // This should never happen... but again, we're catching it for the sake of completeness.
                break;
            }
            catch (SocketException) {


            }
            catch (ServiceNotConnectedException) {

            }
            catch (IOException) {

            }
            finally {
                // We're about to Dispose() the timeout source, so set it to null.
                SetTimeoutSource(null);
            }
            Timeout?.Dispose();
        }
    }
1

1 Answers

0
votes

The problem is that you are waiting for the thread to join from within the ImapClient's event callback which means you are blocking the ImapClient from continuing.

The solution is: don't do that.

MailKit's IMAP events are emitted while the IMAP command processor is still processing the server responses, so you cannot invoke more commands on the same ImapClient within those event handlers.

What you need to do instead is to implement some sort of command queue in your program and then, within the CountChanged event handler (or whatever handler you are handling), queue the next command(s) to invoke once the current command completes.

An easy way to do this is to keep a System.Threading.Tasks.Task somewhere where your event handler has access to it and can then do:

task = task.ContinueWith (...);

That's a simple way of implementing a command queue.