<?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\library\mail;
/**
* Small collection of static parsers for the untagged responses
* the Mail activity reads beyond the per-message FETCH that
* ImapEnvelopeParser handles. Two responses live here today:
*
* - CAPABILITY (RFC 3501 section 7.2.1), used to detect whether
* the server advertises the SORT extension before the inbox
* view offers Subject and From column sorts.
*
* - SORT (RFC 5256 section 3), the sorted sequence-number list
* the server returns for a SORT command. The Mail activity
* caches this list to paginate sorted views without re-issuing
* SORT on every infinite-scroll batch.
*
* Both methods are static and pure: they take the raw untagged
* lines an ImapClient::send() response carries and return their
* parse result, so the same parsers can be exercised by unit tests
* without an IMAP connection.
*
* @author Chris Pollett
*/
class ImapResponseParser
{
/**
* Reports whether the named capability token appears in any
* "* CAPABILITY ..." line of an untagged response. Capability
* tokens are space-separated after the CAPABILITY keyword and
* are matched case-insensitively as RFC 3501 specifies.
*
* @param array $untagged untagged lines from an ImapClient
* response
* @param string $capability the capability token to look for,
* e.g. "SORT"
* @return bool true if the capability is advertised
*/
public static function hasCapability($untagged, $capability)
{
$needle = strtoupper($capability);
foreach ($untagged as $line) {
if (!preg_match('/^\* CAPABILITY (.*)$/i', $line, $m)) {
continue;
}
foreach (preg_split('/\s+/', trim($m[1])) as $token) {
if (strcasecmp($token, $needle) === 0) {
return true;
}
}
}
return false;
}
/**
* Parses a SORT response into the ordered list of sequence
* numbers. The line is shaped "* SORT 12 4 89 7" with the
* numbers listed in the requested sort order; an empty server-
* side result yields an empty array. Only the first SORT line
* is read.
*
* @param array $untagged untagged lines from an ImapClient
* response to a SORT command
* @return array sequence numbers in sort order, as ints
*/
public static function parseSort($untagged)
{
return self::parseNumberList($untagged, 'SORT');
}
/**
* Parses a SEARCH response into the list of matching sequence
* numbers. The line is shaped "* SEARCH 12 4 89 7"; the server
* returns matches in whatever order it pleases (commonly
* ascending sequence order, but the spec does not require it).
* An empty server-side result yields an empty array. Only the
* first SEARCH line is read.
*
* @param array $untagged untagged lines from an ImapClient
* response to a SEARCH command
* @return array matching sequence numbers, as ints
*/
public static function parseSearch($untagged)
{
return self::parseNumberList($untagged, 'SEARCH');
}
/**
* Internal helper that pulls the trailing sequence-number list
* out of a "* KEYWORD n n n..." untagged line. SORT and SEARCH
* share this shape; the difference is just the keyword. Tokens
* that are not digit strings are skipped silently so any extra
* server data after the number list (no known IMAP server adds
* any, but defensive) does not corrupt the result.
*
* @param array $untagged untagged lines from an ImapClient
* response
* @param string $keyword the untagged keyword to look for,
* e.g. "SORT" or "SEARCH"
* @return array sequence numbers, as ints
*/
private static function parseNumberList($untagged, $keyword)
{
$pattern = '/^\* ' . preg_quote($keyword, '/') .
'\b(.*)$/i';
foreach ($untagged as $line) {
if (!preg_match($pattern, $line, $m)) {
continue;
}
$list = [];
foreach (preg_split('/\s+/', trim($m[1])) as $token) {
if ($token !== "" && ctype_digit($token)) {
$list[] = (int) $token;
}
}
return $list;
}
return [];
}
}