diff --git a/lib/Horde/Core/ActiveSync/Connector.php b/lib/Horde/Core/ActiveSync/Connector.php index 128f02e1..922182b4 100644 --- a/lib/Horde/Core/ActiveSync/Connector.php +++ b/lib/Horde/Core/ActiveSync/Connector.php @@ -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]; } } diff --git a/lib/Horde/Core/ActiveSync/Driver.php b/lib/Horde/Core/ActiveSync/Driver.php index 95d80f34..4335e32c 100644 --- a/lib/Horde/Core/ActiveSync/Driver.php +++ b/lib/Horde/Core/ActiveSync/Driver.php @@ -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; } diff --git a/test/Unit/ActiveSync/DriverResolveRecipientTest.php b/test/Unit/ActiveSync/DriverResolveRecipientTest.php new file mode 100644 index 00000000..1a7f0a65 --- /dev/null +++ b/test/Unit/ActiveSync/DriverResolveRecipientTest.php @@ -0,0 +1,153 @@ + + * @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; + } +}