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
6 changes: 5 additions & 1 deletion lib/Horde/Core/ActiveSync/Connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,11 @@ public function resolveRecipient($query, array $opts = [])
try {
return [$query => $this->_registry->calendar->lookupFreeBusy($query, true)];
} catch (Horde_Exception $e) {
return false; // ?
// Optional for many recipients (esp. external addresses without
// a free/busy URL); avoid flooding normal log levels.
$this->_logger->debug($e->getMessage());
// Keep the keyed array shape so callers can index by $query.
return [$query => false];
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/Horde/Core/ActiveSync/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3362,7 +3362,8 @@ public function resolveRecipient($type, $search, array $opts = [])
if (!empty($entry)) {
$fb = $this->_connector->resolveRecipient($search, $opts);
if ($availability_request) {
$entry['availability'] = self::buildFbString($fb[$search], $opts['starttime'], $opts['endtime']);
$fbData = (is_array($fb) && isset($fb[$search])) ? $fb[$search] : null;
$entry['availability'] = self::buildFbString($fbData, $opts['starttime'], $opts['endtime']);
}
$return[] = $entry;
}
Expand Down
153 changes: 153 additions & 0 deletions test/Unit/ActiveSync/DriverResolveRecipientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @author Torben Dannhauer <torben@dannhauer.de>
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
* @subpackage UnitTests
*/

namespace Horde\Core\Test\Unit\ActiveSync;

use Horde\Core\Test\Support\MockSkipConstructorTrait;
use Horde\Http\ServerRequest;
use Horde_ActiveSync;
use Horde_Core_ActiveSync_Auth;
use Horde_Core_ActiveSync_Connector;
use Horde_Core_ActiveSync_Driver;
use Horde_Date;
use Horde_Registry;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
* Unit tests for Horde_Core_ActiveSync_Driver::resolveRecipient().
*/
#[CoversMethod(Horde_Core_ActiveSync_Driver::class, 'resolveRecipient')]
class DriverResolveRecipientTest extends TestCase
{
use MockSkipConstructorTrait;

public function testAvailabilityIgnoresFalseFreeBusyLookup(): void
{
if (!class_exists('Horde_ActiveSync_State_Sql')) {
$this->markTestSkipped('horde/activesync not available');
}

$email = 'alice@example.com';
$gal = 'gal';

$connector = $this->getMockSkipConstructor(
Horde_Core_ActiveSync_Connector::class,
['resolveRecipient', 'contacts_getGal']
);
$connector->expects($this->exactly(2))
->method('resolveRecipient')
->willReturnOnConsecutiveCalls(
[
$email => [
[
'name' => 'Alice Example',
'email' => $email,
'source' => $gal,
'smimePublicKey' => null,
],
],
],
false
);
$connector->expects($this->once())
->method('contacts_getGal')
->willReturn($gal);

$driver = new Horde_Core_ActiveSync_Driver([
'connector' => $connector,
'auth' => $this->getMockSkipConstructor(Horde_Core_ActiveSync_Auth::class),
'serverrequest' => new ServerRequest('POST', '/'),
'registry' => $this->getMockSkipConstructor(Horde_Registry::class),
'state' => $this->createDriverStateMock(),
'imap' => null,
]);

$results = $driver->resolveRecipient('availability', $email, [
'maxambiguous' => 0,
'starttime' => new Horde_Date('2026-07-15T09:00:00.000Z', 'UTC'),
'endtime' => new Horde_Date('2026-07-15T17:00:00.000Z', 'UTC'),
]);

$this->assertCount(1, $results);
$this->assertSame($email, $results[0]['emailaddress']);
$this->assertSame(Horde_ActiveSync::RESOLVE_RESULT_GAL, $results[0]['type']);
$this->assertFalse($results[0]['availability']);
}

public function testAvailabilityUsesKeyedFalseFreeBusyResult(): void
{
if (!class_exists('Horde_ActiveSync_State_Sql')) {
$this->markTestSkipped('horde/activesync not available');
}

$email = 'alice@example.com';
$gal = 'gal';

$connector = $this->getMockSkipConstructor(
Horde_Core_ActiveSync_Connector::class,
['resolveRecipient', 'contacts_getGal']
);
$connector->expects($this->exactly(2))
->method('resolveRecipient')
->willReturnOnConsecutiveCalls(
[
$email => [
[
'name' => 'Alice Example',
'email' => $email,
'source' => $gal,
'smimePublicKey' => null,
],
],
],
[$email => false]
);
$connector->expects($this->once())
->method('contacts_getGal')
->willReturn($gal);

$driver = new Horde_Core_ActiveSync_Driver([
'connector' => $connector,
'auth' => $this->getMockSkipConstructor(Horde_Core_ActiveSync_Auth::class),
'serverrequest' => new ServerRequest('POST', '/'),
'registry' => $this->getMockSkipConstructor(Horde_Registry::class),
'state' => $this->createDriverStateMock(),
'imap' => null,
]);

$results = $driver->resolveRecipient('availability', $email, [
'maxambiguous' => 0,
'starttime' => new Horde_Date('2026-07-15T09:00:00.000Z', 'UTC'),
'endtime' => new Horde_Date('2026-07-15T17:00:00.000Z', 'UTC'),
]);

$this->assertCount(1, $results);
$this->assertFalse($results[0]['availability']);
}

private function createDriverStateMock(): MockObject
{
$state = $this->getMockSkipConstructor('Horde_ActiveSync_State_Sql');
$state->expects($this->once())->method('setLogger');
$state->expects($this->once())->method('setBackend');

return $state;
}
}
Loading