2026-10-04 12:26:26 +02:00
using AIStudio.Tools.Mail ;
namespace AIStudio.Tests.Tools.Mail ;
/// <summary>
/// Checks which folders of a mailbox the sync works through.
/// </summary>
[TestFixture]
public sealed class MailFolderSelectionTests
{
private static readonly IReadOnlyList < MailServerFolder > SERVER_FOLDERS =
[
Folder("Archive", MailFolderSpecialUse.ARCHIVE),
2026-10-10 10:48:54 +02:00
Folder("Drafts", MailFolderSpecialUse.DRAFTS),
2026-10-04 12:26:26 +02:00
Folder("Junk", MailFolderSpecialUse.JUNK),
Folder("Projects"),
Folder("Projects/Alpha"),
Folder("Projects/Alpha/Drafts of the board"),
Folder("ProjectsOld"),
Folder("Sent", MailFolderSpecialUse.SENT),
2026-10-10 10:48:54 +02:00
Folder("Sent/2024"),
2026-10-04 12:26:26 +02:00
Folder("Starred", MailFolderSpecialUse.FLAGGED),
Folder("Trash", MailFolderSpecialUse.TRASH),
Folder("[Gmail] ", canSelect: false),
Folder ( "[Gmail]/All Mail" , MailFolderSpecialUse . ALL ),
Folder ( "[Gmail]/Important" , MailFolderSpecialUse . IMPORTANT ),
Folder ( "INBOX" , isInbox : true ),
Folder ( "INBOX/Invoices" ),
];
2026-10-10 10:48:54 +02:00
private static readonly DateTimeOffset RECEIVED_SINCE = new ( 2025 , 10 , 10 , 0 , 0 , 0 , TimeSpan . Zero );
2026-10-04 12:26:26 +02:00
[Test]
public void TheWholeMailboxLeavesOutWhatWasThrownAway ()
{
2026-10-10 10:48:54 +02:00
var selection = MailFolderSelection . Select ( SERVER_FOLDERS , string . Empty , true );
2026-10-04 12:26:26 +02:00
Assert . Multiple (() =>
{
Assert . That ( selection . RootFolderFound , Is . True );
Assert . That ( selection . Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new []
{
"INBOX" ,
2026-10-10 10:48:54 +02:00
"Sent" ,
"Drafts" ,
2026-10-04 12:26:26 +02:00
"Archive" ,
"INBOX/Invoices" ,
"Projects" ,
"Projects/Alpha" ,
"Projects/Alpha/Drafts of the board" ,
"ProjectsOld" ,
2026-10-10 10:48:54 +02:00
"Sent/2024" ,
2026-10-04 12:26:26 +02:00
"[Gmail]/All Mail" ,
2026-10-10 10:48:54 +02:00
}), "The inbox comes first, then the sent mails and the drafts; the trash, the junk folder, the virtual folders and the grouping folder stay out." );
2026-10-04 12:26:26 +02:00
});
}
2026-10-10 10:48:54 +02:00
[Test]
public void WithoutTheSentMailsAndDraftsTheWholeMailboxStaysWhole () =>
Assert . That ( MailFolderSelection . Select ( SERVER_FOLDERS , string . Empty , false ). Folders , Is . EqualTo ( MailFolderSelection . Select ( SERVER_FOLDERS , string . Empty , true ). Folders ), "Without a root folder, the sent mails and the drafts belong to the whole mailbox anyway." );
2026-10-04 12:26:26 +02:00
[Test]
public void ARootFolderTakesItsSubfoldersAlongAndNothingElse () =>
2026-10-10 10:48:54 +02:00
Assert . That ( MailFolderSelection . Select ( SERVER_FOLDERS , "Projects" , false ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "Projects" , "Projects/Alpha" , "Projects/Alpha/Drafts of the board" }), "A folder merely starting with the same name got in." );
[TestCase(true, new[] { "INBOX" , "Sent" , "Drafts" , "INBOX/Invoices" })]
[TestCase(false, new[] { "INBOX" , "INBOX/Invoices" })]
public void SentMailsAndDraftsComeAlongFromOutsideTheRootFolderWhenIncluded ( bool includeSentAndDrafts , string [] expectedFolders ) =>
Assert . That ( MailFolderSelection . Select ( SERVER_FOLDERS , "INBOX" , includeSentAndDrafts ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( expectedFolders ), "The subfolders of the sent mails must stay out, since the server marks only the folder itself." );
[TestCase(true)]
[TestCase(false)]
public void SentMailsAndDraftsBelowTheRootFolderCountOnce ( bool includeSentAndDrafts )
{
// Some servers keep every folder below the inbox, e.g. Dovecot with the namespace prefix "INBOX.":
IReadOnlyList < MailServerFolder > serverFolders =
[
Folder("INBOX", isInbox: true),
Folder("INBOX/Drafts", MailFolderSpecialUse.DRAFTS),
Folder("INBOX/Invoices"),
Folder("INBOX/Sent", MailFolderSpecialUse.SENT),
] ;
Assert . That ( MailFolderSelection . Select ( serverFolders , "INBOX" , includeSentAndDrafts ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "INBOX" , "INBOX/Sent" , "INBOX/Drafts" , "INBOX/Invoices" }));
}
[Test]
public void SentMailsAsRootFolderCountOnce () =>
Assert . That ( MailFolderSelection . Select ( SERVER_FOLDERS , "Sent" , true ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "Sent" , "Drafts" , "Sent/2024" }));
[Test]
public void FoldersTheServerDoesNotMarkAreNeverToldByTheirNames ()
{
IReadOnlyList < MailServerFolder > serverFolders = [ Folder ( "INBOX" , isInbox : true ), Folder ( "Drafts" ), Folder ( "Sent" )];
Assert . That ( MailFolderSelection . Select ( serverFolders , "INBOX" , true ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "INBOX" }), "A server without the special-use marks leaves nothing to tell these folders by." );
}
2026-10-04 12:26:26 +02:00
[Test]
public void TheInboxIsMatchedWhateverItsCase () =>
2026-10-10 10:48:54 +02:00
Assert . That ( MailFolderSelection . Select ( SERVER_FOLDERS , "inbox" , false ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "INBOX" , "INBOX/Invoices" }));
2026-10-04 12:26:26 +02:00
[Test]
public void AVirtualFolderCountsWhenItIsTheRootFolder () =>
2026-10-10 10:48:54 +02:00
Assert . That ( MailFolderSelection . Select ( SERVER_FOLDERS , "Starred" , false ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "Starred" }));
2026-10-04 12:26:26 +02:00
[Test]
public void TheTrashNeverCountsNotEvenAsRootFolder ()
{
2026-10-10 10:48:54 +02:00
var selection = MailFolderSelection . Select ( SERVER_FOLDERS , "Trash" , false );
2026-10-04 12:26:26 +02:00
Assert . Multiple (() =>
{
Assert . That ( selection . Folders , Is . Empty );
Assert . That ( selection . RootFolderFound , Is . True );
});
}
[Test]
public void ARootFolderTheServerNoLongerListsIsReported ()
{
2026-10-10 10:48:54 +02:00
var selection = MailFolderSelection . Select ( SERVER_FOLDERS , "Projects/Beta" , true );
2026-10-04 12:26:26 +02:00
Assert . Multiple (() =>
{
2026-10-10 10:48:54 +02:00
Assert . That ( selection . RootFolderFound , Is . False , "A renamed root folder would read as a mailbox which became empty, even when the sent mails and the drafts are still there." );
Assert . That ( selection . Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "Sent" , "Drafts" }));
2026-10-04 12:26:26 +02:00
});
}
[Test]
public void AServerWithoutHierarchyHasNoSubfolders ()
{
IReadOnlyList < MailServerFolder > flatFolders = [ Folder ( "Projects" , separator : '\0' ), Folder ( "Projects.Alpha" , separator : '\0' )];
2026-10-10 10:48:54 +02:00
Assert . That ( MailFolderSelection . Select ( flatFolders , "Projects" , true ). Folders . Select ( folder => folder . FullName ), Is . EqualTo ( new [] { "Projects" }));
2026-10-04 12:26:26 +02:00
}
2026-10-10 10:48:54 +02:00
[TestCase(MailFolderSpecialUse.DRAFTS, false)]
[TestCase(MailFolderSpecialUse.SENT, true)]
[TestCase(MailFolderSpecialUse.ALL, true)]
[TestCase(MailFolderSpecialUse.NONE, true)]
public void OnlyDraftsCountWhateverTheirAge ( MailFolderSpecialUse specialUse , bool keepsThePeriod )
{
var expected = keepsThePeriod ? RECEIVED_SINCE : ( DateTimeOffset ?) null ;
Assert . That ( MailFolderSelection . GetReceivedSince ( Folder ( "Folder" , specialUse ), RECEIVED_SINCE ), Is . EqualTo ( expected ));
}
[Test]
public void AMailboxWithoutPeriodKeepsAllMailsOfEveryFolder () =>
Assert . That ( MailFolderSelection . GetReceivedSince ( Folder ( "INBOX" , isInbox : true ), null ), Is . Null );
2026-10-04 12:26:26 +02:00
private static MailServerFolder Folder ( string fullName , MailFolderSpecialUse specialUse = MailFolderSpecialUse . NONE , bool isInbox = false , bool canSelect = true , char separator = '/' )
{
var separatorIndex = separator is '\0' ? - 1 : fullName . LastIndexOf ( separator );
var parentFullName = separatorIndex < 0 ? string . Empty : fullName [.. separatorIndex ];
var name = separatorIndex < 0 ? fullName : fullName [( separatorIndex + 1 )..];
return new ( fullName , name , parentFullName , separator , specialUse , isInbox , canSelect );
}
}