/ tests / ImapFolderListParserTest.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\library\mail\ImapFolderListParser;
use seekquarry\yioop\library\UnitTest;

/**
 * Unit tests for ImapFolderListParser. Each test feeds untagged
 * LIST response lines shaped the way a real IMAP server sends them
 * and checks that the folder names and their selectable flags come
 * back correctly. The cases cover a normal quoted folder, a
 * \Noselect hierarchy folder, a folder whose name arrives as a {N}
 * literal, a NIL hierarchy delimiter, and a non-LIST line that must
 * be skipped.
 *
 * @author Chris Pollett
 */
class ImapFolderListParserTest extends UnitTest
{
    /**
     * No setup state is needed; the parser is pure and static.
     */
    public function setUp()
    {
    }
    /**
     * No setup state is needed; nothing to tear down.
     */
    public function tearDown()
    {
    }
    /**
     * A normal quoted folder name with ordinary attributes: it
     * should parse to its name and be marked selectable.
     */
    public function normalQuotedFolderTestCase()
    {
        $lines = ['* LIST (\\HasNoChildren) "/" "INBOX"'];
        $folders = ImapFolderListParser::parse($lines);
        $this->assertEqual(1, count($folders),
            "One LIST line yields one folder");
        $this->assertEqual("INBOX", $folders[0]["NAME"],
            "The quoted folder name is parsed");
        $this->assertTrue($folders[0]["SELECTABLE"],
            "A folder without \\Noselect is selectable");
    }
    /**
     * A folder carrying the \Noselect attribute exists only as a
     * point in the hierarchy and must come back not selectable.
     */
    public function noselectFolderTestCase()
    {
        $lines = ['* LIST (\\Noselect \\HasChildren) "/" "Archive"'];
        $folders = ImapFolderListParser::parse($lines);
        $this->assertEqual(1, count($folders),
            "The \\Noselect line still yields a folder entry");
        $this->assertEqual("Archive", $folders[0]["NAME"],
            "The \\Noselect folder name is parsed");
        $this->assertFalse($folders[0]["SELECTABLE"],
            "A \\Noselect folder is not selectable");
    }
    /**
     * A folder whose name arrives as a {N} literal, the form a
     * server uses when the name contains characters awkward to
     * quote. ImapClient::send() stitches the payload inline.
     */
    public function literalFolderNameTestCase()
    {
        $lines = ['* LIST (\\HasNoChildren) "/" {9}Sent Mail'];
        $folders = ImapFolderListParser::parse($lines);
        $this->assertEqual(1, count($folders),
            "The literal-named line yields one folder");
        $this->assertEqual("Sent Mail", $folders[0]["NAME"],
            "A {N} literal folder name is read by byte count");
    }
    /**
     * A flat mailbox with no hierarchy sends NIL for the delimiter.
     * The NIL delimiter token must be consumed so the folder name
     * after it is still read correctly.
     */
    public function nilDelimiterTestCase()
    {
        $lines = ['* LIST (\\HasNoChildren) NIL "Drafts"'];
        $folders = ImapFolderListParser::parse($lines);
        $this->assertEqual(1, count($folders),
            "A NIL-delimiter line still yields a folder");
        $this->assertEqual("Drafts", $folders[0]["NAME"],
            "The folder name after a NIL delimiter is parsed");
    }
    /**
     * A response carrying lines that are not LIST responses, such
     * as the tagged completion line: the non-LIST lines must be
     * skipped and only the real LIST entries returned.
     */
    public function nonListLineSkippedTestCase()
    {
        $lines = [
            '* LIST (\\HasNoChildren) "/" "INBOX"',
            '* STATUS "INBOX" (MESSAGES 42)',
            'A001 OK LIST completed',
        ];
        $folders = ImapFolderListParser::parse($lines);
        $this->assertEqual(1, count($folders),
            "Only the real LIST line becomes a folder");
        $this->assertEqual("INBOX", $folders[0]["NAME"],
            "The surviving folder is the one from the LIST line");
    }
    /**
     * A folder carrying an RFC 6154 special-use attribute must have
     * that attribute surfaced in SPECIAL_USE; a folder with no such
     * attribute must come back with an empty SPECIAL_USE.
     */
    public function specialUseAttributeTestCase()
    {
        $lines = [
            '* LIST (\\HasNoChildren \\Sent) "." "Sent"',
            '* LIST (\\HasNoChildren) "." "Notes"',
        ];
        $folders = ImapFolderListParser::parse($lines);
        $this->assertEqual("\\Sent", $folders[0]["SPECIAL_USE"],
            "The \\Sent special-use attribute is captured");
        $this->assertEqual("", $folders[1]["SPECIAL_USE"],
            "A folder with no special-use attribute reports none");
    }
    /**
     * sortBySpecialUse must order a scrambled folder list as INBOX
     * first, then the special-use folders in their conventional
     * sequence, then the remaining folders alphabetically. This
     * setup mirrors a real server that marks two different
     * mailboxes \Sent; both must land in the Sent slot, ordered
     * alphabetically against each other.
     */
    public function sortBySpecialUseTestCase()
    {
        $lines = [
            '* LIST (\\HasNoChildren \\Archive) "." "Archive"',
            '* LIST (\\HasNoChildren \\Trash) "." "Trash"',
            '* LIST (\\HasNoChildren \\Sent) "." "Sent Messages"',
            '* LIST (\\HasNoChildren) "." "Notes"',
            '* LIST (\\HasNoChildren \\Sent) "." "Sent"',
            '* LIST (\\HasNoChildren \\Drafts) "." "Drafts"',
            '* LIST (\\HasChildren) "." "INBOX"',
        ];
        $sorted = ImapFolderListParser::sortBySpecialUse(
            ImapFolderListParser::parse($lines));
        $order = [];
        foreach ($sorted as $folder) {
            $order[] = $folder["NAME"];
        }
        $expected = ["INBOX", "Drafts", "Sent", "Sent Messages",
            "Archive", "Trash", "Notes"];
        $this->assertEqual($expected, $order,
            "Folders sort INBOX-first, then by special-use role, " .
            "then alphabetically, with same-role folders ordered " .
            "alphabetically among themselves");
    }
}
X