Skip to content

Commit

Permalink
Added unit tests for LIST responses with NIL (or "") directory separa…
Browse files Browse the repository at this point in the history
…tors
  • Loading branch information
jstedfast committed Dec 31, 2023
1 parent 6ad1c01 commit 9a1c39c
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 4 deletions.
26 changes: 24 additions & 2 deletions MailKit/Net/Imap/ImapFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,13 @@ ImapCommand QueueGetSubfoldersCommand (StatusItems items, bool subscribedOnly, C
{
CheckState (false, false);

// Any folder with a nil directory separator cannot have children.
if (DirectorySeparator == '\0') {
status = false;
list = null;
return null;
}

// Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%'
// in order to reduce the list of folders returned by our LIST command.
var pattern = new StringBuilder (EncodedName.Length + 2);
Expand Down Expand Up @@ -1769,6 +1776,9 @@ public override IList<IMailFolder> GetSubfolders (StatusItems items, bool subscr
{
var ic = QueueGetSubfoldersCommand (items, subscribedOnly, cancellationToken, out var list, out var status);

if (ic == null)
return Array.Empty<IMailFolder> ();

Engine.Run (ic);

var children = ProcessGetSubfoldersResponse (ic, list, out var unparented);
Expand Down Expand Up @@ -1823,6 +1833,9 @@ public override async Task<IList<IMailFolder>> GetSubfoldersAsync (StatusItems i
{
var ic = QueueGetSubfoldersCommand (items, subscribedOnly, cancellationToken, out var list, out var status);

if (ic == null)
return Array.Empty<IMailFolder> ();

await Engine.RunAsync (ic).ConfigureAwait (false);

var children = ProcessGetSubfoldersResponse (ic, list, out var unparented);
Expand Down Expand Up @@ -1852,6 +1865,15 @@ ImapCommand QueueGetSubfolderCommand (string name, CancellationToken cancellatio

CheckState (false, false);

// Any folder with a nil directory separator cannot have children.
if (DirectorySeparator == '\0') {
encodedName = null;
fullName = null;
folder = null;
list = null;
return null;
}

fullName = FullName.Length > 0 ? FullName + DirectorySeparator + name : name;
encodedName = Engine.EncodeMailboxName (fullName);

Expand Down Expand Up @@ -1931,7 +1953,7 @@ public override IMailFolder GetSubfolder (string name, CancellationToken cancell
var ic = QueueGetSubfolderCommand (name, cancellationToken, out var list, out var fullName, out var encodedName, out var folder);

if (ic == null)
return folder;
return folder ?? throw new FolderNotFoundException (name);

Engine.Run (ic);

Expand Down Expand Up @@ -1993,7 +2015,7 @@ public override async Task<IMailFolder> GetSubfolderAsync (string name, Cancella
var ic = QueueGetSubfolderCommand (name, cancellationToken, out var list, out var fullName, out var encodedName, out var folder);

if (ic == null)
return folder;
return folder ?? throw new FolderNotFoundException (name);

await Engine.RunAsync (ic).ConfigureAwait (false);

Expand Down
4 changes: 2 additions & 2 deletions MailKit/Net/Imap/ImapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ public static void ParseFolderList (ImapEngine engine, List<ImapFolder> list, bo
if (token.Type == ImapTokenType.QString) {
var qstring = (string) token.Value;

delim = qstring[0];
delim = qstring.Length > 0 ? qstring[0] : '\0';
} else if (token.Type == ImapTokenType.Nil) {
delim = '\0';
} else {
Expand Down Expand Up @@ -755,7 +755,7 @@ public static async Task ParseFolderListAsync (ImapEngine engine, List<ImapFolde
if (token.Type == ImapTokenType.QString) {
var qstring = (string) token.Value;

delim = qstring[0];
delim = qstring.Length > 0 ? qstring[0] : '\0';
} else if (token.Type == ImapTokenType.Nil) {
delim = '\0';
} else {
Expand Down
87 changes: 87 additions & 0 deletions UnitTests/Net/Imap/ImapFolderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,93 @@ public async Task TestLiteralFolderNamesAsync ()
}
}

static IList<ImapReplayCommand> CreateNilDirectorySeparatorCommands ()
{
return new List<ImapReplayCommand> {
new ImapReplayCommand ("", "common.basic-greeting.txt"),
new ImapReplayCommand ("A00000000 CAPABILITY\r\n", "common.capability.txt"),
new ImapReplayCommand ("A00000001 LOGIN username password\r\n", ImapReplayCommandResponse.OK),
new ImapReplayCommand ("A00000002 CAPABILITY\r\n", "common.capability.txt"),
new ImapReplayCommand ("A00000003 LIST \"\" \"\"\r\n", "common.list-namespace.txt"),
new ImapReplayCommand ("A00000004 LIST \"\" \"INBOX\"\r\n", "common.list-inbox.txt"),
new ImapReplayCommand ("A00000005 LIST \"\" \"%\"\r\n", "common.list-nil-folder-delim.txt"),
};
}

[Test]
public void TestNilDirectorySeparator ()
{
var commands = CreateNilDirectorySeparatorCommands ();

using (var client = new ImapClient () { TagPrefix = 'A' }) {
try {
client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None);
} catch (Exception ex) {
Assert.Fail ($"Did not expect an exception in Connect: {ex}");
}

try {
client.Authenticate ("username", "password");
} catch (Exception ex) {
Assert.Fail ($"Did not expect an exception in Authenticate: {ex}");
}

var personal = client.GetFolder (client.PersonalNamespaces[0]);
var subfolders = personal.GetSubfolders (false);

Assert.That (subfolders.Count, Is.EqualTo (3), "Count");
Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX"));
Assert.That (subfolders[1].Name, Is.EqualTo ("Folder1"));
Assert.That (subfolders[1].DirectorySeparator, Is.EqualTo ('\0'));
Assert.That (subfolders[2].Name, Is.EqualTo ("Folder2"));
Assert.That (subfolders[2].DirectorySeparator, Is.EqualTo ('\0'));

Assert.Throws<FolderNotFoundException> (() => subfolders[1].GetSubfolder ("Subfolder"));

var empty = subfolders[1].GetSubfolders (false);
Assert.That (empty.Count, Is.EqualTo (0), "GetSubfolders when DirectorySeparator is nil");

client.Disconnect (false);
}
}

[Test]
public async Task TestNilDirectorySeparatorAsync ()
{
var commands = CreateNilDirectorySeparatorCommands ();

using (var client = new ImapClient () { TagPrefix = 'A' }) {
try {
await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None);
} catch (Exception ex) {
Assert.Fail ($"Did not expect an exception in Connect: {ex}");
}

try {
await client.AuthenticateAsync ("username", "password");
} catch (Exception ex) {
Assert.Fail ($"Did not expect an exception in Authenticate: {ex}");
}

var personal = client.GetFolder (client.PersonalNamespaces[0]);
var subfolders = await personal.GetSubfoldersAsync (false);

Assert.That (subfolders.Count, Is.EqualTo (3), "Count");
Assert.That (subfolders[0].Name, Is.EqualTo ("INBOX"));
Assert.That (subfolders[1].Name, Is.EqualTo ("Folder1"));
Assert.That (subfolders[1].DirectorySeparator, Is.EqualTo ('\0'));
Assert.That (subfolders[2].Name, Is.EqualTo ("Folder2"));
Assert.That (subfolders[2].DirectorySeparator, Is.EqualTo ('\0'));

Assert.ThrowsAsync<FolderNotFoundException> (() => subfolders[1].GetSubfolderAsync ("Subfolder"));

var empty = await subfolders[1].GetSubfoldersAsync (false);
Assert.That (empty.Count, Is.EqualTo (0), "GetSubfolders when DirectorySeparator is nil");

await client.DisconnectAsync (false);
}
}

static IList<ImapReplayCommand> CreateAppendLimitCommands ()
{
return new List<ImapReplayCommand> {
Expand Down
4 changes: 4 additions & 0 deletions UnitTests/Net/Imap/Resources/common/list-nil-folder-delim.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
* LIST () "/" INBOX
* LIST (\HasNoChildren) NIL "Folder1"
* LIST (\HasNoChildren) "" "Folder2"
A######## OK LIST Completed

0 comments on commit 9a1c39c

Please sign in to comment.