Search code examples
c#imapmailkit

Exception trying to get folder in MailKit, but not when enumerating folders first


I'm using MaikKit, and am trying to work out how to get the mail in a Jim folder I set up in a Gmail account. I tried enumerating the folders as follows...

    private void GetFolders() {
      using ImapClient client = new();
      EmailAccount emailAccount = EmailAccountOptions.Value;
      client.Connect(emailAccount.Server, emailAccount.Port, SecureSocketOptions.SslOnConnect);
      client.Authenticate(emailAccount.UserName, emailAccount.Password);

      foreach (FolderNamespace ns in client.PersonalNamespaces) {
        IMailFolder folder = client.GetFolder(ns);
        _msg += $"<br/>Ns: {ns.Path} / {folder.FullName}";
        foreach (IMailFolder subfolder in folder.GetSubfolders()) {
          _msg += $"<br/>&nbsp;&nbsp; {subfolder.FullName}";
        }
      }

      try {
        IMailFolder jim = client.GetFolder(new FolderNamespace('/', "Jim"));
        jim.Open(FolderAccess.ReadOnly);
        _msg += $"<br/>Got Jim, has {jim.Count} email(s)";
      }
      catch (Exception ex) {
        _msg += $"<br/>Ex ({ex.GetType()}): {ex.Message}";
      }
    }

This shows the following...

Ns: /
   INBOX
   Jim
   [Gmail]
   ✔
   ✔✔
Got Jim, has 1 email(s)

So, it seems I can access Jim without problem. As the purpose of this was to access Jim only, the enumeration isn't needed. However, when I removed it, I got an exception...

Ns: /
Ex (MailKit.FolderNotFoundException): The requested folder could not be found

After some trial and error, I found out that the following works...

private void GetFolders() {
  using ImapClient client = new();
  EmailAccount emailAccount = EmailAccountOptions.Value;
  client.Connect(emailAccount.Server, emailAccount.Port, SecureSocketOptions.SslOnConnect);
  client.Authenticate(emailAccount.UserName, emailAccount.Password);

  foreach (FolderNamespace ns in client.PersonalNamespaces) {
    IMailFolder folder = client.GetFolder(ns);
    var subs = folder.GetSubfolders();
  }

  try {
    IMailFolder jim = client.GetFolder(new FolderNamespace('/', "Jim"));
    jim.Open(FolderAccess.ReadOnly);
    _msg += $"<br/>Got Jim, has {jim.Count} email(s)";
  }
  catch (Exception ex) {
    _msg += $"<br/>Ex ({ex.GetType()}): {ex.Message}";
  }
}

...but if I remove the call to folder.GetSubfolders() it throws an exception.

If I change the code to look for INBOX instead...

IMailFolder jim = client.GetFolder(new FolderNamespace('/', "INBOX"));

...then it works fine even without the call to folder.GetSubfolders().

Anyone any idea why this happens? As far as I can see, the foreach loop isn't doing anything that should affect Jim.


Solution

  • Just so it's clear, using ImapClient.GetFolder (FolderNamespace) the way you are using it is not the correct way of using that API.

    A few general API rules to consider when using MailKit:

    1. All APIs that require network I/O take a CancellationToken argument. So if a *Client or *Folder API doesn't take a CancellationToken, that means it's a cache lookup or something else that doesn't require hitting the network.
    2. MailKit APIs don't require you to call a constructor just to pass a string to the method ;-)

    With the ImapClient.GetFolder(FolderNamespace) API, there is an equivalent ImapFolder.GetFolder(string path, CancellationToken) API that is meant to be used for such tasks.

    (Note: The only reason that the FolderNamespace .ctor is public is because someone out there may want to implement the MailKit IMailStore or IImapClient interfaces and will need to be able to create FolderNamespace instances).

    That said, I am loathe to recommend this API because MailKit's ImapFolder was designed to get folders 1 level at a time via the ImapFolder.GetSubfolders() and/or ImapFolder.GetSubfolder() APIs and so the ImapClient.GetFolder() API is a huge hack that has to recursively fetch folder paths and string them together.

    Keep in mind that ImapFolder instances have a ParentFolder property that the MailKit API guarantees will always be set.

    That makes things difficult to guarantee if the developer can just request rando folders anywhere in the tree at the ImapClient level using a full path.

    Anyway... yea, the Imapclient.GetFolder (string path, CancellationToken) API exists and it works and you can use it if you want to, but let it be known that I hate that API.

    Okay, so now on to why your client.GetFolder(new FolderNamespace(...)) hack doesn't work if you remove the call to GetSubfolders()

    As you've discovered, IMailFolder jim = client.GetFolder(new FolderNamespace('/', "Jim")); only works if a call such as var subs = folder.GetSubfolders(); exists before it that happens to return the /Jim folder as one of the subfolders.

    Your question is: why?

    (or maybe your question is: why doesn't it work without that line?)

    Either way, the answer is the same.

    MailKit's ImapClient keeps a cache of known folders that exist in the IMAP folder tree so that it can quickly and easily find the parent of each folder and doesn't have to explicitly call LIST on each parent node in the path in order to build the ImapFolder chain (remmeber: each ImapFolder has a ParentFolder property that points to an ImapFolder instance that represents its direct parent all the way back up to the root).

    So... that GetSubfolders() call is adding the /Jim folder to the cache.

    Since the GetFolder (new FolderNamesdpace ('/', "Jim")) call cannot go out to the network, it relies on the internal cache. The only folders that exist in the cache after connecting are the folders that are returned in the NAMESPACE command and INBOX. Everything else gets added to the cache as they become known due to LIST responses (as a result of GetSubfolders() and GetSubfolder() calls).