rklec
rklec

Reputation: 339

How to create (or get) folders in MailKit with multiple sub-folders when they are separated in one string with slashes et al?

I use MailKit for creating a folder. This is simple, I can e.g. call CreateFolderAsync (if I want to async way) on the parent folder.

    public async Task<IMailFolder> GetOrCreateFolder(string folderName)
    {
        var rootFolder = await authenticatedClient.GetFolderAsync(imapClient.PersonalNamespaces[0].Path);
        var existingFolders = await rootFolder.GetSubfoldersAsync();
        var matchingFolder = existingFolders.FirstOrDefault(folder => folder.FullName == folderName);

        if (matchingFolder == null)
        {
            matchingFolder = await rootFolder.CreateAsync(folderName, true);
        }

        return matchingFolder;
    }

However, if I pass in a string like GetOrCreateFolder("folder/subfolder") or GetOrCreateFolder("folder.subfolder") (because I later found out many IMAP servers apparently use . as a directory separator sign. This is defined/returned by the server in IMailFolder.DirectorySeparator e.g.).

Both will throw an exception if you try this ("The name is not a legal folder name"):

System.ArgumentException
  HResult=0x80070057
  Nachricht = The name is not a legal folder name. Arg_ParamName_Name
  Quelle = MailKit
  Stapelüberwachung:
   bei MailKit.Net.Imap.ImapFolder.QueueCreateCommand(String name, Boolean isMessageFolder, CancellationToken cancellationToken, String& encodedName)
   bei MailKit.Net.Imap.ImapFolder.CreateAsync(String name, Boolean isMessageFolder, CancellationToken cancellationToken)
   // ...

  Diese Ausnahme wurde ursprünglich von dieser Aufrufliste ausgelöst:
    MailKit.Net.Imap.ImapFolder.QueueCreateCommand(string, bool, System.Threading.CancellationToken, out string)
    MailKit.Net.Imap.ImapFolder.CreateAsync(string, bool, System.Threading.CancellationToken)

You can obviously hardcode the creation of the first folder, and then the second, but this is really not flexible at all. I mean, after all, mkdir on Linux e.g. also supports creating multiple folders at once, why can't we here?

So, how can I just make this work?

Upvotes: 0

Views: 82

Answers (2)

rklec
rklec

Reputation: 339

Actually, I have an answer where I just recursively create such a directory:

    private const char directorySeparator = '/';

    /// <summary>
    /// Gets a folder or sub-folder (recursively). If needed, the folder(s) is/are automatically created.
    /// </summary>
    /// <param name="folderPath">The full path of the folder to create, separated by slashes (<see cref="directorySeparator"/>).</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task<IMailFolder> GetOrCreateFolder(string folderPath)
    {
        var rootFolder = await imapClient.GetFolderAsync(imapClient.PersonalNamespaces[0].Path);
        return await GetOrCreateFolderRecursiveAsync(rootFolder, folderPath);
    }

    private async Task<IMailFolder> GetOrCreateFolderRecursiveAsync(IMailFolder parentFolder, string folderPath)
    {
        if (string.IsNullOrEmpty(folderPath) || folderPath == ".")
            return parentFolder;

        var (currentFolderName, remainingPath) = SplitCurrentFolderAndRemainingPath(folderPath, [directorySeparator, parentFolder.DirectorySeparator]);

        IMailFolder matchingFolder;
        try
        {
            matchingFolder = await parentFolder.GetSubfolderAsync(currentFolderName);
        }
        catch (FolderNotFoundException)
        {
            logger.LogInformation($"Creating folder {currentFolderName} under {parentFolder.FullName}.");
            matchingFolder = await parentFolder.CreateAsync(currentFolderName, true);
        }

        return await GetOrCreateFolderRecursiveAsync(matchingFolder, remainingPath);
    }

    private static (string currentFolderName, string remainingPath) SplitCurrentFolderAndRemainingPath(string folderPath,
        char[] separator)
    {
        var parts = folderPath.Split(separator, 2, StringSplitOptions.RemoveEmptyEntries);
        var currentFolderName = parts[0];
        var remainingPath = parts.Length > 1 ? parts[1] : string.Empty;
        return (currentFolderName, remainingPath);
    }

It just assumes you have an already authenticated imapClient aka ImapClient of course. The logger is also optional.

It allows / as a hardcoded directory separator or IMailFolder.DirectorySeparator from the server, which I have mentioned before. That, AFAIK, usually is a . aka dot. AFAIK if IMailFolder.DirectorySeparator appears in your folder, the server will reject it anyway, as shown above, so it does not hurt to split it here.

I only implemented the asynchronous way, because that's what I use, but obviously, the synchronous way would be equivalent.

Upvotes: 0

jstedfast
jstedfast

Reputation: 38653

Change your implementation to this:

public async Task<IMailFolder> GetOrCreateFolder(string folderName)
{
    var rootFolder = await authenticatedClient.GetFolderAsync(imapClient.PersonalNamespaces[0]);
    var path = folderName.Split(new char[] { rootFolder.DirectorySeparator }, folderName);
    var folder = rootFolder;
    int i = 0;

    // Traverse down the path getting each node until one fails...
    while (i < path.Count) {
        try {
            folder = await folder.GetSubfolderAsync(path[i]);
            i++;
        } catch (FolderNotFoundException) {
            break;
        }
    }

    // Create any remaining path nodes...
    while (i < path.Count) {
        folder = await folder.CreateAsync(path[i], i + 1 == path.Count);
        i++;
    }

    return folder;
}

Upvotes: 1

Related Questions