Reputation: 9492
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/> {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
.
Upvotes: 3
Views: 3911
Reputation: 38643
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:
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.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).
Upvotes: 2