<?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\ImapResponseParser;
use seekquarry\yioop\library\UnitTest;
/**
* Unit tests for ImapResponseParser, covering both the CAPABILITY
* tokenizer and the SORT sequence-number parser. The cases here
* are the shapes the Mail activity depends on: SORT being absent
* from a server's advertised capabilities so column-sort headers
* stay non-clickable, a SORT response with a handful of sequence
* numbers in the order the server chose, and a few empty-or-odd
* shapes where the parsers must degrade quietly rather than blow
* up.
*
* @author Chris Pollett
*/
class ImapResponseParserTest extends UnitTest
{
/**
* No setup state is needed; both parsers are pure and static.
*/
public function setUp()
{
}
/**
* No setup state is needed; nothing to tear down.
*/
public function tearDown()
{
}
/**
* Gmail-shaped CAPABILITY line that advertises SORT among many
* other tokens. The parser must find SORT regardless of where
* in the token list it appears.
*/
public function capabilityFoundTestCase()
{
$untagged = ['* CAPABILITY IMAP4rev1 UNSELECT IDLE ' .
'NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 ' .
'UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ' .
'ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS ' .
'LITERAL- SPECIAL-USE APPENDLIMIT=35651584 SORT'];
$this->assertTrue(ImapResponseParser::hasCapability(
$untagged, 'SORT'),
"SORT detected at end of long capability list");
}
/**
* A CAPABILITY line that omits SORT must return false. This
* is the path the Mail activity falls back through on smaller
* servers, leaving Subject and From headers non-clickable.
*/
public function capabilityMissingTestCase()
{
$untagged = ['* CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED'];
$this->assertFalse(ImapResponseParser::hasCapability(
$untagged, 'SORT'),
"SORT absent from advertised capabilities");
}
/**
* Capability tokens are case-insensitive per RFC 3501; both
* the line and the lookup must be compared without regard to
* case. The test uses an unusual mixed casing to be sure.
*/
public function capabilityCaseInsensitiveTestCase()
{
$untagged = ['* CAPABILITY IMAP4rev1 sort'];
$this->assertTrue(ImapResponseParser::hasCapability(
$untagged, 'Sort'),
"Lowercase sort token matches mixed-case lookup");
}
/**
* When the response contains no CAPABILITY line at all, the
* parser returns false rather than treating other untagged
* content as if it were a capability list.
*/
public function capabilityNoCapabilityLineTestCase()
{
$untagged = ['* 42 EXISTS', '* 0 RECENT'];
$this->assertFalse(ImapResponseParser::hasCapability(
$untagged, 'SORT'),
"No CAPABILITY line means the capability is absent");
}
/**
* A standard SORT response with a handful of sequence numbers
* in non-numeric order: the parser must preserve the order the
* server gave so the caller can paginate the list as-is.
*/
public function sortBasicTestCase()
{
$untagged = ['* SORT 12 4 89 7'];
$this->assertEqual([12, 4, 89, 7],
ImapResponseParser::parseSort($untagged),
"Sequence numbers preserved in server-given order");
}
/**
* The server may match no messages and return a SORT line
* with no numbers. The parser yields an empty array rather
* than failing.
*/
public function sortEmptyResultTestCase()
{
$untagged = ['* SORT'];
$this->assertEqual([],
ImapResponseParser::parseSort($untagged),
"Empty SORT line parses to an empty list");
}
/**
* SORT may be followed by extra whitespace between the keyword
* and the first sequence number, or by trailing whitespace.
* The parser must split flexibly and drop empties.
*/
public function sortWhitespaceVariantsTestCase()
{
$untagged = ['* SORT 1 2 3 '];
$this->assertEqual([1, 2, 3],
ImapResponseParser::parseSort($untagged),
"Extra whitespace between tokens is tolerated");
}
/**
* When the response contains untagged noise lines before the
* SORT line, the parser must skip them and read the SORT line
* out of the middle of the list.
*/
public function sortFindsLineAmongOthersTestCase()
{
$untagged = ['* OK something', '* SORT 5 9 1',
'* BYE done'];
$this->assertEqual([5, 9, 1],
ImapResponseParser::parseSort($untagged),
"SORT line found regardless of position");
}
/**
* A response with no SORT line at all comes back as an empty
* list, matching the empty-result shape.
*/
public function sortNoSortLineTestCase()
{
$untagged = ['* 42 EXISTS', '* 0 RECENT'];
$this->assertEqual([],
ImapResponseParser::parseSort($untagged),
"Missing SORT line yields the empty list");
}
/**
* A standard SEARCH response with a handful of matching
* sequence numbers: the parser preserves the order the server
* gave (typically ascending, but the spec does not require it).
*/
public function searchBasicTestCase()
{
$untagged = ['* SEARCH 1 5 12 33'];
$this->assertEqual([1, 5, 12, 33],
ImapResponseParser::parseSearch($untagged),
"Matching sequence numbers preserved in given order");
}
/**
* A SEARCH that finds nothing emits an empty SEARCH line; the
* parser yields an empty array rather than failing.
*/
public function searchEmptyResultTestCase()
{
$untagged = ['* SEARCH'];
$this->assertEqual([],
ImapResponseParser::parseSearch($untagged),
"Empty SEARCH line parses to an empty list");
}
/**
* Untagged noise lines before the SEARCH line are skipped;
* the parser finds the SEARCH line wherever it sits.
*/
public function searchFindsLineAmongOthersTestCase()
{
$untagged = ['* OK something', '* SEARCH 7 11',
'* BYE done'];
$this->assertEqual([7, 11],
ImapResponseParser::parseSearch($untagged),
"SEARCH line found regardless of position");
}
/**
* A response with no SEARCH line at all yields the empty
* list rather than picking up data from unrelated untagged
* lines.
*/
public function searchNoSearchLineTestCase()
{
$untagged = ['* 42 EXISTS', '* 0 RECENT'];
$this->assertEqual([],
ImapResponseParser::parseSearch($untagged),
"Missing SEARCH line yields the empty list");
}
}