Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Horde/ActiveSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ public function getSupportedCommands()
case self::VERSION_FOURTEEN:
case self::VERSION_FOURTEENONE:
case self::VERSION_SIXTEEN:
return 'Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,Search,Settings,Ping,ItemOperations,Provision,ResolveRecipients,ValidateCert';
return 'Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,Search,Settings,Ping,ItemOperations,Provision,ResolveRecipients,ValidateCert,Find';
}
}

Expand Down
17 changes: 17 additions & 0 deletions lib/Horde/ActiveSync/Driver/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,23 @@ abstract public function moveMessage($folderid, array $ids, $newfolderid);
*/
abstract public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ActiveSync_Search_Results;

/**
* Returns Find command results for the given parameters.
*
* @author Torben Dannhauer <torben@dannhauer.de>
*
* @param Horde_ActiveSync_Find_Params $params The find parameters.
* @param array $bodyprefs Body preference options.
* @param integer $mimesupport MIME support flag.
*
* @return Horde_ActiveSync_Find_Results
*/
abstract public function getFindResults(
Horde_ActiveSync_Find_Params $params,
array $bodyprefs = [],
$mimesupport = 0
): Horde_ActiveSync_Find_Results;

/**
* Stat folder. Note that since the only thing that can ever change for a
* folder is the name, we use that as the 'mod' value.
Expand Down
20 changes: 19 additions & 1 deletion lib/Horde/ActiveSync/Driver/Mock.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,25 @@ public function moveMessage($folderid, array $ids, $newfolderid)
*/
public function getSearchResults(Horde_ActiveSync_Search_Params $params): Horde_ActiveSync_Search_Results
{
return [];
return new Horde_ActiveSync_Search_Results(0, [], 0);
}

/**
* Returns Find command results for the given parameters.
*
* @author Torben Dannhauer <torben@dannhauer.de>
*/
public function getFindResults(
Horde_ActiveSync_Find_Params $params,
array $bodyprefs = [],
$mimesupport = 0
): Horde_ActiveSync_Find_Results {
return new Horde_ActiveSync_Find_Results(
Horde_ActiveSync_Request_Find::STATUS_SUCCESS,
Horde_ActiveSync_Request_Find::STORE_STATUS_SUCCESS,
0,
[]
);
}

/**
Expand Down
113 changes: 113 additions & 0 deletions lib/Horde/ActiveSync/Find/Kql.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

/**
* Horde_ActiveSync_Find_Kql
*
* Minimal KQL parser for EAS 16.0 Find mailbox searches.
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/

/**
* Parse a subset of Keyword Query Language used by EAS Find requests.
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/
class Horde_ActiveSync_Find_Kql
{
/**
* Parse KQL / free-text into an IMAP search query.
*
* @param string $text Raw query string from the client.
*
* @return Horde_Imap_Client_Search_Query
*/
public static function toImapQuery(string $text): Horde_Imap_Client_Search_Query
{
$text = trim($text);
$query = new Horde_Imap_Client_Search_Query();
$query->charset('UTF-8', false);

if ($text === '') {
return $query;
}

if (preg_match('/\s+OR\s+/i', $text)) {
$parts = preg_split('/\s+OR\s+/i', $text);
$queries = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part !== '') {
$queries[] = self::_parseClause($part);
}
}
if (count($queries) > 1) {
$query->orSearch($queries);

return $query;
}
if (count($queries) === 1) {
return $queries[0];
}
}

return self::_parseClause($text);
}

/**
* Parse a single KQL clause (no OR).
*
* @param string $text One clause from a Find FreeText string.
*
* @return Horde_Imap_Client_Search_Query
*/
protected static function _parseClause(string $text): Horde_Imap_Client_Search_Query
{
$text = trim($text);
$query = new Horde_Imap_Client_Search_Query();
$query->charset('UTF-8', false);

if (preg_match('/^\s*(from|to|cc|bcc|subject)\s*:\s*("([^"]+)"|(\S+))\s*$/i', $text, $m)) {
$value = !empty($m[3]) ? $m[3] : $m[4];
$query->headerText(Horde_String::lower($m[1]), $value, false);

return $query;
}

if (preg_match('/^"([^"]+)"$/', $text, $m)) {
$query->text($m[1], false);

return $query;
}

if (preg_match('/^hasattachment:\s*(yes|true|1)$/i', $text)) {
$query->text('multipart', false);

return $query;
}

if (preg_match('/^received\s*(>=|<=|>|<|:)\s*(\d{4}-\d{2}-\d{2})/i', $text, $m)) {
$date = new Horde_Date($m[2], 'UTC');
$op = $m[1];
if ($op === '>=' || $op === '>') {
$query->dateSearch($date, Horde_Imap_Client_Search_Query::DATE_SINCE);
} elseif ($op === '<=' || $op === '<') {
$query->dateSearch($date, Horde_Imap_Client_Search_Query::DATE_BEFORE);
} else {
$query->dateSearch($date, Horde_Imap_Client_Search_Query::DATE_ON);
}

return $query;
}

$query->text($text, false);

return $query;
}
}
40 changes: 40 additions & 0 deletions lib/Horde/ActiveSync/Find/Params.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* Horde_ActiveSync_Find_Params
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/

/**
* Parameters for an EAS Find command request.
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/
class Horde_ActiveSync_Find_Params
{
/**
* @param string $type One of 'mailbox' or 'gal'.
* @param string|null $searchId Client-provided search session id.
* @param array $query Parsed query (freetext, class, collectionid).
* @param array $options Options (picture, etc.).
* @param int $start Result range start.
* @param int $limit Maximum results to return.
* @param bool $deepTraversal Search subfolders when true.
*/
public function __construct(
public string $type,
public ?string $searchId,
public array $query,
public array $options,
public int $start,
public int $limit,
public bool $deepTraversal,
) {}
}
72 changes: 72 additions & 0 deletions lib/Horde/ActiveSync/Find/QueryMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/**
* Horde_ActiveSync_Find_QueryMapper
*
* Maps EAS Find command parameters to Search command parameters so both
* commands share the same IMAP/GAL backend.
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/
class Horde_ActiveSync_Find_QueryMapper
{
/**
* Convert Find parameters into the structure expected by
* Horde_Core_ActiveSync_Driver::getSearchResults().
*
* @param Horde_ActiveSync_Find_Params $params Parsed Find request.
*
* @return Horde_ActiveSync_Search_Params
*/
public static function toSearchParams(
Horde_ActiveSync_Find_Params $params
): Horde_ActiveSync_Search_Params {
$type = Horde_String::lower($params->type);

if ($type === 'gal') {
$text = $params->query['text']
?? $params->query['freetext']
?? '';

return new Horde_ActiveSync_Search_Params(
type: 'gal',
query: [$text],
options: $params->options,
start: $params->start,
limit: $params->limit,
rebuildResults: false,
deepTraversal: false,
);
}

$criteria = [];
if (!empty($params->query['class'])) {
$criteria['FolderType'] = $params->query['class'];
}
if (!empty($params->query['serverid'])) {
$criteria['serverid'] = $params->query['serverid'];
}
if (!empty($params->query['freetext'])) {
$criteria[Horde_ActiveSync_Request_Search::SEARCH_FREETEXT]
= $params->query['freetext'];
}

return new Horde_ActiveSync_Search_Params(
type: 'mailbox',
query: [
[
'op' => Horde_ActiveSync_Request_Search::SEARCH_AND,
'value' => $criteria,
],
],
options: $params->options,
start: $params->start,
limit: $params->limit,
rebuildResults: false,
deepTraversal: $params->deepTraversal,
);
}
}
34 changes: 34 additions & 0 deletions lib/Horde/ActiveSync/Find/Results.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* Horde_ActiveSync_Find_Results
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/

/**
* Results for an EAS Find command request.
*
* @license http://www.horde.org/licenses/gpl GPLv2
* @copyright 2026 Horde LLC (http://www.horde.org)
* @author Torben Dannhauer <torben@dannhauer.de>
* @package ActiveSync
*/
class Horde_ActiveSync_Find_Results
{
/**
* @param int $status Top-level Find status.
* @param int $storeStatus Store-level status.
* @param int $total Total matching entries.
* @param array|null $rows Result rows or null on error.
*/
public function __construct(
public int $status,
public int $storeStatus,
public int $total,
public ?array $rows,
) {}
}
Loading
Loading