From f233f0df3a2ffae193cabf44f9e0f281e09bf67c Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sun, 12 Apr 2026 14:17:47 +0200 Subject: [PATCH 1/4] feat: Add connection tests --- phpunit.xml.dist | 4 + .../Connection/ImapConnectionTest.php | 143 ++++++++++ .../Connection/Pop3ConnectionTest.php | 123 +++++++++ test/Stub/Base.php | 246 ++++++++++++++++++ test/Unit/Base/AlertsTest.php | 115 ++++++++ test/Unit/Base/BaseConstructorTest.php | 136 ++++++++++ test/Unit/Base/BaseInitCacheTest.php | 82 ++++++ test/Unit/Base/BaseObserverTest.php | 77 ++++++ test/Unit/Base/BaseParamsTest.php | 64 +++++ test/Unit/Base/BasePropertyTest.php | 87 +++++++ test/Unit/Base/BaseSerializationTest.php | 73 ++++++ test/Unit/Base/BaseSetInitTest.php | 127 +++++++++ test/Unit/Base/DebugTest.php | 128 +++++++++ test/Unit/Base/DeprecatedTest.php | 138 ++++++++++ test/Unit/Socket/CatenateTest.php | 165 ++++++++++++ 15 files changed, 1708 insertions(+) create mode 100644 test/Integration/Connection/ImapConnectionTest.php create mode 100644 test/Integration/Connection/Pop3ConnectionTest.php create mode 100644 test/Stub/Base.php create mode 100644 test/Unit/Base/AlertsTest.php create mode 100644 test/Unit/Base/BaseConstructorTest.php create mode 100644 test/Unit/Base/BaseInitCacheTest.php create mode 100644 test/Unit/Base/BaseObserverTest.php create mode 100644 test/Unit/Base/BaseParamsTest.php create mode 100644 test/Unit/Base/BasePropertyTest.php create mode 100644 test/Unit/Base/BaseSerializationTest.php create mode 100644 test/Unit/Base/BaseSetInitTest.php create mode 100644 test/Unit/Base/DebugTest.php create mode 100644 test/Unit/Base/DeprecatedTest.php create mode 100644 test/Unit/Socket/CatenateTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a3e582fb..aeaa9076 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -20,6 +20,10 @@ test/Integration + test/Integration/Connection + + + test/Integration/Connection diff --git a/test/Integration/Connection/ImapConnectionTest.php b/test/Integration/Connection/ImapConnectionTest.php new file mode 100644 index 00000000..c354f155 --- /dev/null +++ b/test/Integration/Connection/ImapConnectionTest.php @@ -0,0 +1,143 @@ +logout(); + } catch (Horde_Imap_Client_Exception $e) { + } + } + self::$live = null; + self::$clientConfig = null; + } + + public function testConnectReturnsCapability(): void + { + $this->assertInstanceOf( + Horde_Imap_Client_Data_Capability::class, + self::$live->capability + ); + } + + #[Depends('testConnectReturnsCapability')] + public function testLogin(): void + { + self::$live->login(); + $this->assertTrue(true); + } + + #[Depends('testLogin')] + public function testCapabilityAfterLoginIncludesImap4Rev1(): void + { + $this->assertTrue( + self::$live->capability->query('IMAP4REV1') + ); + } + + #[Depends('testLogin')] + public function testNoop(): void + { + self::$live->noop(); + $this->assertTrue(true); + } + + #[Depends('testLogin')] + public function testUrlPropertyMatchesConfig(): void + { + $url = self::$live->url; + $this->assertEquals( + self::$clientConfig['hostspec'], + $url->hostspec + ); + } + + #[Depends('testLogin')] + public function testLogout(): void + { + self::$live->logout(); + $this->assertTrue(true); + } + + #[Depends('testLogout')] + public function testReconnectAfterLogout(): void + { + self::$live->login(); + $this->assertTrue(true); + } + + public function testInvalidCredentialsFails(): void + { + if (empty(self::$clientConfig)) { + $this->markTestSkipped('No IMAP configuration available.'); + } + + $config = self::$clientConfig; + $config['password'] = 'intentionally-wrong-password-' . mt_rand(); + + $bad = new Horde_Imap_Client_Socket($config); + + $this->expectException(Horde_Imap_Client_Exception::class); + $bad->login(); + } +} diff --git a/test/Integration/Connection/Pop3ConnectionTest.php b/test/Integration/Connection/Pop3ConnectionTest.php new file mode 100644 index 00000000..8404048d --- /dev/null +++ b/test/Integration/Connection/Pop3ConnectionTest.php @@ -0,0 +1,123 @@ +logout(); + } catch (Horde_Imap_Client_Exception $e) { + } + } + self::$live = null; + self::$clientConfig = null; + } + + public function testConnect(): void + { + $this->assertNotNull(self::$live->capability); + } + + #[Depends('testConnect')] + public function testLogin(): void + { + self::$live->login(); + $this->assertTrue(true); + } + + #[Depends('testLogin')] + public function testLogout(): void + { + self::$live->logout(); + $this->assertTrue(true); + } + + public function testInvalidCredentialsFails(): void + { + if (empty(self::$clientConfig)) { + $this->markTestSkipped('No POP3 configuration available.'); + } + + $config = self::$clientConfig; + $config['password'] = 'intentionally-wrong-password-' . mt_rand(); + + $bad = new Horde_Imap_Client_Socket_Pop3($config); + + $this->expectException(Horde_Imap_Client_Exception::class); + $bad->login(); + } + + #[Depends('testLogin')] + public function testStatusAfterLogin(): void + { + // Re-login after logout test may have run + self::$live = new Horde_Imap_Client_Socket_Pop3(self::$clientConfig); + self::$live->login(); + + $status = self::$live->status( + 'INBOX', + Horde_Imap_Client::STATUS_MESSAGES + ); + + $this->assertArrayHasKey('messages', $status); + } +} diff --git a/test/Stub/Base.php b/test/Stub/Base.php new file mode 100644 index 00000000..d8a4dae0 --- /dev/null +++ b/test/Stub/Base.php @@ -0,0 +1,246 @@ +_setInit($key, $val); + } + + public function initCache($current = false): bool + { + return $this->_initCache($current); + } + + protected function _initCapability() + { + } + + protected function _noop() + { + } + + protected function _getNamespaces() + { + return []; + } + + protected function _connect() + { + } + + protected function _login() + { + return true; + } + + protected function _logout() + { + } + + protected function _sendID($info) + { + } + + protected function _getID() + { + return []; + } + + protected function _setLanguage($langs) + { + return []; + } + + protected function _getLanguage($list) + { + return []; + } + + protected function _openMailbox( + Horde_Imap_Client_Mailbox $mailbox, + $mode + ) { + } + + protected function _createMailbox( + Horde_Imap_Client_Mailbox $mailbox, + $opts + ) { + } + + protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox) + { + } + + protected function _renameMailbox( + Horde_Imap_Client_Mailbox $old, + Horde_Imap_Client_Mailbox $new + ) { + } + + protected function _subscribeMailbox( + Horde_Imap_Client_Mailbox $mailbox, + $subscribe + ) { + } + + protected function _listMailboxes($pattern, $mode, $options) + { + return []; + } + + protected function _status($mboxes, $flags) + { + return []; + } + + protected function _append( + Horde_Imap_Client_Mailbox $mailbox, + $data, + $options + ) { + return new Horde_Imap_Client_Ids(); + } + + protected function _check() + { + } + + protected function _close($options) + { + } + + protected function _expunge($options) + { + } + + protected function _search($query, $options) + { + return []; + } + + protected function _setComparator($comparator) + { + } + + protected function _getComparator() + { + return []; + } + + protected function _thread($options) + { + } + + protected function _fetch( + Horde_Imap_Client_Fetch_Results $results, + $queries + ) { + } + + protected function _vanished( + $modseq, + Horde_Imap_Client_Ids $ids + ) { + } + + protected function _store($options) + { + return []; + } + + protected function _copy( + Horde_Imap_Client_Mailbox $dest, + $options + ) { + return new Horde_Imap_Client_Ids(); + } + + protected function _setQuota( + Horde_Imap_Client_Mailbox $root, + $resources + ) { + } + + protected function _getQuota(Horde_Imap_Client_Mailbox $root) + { + return []; + } + + protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox) + { + return []; + } + + protected function _getACL(Horde_Imap_Client_Mailbox $mailbox) + { + return []; + } + + protected function _setACL( + Horde_Imap_Client_Mailbox $mailbox, + $identifier, + $options + ) { + } + + protected function _deleteACL( + Horde_Imap_Client_Mailbox $mailbox, + $identifier + ) { + } + + protected function _listACLRights( + Horde_Imap_Client_Mailbox $mailbox, + $identifier + ) { + } + + protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox) + { + } + + protected function _getMetadata( + Horde_Imap_Client_Mailbox $mailbox, + $entries, + $options + ) { + return []; + } + + protected function _setMetadata( + Horde_Imap_Client_Mailbox $mailbox, + $data + ) { + } +} diff --git a/test/Unit/Base/AlertsTest.php b/test/Unit/Base/AlertsTest.php new file mode 100644 index 00000000..bfb5eea6 --- /dev/null +++ b/test/Unit/Base/AlertsTest.php @@ -0,0 +1,115 @@ +alerts = new Horde_Imap_Client_Base_Alerts(); + } + + public function testGetLastReturnsNullInitially(): void + { + $this->assertNull($this->alerts->getLast()); + } + + public function testAddSetsAlertData(): void + { + $this->alerts->add('Test alert'); + + $last = $this->alerts->getLast(); + $this->assertEquals('Test alert', $last->alert); + $this->assertFalse(isset($last->type)); + } + + public function testAddWithType(): void + { + $this->alerts->add('Alert msg', 'WARNING'); + + $last = $this->alerts->getLast(); + $this->assertEquals('Alert msg', $last->alert); + $this->assertEquals('WARNING', $last->type); + } + + public function testAddOverwritesPrevious(): void + { + $this->alerts->add('First'); + $this->alerts->add('Second'); + + $this->assertEquals('Second', $this->alerts->getLast()->alert); + } + + public function testAttachAndNotify(): void + { + $observer = $this->createMock(SplObserver::class); + $observer->expects($this->once()) + ->method('update') + ->with($this->alerts); + + $this->alerts->attach($observer); + $this->alerts->add('Test'); + } + + public function testDetach(): void + { + $observer = $this->createMock(SplObserver::class); + $observer->expects($this->never()) + ->method('update'); + + $this->alerts->attach($observer); + $this->alerts->detach($observer); + $this->alerts->add('Test'); + } + + public function testAttachDeduplicates(): void + { + $observer = $this->createMock(SplObserver::class); + $observer->expects($this->once()) + ->method('update'); + + $this->alerts->attach($observer); + $this->alerts->attach($observer); + $this->alerts->add('Test'); + } + + public function testMultipleObservers(): void + { + $obs1 = $this->createMock(SplObserver::class); + $obs1->expects($this->once())->method('update'); + + $obs2 = $this->createMock(SplObserver::class); + $obs2->expects($this->once())->method('update'); + + $this->alerts->attach($obs1); + $this->alerts->attach($obs2); + $this->alerts->add('Test'); + } +} diff --git a/test/Unit/Base/BaseConstructorTest.php b/test/Unit/Base/BaseConstructorTest.php new file mode 100644 index 00000000..0075df71 --- /dev/null +++ b/test/Unit/Base/BaseConstructorTest.php @@ -0,0 +1,136 @@ + 'user', + 'password' => 'pass', + ], $params)); + } + + public function testRequiresUsername(): void + { + $this->expectException(InvalidArgumentException::class); + new Base([]); + } + + public function testDefaultHostspec(): void + { + $ob = $this->create(); + $this->assertEquals('localhost', $ob->getParam('hostspec')); + } + + public function testDefaultTimeout(): void + { + $ob = $this->create(); + $this->assertEquals(30, $ob->getParam('timeout')); + } + + public function testDefaultReadTimeout(): void + { + $ob = $this->create(); + $this->assertEquals(120, $ob->getParam('read_timeout')); + } + + public function testDefaultSecure(): void + { + $ob = $this->create(); + $this->assertFalse($ob->getParam('secure')); + } + + public function testPortDefaultsTo143WhenNoSsl(): void + { + $ob = $this->create(['secure' => false]); + $this->assertEquals(143, $ob->getParam('port')); + } + + public function testPortDefaultsTo993WithSsl(): void + { + $ob = $this->create(['secure' => 'ssl']); + $this->assertEquals(993, $ob->getParam('port')); + } + + public function testPortDefaultsTo993WithSslv2(): void + { + $ob = $this->create(['secure' => 'sslv2']); + $this->assertEquals(993, $ob->getParam('port')); + } + + public function testPortDefaultsTo993WithSslv3(): void + { + $ob = $this->create(['secure' => 'sslv3']); + $this->assertEquals(993, $ob->getParam('port')); + } + + public function testPortDefaultsTo143WithTls(): void + { + $ob = $this->create(['secure' => 'tls']); + $this->assertEquals(143, $ob->getParam('port')); + } + + public function testPortDefaultsTo143WithTrue(): void + { + $ob = $this->create(['secure' => true]); + $this->assertEquals(143, $ob->getParam('port')); + } + + public function testExplicitPortOverridesDefault(): void + { + $ob = $this->create(['port' => 999]); + $this->assertEquals(999, $ob->getParam('port')); + } + + public function testCacheFieldsEmptyWhenNoCacheSet(): void + { + $ob = $this->create(); + $cache = $ob->getParam('cache'); + $this->assertEmpty($cache['fields']); + } + + public function testCacheFieldsDefaultWhenBackendProvided(): void + { + $backend = $this->createMock(\Horde_Imap_Client_Cache_Backend::class); + $ob = $this->create([ + 'cache' => ['backend' => $backend], + ]); + + $cache = $ob->getParam('cache'); + $this->assertNotEmpty($cache['fields']); + $this->assertArrayHasKey(Horde_Imap_Client::FETCH_ENVELOPE, $cache['fields']); + } + + public function testChangedFlagSetAfterConstruction(): void + { + $ob = $this->create(); + $this->assertTrue($ob->changed); + } +} diff --git a/test/Unit/Base/BaseInitCacheTest.php b/test/Unit/Base/BaseInitCacheTest.php new file mode 100644 index 00000000..b1d811bc --- /dev/null +++ b/test/Unit/Base/BaseInitCacheTest.php @@ -0,0 +1,82 @@ + 'user', + 'password' => 'pass', + ]); + + $this->assertFalse($ob->initCache()); + } + + public function testReturnsFalseWhenNoBackend(): void + { + $ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + 'cache' => ['fields' => [Horde_Imap_Client::FETCH_ENVELOPE]], + ]); + + $this->assertFalse($ob->initCache()); + } + + public function testReturnsTrueWhenBackendProvided(): void + { + $backend = $this->createMock(Horde_Imap_Client_Cache_Backend::class); + $ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + 'cache' => ['backend' => $backend], + ]); + + $this->assertTrue($ob->initCache()); + } + + public function testCreatesCacheObject(): void + { + $backend = $this->createMock(Horde_Imap_Client_Cache_Backend::class); + $ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + 'cache' => ['backend' => $backend], + ]); + + $ob->initCache(); + + $this->assertInstanceOf( + Horde_Imap_Client_Cache::class, + $ob->getCache() + ); + } +} diff --git a/test/Unit/Base/BaseObserverTest.php b/test/Unit/Base/BaseObserverTest.php new file mode 100644 index 00000000..8a68ceb4 --- /dev/null +++ b/test/Unit/Base/BaseObserverTest.php @@ -0,0 +1,77 @@ +ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + ]); + } + + public function testUpdateSetsChangedOnCapabilitySubject(): void + { + $cap = new Horde_Imap_Client_Data_Capability(); + $this->ob->changed = false; + + $this->ob->update($cap); + + $this->assertTrue($this->ob->changed); + } + + public function testUpdateSetsChangedOnSearchCharsetSubject(): void + { + $sc = new Horde_Imap_Client_Data_SearchCharset(); + $this->ob->changed = false; + + $this->ob->update($sc); + + $this->assertTrue($this->ob->changed); + } + + public function testUpdateCollectsAlertFromAlertsSubject(): void + { + $this->ob->alerts_ob->add('Test Alert'); + + $alerts = $this->ob->alerts(); + $this->assertContains('Test Alert', $alerts); + } + + public function testAlertsAreClearedAfterRetrieval(): void + { + $this->ob->alerts_ob->add('Alert'); + $this->ob->alerts(); + + $this->assertEmpty($this->ob->alerts()); + } +} diff --git a/test/Unit/Base/BaseParamsTest.php b/test/Unit/Base/BaseParamsTest.php new file mode 100644 index 00000000..37ebe25c --- /dev/null +++ b/test/Unit/Base/BaseParamsTest.php @@ -0,0 +1,64 @@ +ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + ]); + } + + public function testGetParamReturnsNullForMissing(): void + { + $this->assertNull($this->ob->getParam('nonexistent')); + } + + public function testSetAndGetParam(): void + { + $this->ob->setParam('foo', 'bar'); + $this->assertEquals('bar', $this->ob->getParam('foo')); + } + + public function testPasswordObjectSupport(): void + { + $pwObj = $this->createMock(Horde_Imap_Client_Base_Password::class); + $pwObj->method('getPassword')->willReturn('secret123'); + + $ob = new Base([ + 'username' => 'user', + 'password' => $pwObj, + ]); + + $this->assertEquals('secret123', $ob->getParam('password')); + } +} diff --git a/test/Unit/Base/BasePropertyTest.php b/test/Unit/Base/BasePropertyTest.php new file mode 100644 index 00000000..63959ac1 --- /dev/null +++ b/test/Unit/Base/BasePropertyTest.php @@ -0,0 +1,87 @@ +ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + ]); + } + + public function testAlertsObProperty(): void + { + $this->assertInstanceOf( + Horde_Imap_Client_Base_Alerts::class, + $this->ob->alerts_ob + ); + } + + public function testSearchCharsetCreatedOnDemand(): void + { + $this->assertInstanceOf( + Horde_Imap_Client_Data_SearchCharset::class, + $this->ob->search_charset + ); + } + + public function testSearchCharsetReturnsSameInstance(): void + { + $first = $this->ob->search_charset; + $second = $this->ob->search_charset; + $this->assertSame($first, $second); + } + + public function testUrlProperty(): void + { + $url = $this->ob->url; + $this->assertEquals('localhost', $url->hostspec); + $this->assertEquals(143, $url->port); + } + + public function testUrlReflectsCustomParams(): void + { + $ob = new Base([ + 'username' => 'user', + 'password' => 'pass', + 'hostspec' => 'mail.example.com', + 'port' => 993, + 'secure' => 'ssl', + ]); + + $url = $ob->url; + $this->assertEquals('mail.example.com', $url->hostspec); + $this->assertEquals(993, $url->port); + } +} diff --git a/test/Unit/Base/BaseSerializationTest.php b/test/Unit/Base/BaseSerializationTest.php new file mode 100644 index 00000000..11b07696 --- /dev/null +++ b/test/Unit/Base/BaseSerializationTest.php @@ -0,0 +1,73 @@ + 'user', + 'password' => 'pass', + ], $params)); + } + + public function testSerializeContainsVersionAndParams(): void + { + $ob = $this->create(); + $data = $ob->__serialize(); + + $this->assertArrayHasKey('v', $data); + $this->assertArrayHasKey('p', $data); + $this->assertArrayHasKey('i', $data); + $this->assertEquals(Horde_Imap_Client_Base::VERSION, $data['v']); + } + + public function testUnserializeThrowsOnVersionMismatch(): void + { + $ob = $this->create(); + + $this->expectException(Exception::class); + $ob->__unserialize(['v' => 0, 'i' => [], 'p' => ['username' => 'u']]); + } + + public function testRoundTripSerialization(): void + { + $ob = $this->create([ + 'hostspec' => 'mail.example.com', + 'port' => 993, + ]); + + $serialized = serialize($ob); + $restored = unserialize($serialized); + + $this->assertEquals('mail.example.com', $restored->getParam('hostspec')); + $this->assertEquals(993, $restored->getParam('port')); + $this->assertEquals('user', $restored->getParam('username')); + } +} diff --git a/test/Unit/Base/BaseSetInitTest.php b/test/Unit/Base/BaseSetInitTest.php new file mode 100644 index 00000000..9c8a18c9 --- /dev/null +++ b/test/Unit/Base/BaseSetInitTest.php @@ -0,0 +1,127 @@ + 'user', + 'password' => 'pass', + ], $params)); + } + + public function testSetsChangedFlag(): void + { + $ob = $this->create(); + $ob->changed = false; + + $ob->setInit('testkey', 'testval'); + + $this->assertTrue($ob->changed); + } + + public function testNullKeyResetsAll(): void + { + $ob = $this->create(); + $ob->setInit('key1', 'val1'); + $ob->setInit('key2', 'val2'); + + $ob->setInit(null); + + $data = $ob->__serialize(); + $this->assertEmpty($data['i']); + } + + public function testNullValueRemovesKey(): void + { + $ob = $this->create(); + $ob->setInit('key', 'val'); + $ob->setInit('key', null); + + $data = $ob->__serialize(); + $this->assertArrayNotHasKey('key', $data['i']); + } + + public function testCapabilityFilterRemovesIgnored(): void + { + $ob = $this->create([ + 'capability_ignore' => ['IDLE'], + ]); + + $cap = new Horde_Imap_Client_Data_Capability(); + $cap->add('IDLE'); + $cap->add('SORT'); + + $ob->setInit('capability', $cap); + + $this->assertFalse($ob->capability->query('IDLE')); + $this->assertTrue($ob->capability->query('SORT')); + } + + public function testCapabilityFilterWithParam(): void + { + $ob = $this->create([ + 'capability_ignore' => ['AUTH=PLAIN'], + ]); + + $cap = new Horde_Imap_Client_Data_Capability(); + $cap->add('AUTH', 'PLAIN'); + $cap->add('AUTH', 'LOGIN'); + + $ob->setInit('capability', $cap); + + $this->assertFalse($ob->capability->query('AUTH', 'PLAIN')); + $this->assertTrue($ob->capability->query('AUTH', 'LOGIN')); + } + + public function testCapabilityAttachesObserver(): void + { + $ob = $this->create(); + $cap = new Horde_Imap_Client_Data_Capability(); + + $ob->setInit('capability', $cap); + $ob->changed = false; + + // Modifying the capability object should trigger the observer + $cap->add('NEWCAP'); + + $this->assertTrue($ob->changed); + } + + public function testNoChangeIfValueIdentical(): void + { + $ob = $this->create(); + $ob->setInit('key', 'val'); + $ob->changed = false; + + $ob->setInit('key', 'val'); + + $this->assertFalse($ob->changed); + } +} diff --git a/test/Unit/Base/DebugTest.php b/test/Unit/Base/DebugTest.php new file mode 100644 index 00000000..2b4218e5 --- /dev/null +++ b/test/Unit/Base/DebugTest.php @@ -0,0 +1,128 @@ +createDebug(); + $debug->client('hello'); + + $output = $this->readStream($stream); + $this->assertStringContainsString('C: hello', $output); + } + + public function testServerOutput(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->server('response'); + + $output = $this->readStream($stream); + $this->assertStringContainsString('S: response', $output); + } + + public function testInfoOutput(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->info('message'); + + $output = $this->readStream($stream); + $this->assertStringContainsString('>> message', $output); + } + + public function testRawOutput(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->raw('rawdata'); + + $output = $this->readStream($stream); + $this->assertEquals('rawdata', $output); + } + + public function testFirstWriteOutputsDateHeader(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->client('first'); + + $output = $this->readStream($stream); + $this->assertStringContainsString('-----', $output); + $this->assertStringContainsString('>> ', $output); + $this->assertStringContainsString('C: first', $output); + } + + public function testSlowCommandDetection(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->client('first'); + + $ref = new ReflectionProperty($debug, '_time'); + $ref->setAccessible(true); + $ref->setValue($debug, microtime(true) - 6); + + $debug->client('second'); + + $output = $this->readStream($stream); + $this->assertStringContainsString('Slow Command:', $output); + } + + public function testDebugDisabledSuppressesOutput(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->debug = false; + + $debug->client('nothing'); + $debug->server('nothing'); + $debug->info('nothing'); + + $this->assertEquals('', $this->readStream($stream)); + } + + public function testShutdownClosesStream(): void + { + [$debug, $stream] = $this->createDebug(); + $debug->shutdown(); + + // After shutdown, writing should produce no output + $debug->client('after shutdown'); + // Stream is closed, so we can't read it — just verify no error + $this->assertFalse(is_resource($stream)); + } +} diff --git a/test/Unit/Base/DeprecatedTest.php b/test/Unit/Base/DeprecatedTest.php new file mode 100644 index 00000000..2c7934e2 --- /dev/null +++ b/test/Unit/Base/DeprecatedTest.php @@ -0,0 +1,138 @@ +assertEquals(12345, $result['uidvalidity']); + $this->assertEquals(67890, $result['highestmodseq']); + $this->assertArrayNotHasKey('messages', $result); + $this->assertArrayNotHasKey('uidnext', $result); + } + + public function testParseCacheIdWithoutModseq(): void + { + $result = Horde_Imap_Client_Base_Deprecated::parseCacheId('V12345|U100|M50'); + + $this->assertEquals(12345, $result['uidvalidity']); + $this->assertEquals(100, $result['uidnext']); + $this->assertEquals(50, $result['messages']); + $this->assertArrayNotHasKey('highestmodseq', $result); + } + + public function testParseCacheIdIgnoresUnknownParts(): void + { + $result = Horde_Imap_Client_Base_Deprecated::parseCacheId('V1|X999|custom'); + + $this->assertEquals(['uidvalidity' => 1], $result); + } + + public function testGetCacheIdWithCondstore(): void + { + $base = $this->createMock(Horde_Imap_Client_Base::class); + $base->method('status') + ->willReturn([ + 'uidvalidity' => 1, + 'highestmodseq' => 99, + 'messages' => 5, + 'uidnext' => 10, + ]); + + $result = Horde_Imap_Client_Base_Deprecated::getCacheId( + $base, + 'INBOX', + true + ); + + $this->assertEquals('V1|H99', $result); + } + + public function testGetCacheIdWithoutCondstore(): void + { + $base = $this->createMock(Horde_Imap_Client_Base::class); + $base->method('status') + ->willReturn([ + 'uidvalidity' => 1, + 'highestmodseq' => 0, + 'messages' => 5, + 'uidnext' => 10, + ]); + + $result = Horde_Imap_Client_Base_Deprecated::getCacheId( + $base, + 'INBOX', + false + ); + + $this->assertEquals('V1|U10|M5', $result); + } + + public function testGetCacheIdWithAdditionalData(): void + { + $base = $this->createMock(Horde_Imap_Client_Base::class); + $base->method('status') + ->willReturn([ + 'uidvalidity' => 1, + 'highestmodseq' => 99, + 'messages' => 5, + 'uidnext' => 10, + ]); + + $result = Horde_Imap_Client_Base_Deprecated::getCacheId( + $base, + 'INBOX', + true, + ['extra'] + ); + + $this->assertEquals('V1|H99|extra', $result); + } + + public function testRoundTrip(): void + { + $base = $this->createMock(Horde_Imap_Client_Base::class); + $base->method('status') + ->willReturn([ + 'uidvalidity' => 42, + 'highestmodseq' => 0, + 'messages' => 15, + 'uidnext' => 200, + ]); + + $id = Horde_Imap_Client_Base_Deprecated::getCacheId($base, 'INBOX', false); + $parsed = Horde_Imap_Client_Base_Deprecated::parseCacheId($id); + + $this->assertEquals(42, $parsed['uidvalidity']); + $this->assertEquals(200, $parsed['uidnext']); + $this->assertEquals(15, $parsed['messages']); + } +} diff --git a/test/Unit/Socket/CatenateTest.php b/test/Unit/Socket/CatenateTest.php new file mode 100644 index 00000000..e785a6ab --- /dev/null +++ b/test/Unit/Socket/CatenateTest.php @@ -0,0 +1,165 @@ +createMock(Horde_Imap_Client_Data_Fetch::class); + $fetchData->method('getFullMsg')->willReturn($stream); + $fetchData->method('getHeaders')->willReturn($stream); + $fetchData->method('getBodyPart')->willReturn($stream); + $fetchData->method('getHeaderText')->willReturn($stream); + $fetchData->method('getBodyText')->willReturn($stream); + $fetchData->method('getMimeHeader')->willReturn($stream); + + $fetchResult = [$uid => $fetchData]; + + $socket = $this->createMock(Horde_Imap_Client_Socket::class); + $socket->method('getIdsOb') + ->with($uid) + ->willReturn(new Horde_Imap_Client_Ids($uid)); + $socket->method('fetch') + ->willReturn($fetchResult); + + $url = new Horde_Imap_Client_Url_Imap(); + $url->uid = $uid; + $url->section = $section; + $url->mailbox = $mailbox; + + $catenate = new Horde_Imap_Client_Socket_Catenate($socket); + + return [$catenate, $url, $socket, $fetchData]; + } + + public function testFetchFullBody(): void + { + [$catenate, $url, $socket, $fetchData] = $this->createCatenate(42, null); + + $fetchData->expects($this->once()) + ->method('getFullMsg') + ->with(true); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchHeaderFields(): void + { + [$catenate, $url] = $this->createCatenate(1, 'HEADER.FIELDS (Subject From)'); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchHeaderFieldsNot(): void + { + [$catenate, $url] = $this->createCatenate(1, 'HEADER.FIELDS.NOT (Subject)'); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchBodyPart(): void + { + [$catenate, $url, , $fetchData] = $this->createCatenate(1, '1.2'); + + $fetchData->expects($this->once()) + ->method('getBodyPart') + ->with('1.2', true); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchHeader(): void + { + [$catenate, $url, , $fetchData] = $this->createCatenate(1, 'HEADER'); + + $fetchData->expects($this->once()) + ->method('getHeaderText') + ->with(0, Horde_Imap_Client_Data_Fetch::HEADER_STREAM); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchNestedHeader(): void + { + [$catenate, $url, , $fetchData] = $this->createCatenate(1, '2.HEADER'); + + $fetchData->expects($this->once()) + ->method('getHeaderText') + ->with('2', Horde_Imap_Client_Data_Fetch::HEADER_STREAM); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchText(): void + { + [$catenate, $url, , $fetchData] = $this->createCatenate(1, 'TEXT'); + + $fetchData->expects($this->once()) + ->method('getBodyText') + ->with(0, true); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testFetchMime(): void + { + [$catenate, $url, , $fetchData] = $this->createCatenate(1, '2.MIME'); + + $fetchData->expects($this->once()) + ->method('getMimeHeader') + ->with('2', Horde_Imap_Client_Data_Fetch::HEADER_STREAM); + + $result = $catenate->fetchFromUrl($url); + $this->assertIsResource($result); + } + + public function testUnrecognizedSectionReturnsNull(): void + { + [$catenate, $url] = $this->createCatenate(1, 'UNKNOWN'); + + $this->assertNull($catenate->fetchFromUrl($url)); + } +} From 255af259605edba50ca8dd1869074353c17da6f5 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sun, 12 Apr 2026 20:10:09 +0200 Subject: [PATCH 2/4] feat: PSR-4 / PSR-16 / PSR-14 Modern PHP Re-Packaging of ImapClient --- src/AclRight.php | 39 +++++++++++ src/CapabilityInterface.php | 40 +++++++++++ src/ConnectionConfig.php | 46 +++++++++++++ src/Event/AlertReceived.php | 24 +++++++ src/Event/AuthenticationFailed.php | 24 +++++++ src/Event/AuthenticationSucceeded.php | 24 +++++++ src/Event/CacheDeleted.php | 24 +++++++ src/Event/CacheRetrieved.php | 24 +++++++ src/Event/CacheStored.php | 24 +++++++ src/Event/CapabilityIgnored.php | 25 +++++++ src/Event/CapabilityNegotiated.php | 24 +++++++ src/Event/ConnectionClosed.php | 24 +++++++ src/Event/ConnectionEstablished.php | 24 +++++++ src/Event/DiagnosticEvent.php | 28 ++++++++ src/Event/ImapEvent.php | 46 +++++++++++++ src/Event/MailboxExpunged.php | 24 +++++++ src/Event/MailboxSelected.php | 24 +++++++ src/Event/SlowCommand.php | 24 +++++++ src/Exception/AuthenticationException.php | 24 +++++++ .../CapabilityNotSupportedException.php | 24 +++++++ src/Exception/ConnectionException.php | 24 +++++++ src/Exception/ImapProtocolException.php | 24 +++++++ src/Exception/MailboxNotFoundException.php | 24 +++++++ src/Exception/MailboxProtocolException.php | 28 ++++++++ src/Exception/Pop3ProtocolException.php | 24 +++++++ src/Exception/ServerResponseException.php | 41 +++++++++++ src/FilteredEventDispatcher.php | 50 ++++++++++++++ src/ImapAclAware.php | 46 +++++++++++++ src/ImapMetadataAware.php | 31 +++++++++ src/ImapProtocol.php | 69 +++++++++++++++++++ src/ImapQuotaAware.php | 33 +++++++++ src/MailboxListMode.php | 31 +++++++++ src/MailboxProtocol.php | 53 ++++++++++++++ src/MessageContent.php | 35 ++++++++++ src/MessageIdSet.php | 42 +++++++++++ src/MessageMetadata.php | 42 +++++++++++ src/OpenMode.php | 29 ++++++++ src/ParsedAccess.php | 52 ++++++++++++++ src/PartAccess.php | 41 +++++++++++ src/PasswordInterface.php | 32 +++++++++ src/SearchResultType.php | 33 +++++++++ src/SecureMode.php | 32 +++++++++ src/SortCriteria.php | 46 +++++++++++++ src/SpecialUse.php | 33 +++++++++ src/SystemFlag.php | 43 ++++++++++++ src/ThreadAlgorithm.php | 29 ++++++++ 46 files changed, 1527 insertions(+) create mode 100644 src/AclRight.php create mode 100644 src/CapabilityInterface.php create mode 100644 src/ConnectionConfig.php create mode 100644 src/Event/AlertReceived.php create mode 100644 src/Event/AuthenticationFailed.php create mode 100644 src/Event/AuthenticationSucceeded.php create mode 100644 src/Event/CacheDeleted.php create mode 100644 src/Event/CacheRetrieved.php create mode 100644 src/Event/CacheStored.php create mode 100644 src/Event/CapabilityIgnored.php create mode 100644 src/Event/CapabilityNegotiated.php create mode 100644 src/Event/ConnectionClosed.php create mode 100644 src/Event/ConnectionEstablished.php create mode 100644 src/Event/DiagnosticEvent.php create mode 100644 src/Event/ImapEvent.php create mode 100644 src/Event/MailboxExpunged.php create mode 100644 src/Event/MailboxSelected.php create mode 100644 src/Event/SlowCommand.php create mode 100644 src/Exception/AuthenticationException.php create mode 100644 src/Exception/CapabilityNotSupportedException.php create mode 100644 src/Exception/ConnectionException.php create mode 100644 src/Exception/ImapProtocolException.php create mode 100644 src/Exception/MailboxNotFoundException.php create mode 100644 src/Exception/MailboxProtocolException.php create mode 100644 src/Exception/Pop3ProtocolException.php create mode 100644 src/Exception/ServerResponseException.php create mode 100644 src/FilteredEventDispatcher.php create mode 100644 src/ImapAclAware.php create mode 100644 src/ImapMetadataAware.php create mode 100644 src/ImapProtocol.php create mode 100644 src/ImapQuotaAware.php create mode 100644 src/MailboxListMode.php create mode 100644 src/MailboxProtocol.php create mode 100644 src/MessageContent.php create mode 100644 src/MessageIdSet.php create mode 100644 src/MessageMetadata.php create mode 100644 src/OpenMode.php create mode 100644 src/ParsedAccess.php create mode 100644 src/PartAccess.php create mode 100644 src/PasswordInterface.php create mode 100644 src/SearchResultType.php create mode 100644 src/SecureMode.php create mode 100644 src/SortCriteria.php create mode 100644 src/SpecialUse.php create mode 100644 src/SystemFlag.php create mode 100644 src/ThreadAlgorithm.php diff --git a/src/AclRight.php b/src/AclRight.php new file mode 100644 index 00000000..579c4e91 --- /dev/null +++ b/src/AclRight.php @@ -0,0 +1,39 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum AclRight: string +{ + case Lookup = 'l'; + case Read = 'r'; + case Seen = 's'; + case Write = 'w'; + case Insert = 'i'; + case Post = 'p'; + case CreateMbox = 'k'; + case DeleteMbox = 'x'; + case DeleteMsgs = 't'; + case Expunge = 'e'; + case Administer = 'a'; +} diff --git a/src/CapabilityInterface.php b/src/CapabilityInterface.php new file mode 100644 index 00000000..d0f0d711 --- /dev/null +++ b/src/CapabilityInterface.php @@ -0,0 +1,40 @@ + + * @copyright 2014-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface CapabilityInterface +{ + /** + * Query whether a capability (and optional parameter) is supported. + */ + public function query(string $capability, ?string $parameter = null): bool; + + /** + * Get the parameters for a capability. + * + * @return string[] + */ + public function getParams(string $capability): array; +} diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php new file mode 100644 index 00000000..3d3f9281 --- /dev/null +++ b/src/ConnectionConfig.php @@ -0,0 +1,46 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +final class ConnectionConfig +{ + public function __construct( + public readonly string $username, + public readonly string|PasswordInterface $password, + public readonly string $hostspec = 'localhost', + public readonly ?int $port = null, + public readonly SecureMode $secure = SecureMode::None, + public readonly int $timeout = 30, + public readonly int $readTimeout = 120, + public readonly ?array $context = null, + public readonly array $capabilityIgnore = [], + public readonly ?array $id = null, + public readonly array $lang = [], + ) {} +} diff --git a/src/Event/AlertReceived.php b/src/Event/AlertReceived.php new file mode 100644 index 00000000..ab9cf8b0 --- /dev/null +++ b/src/Event/AlertReceived.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class AlertReceived extends ImapEvent {} diff --git a/src/Event/AuthenticationFailed.php b/src/Event/AuthenticationFailed.php new file mode 100644 index 00000000..3713cd76 --- /dev/null +++ b/src/Event/AuthenticationFailed.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class AuthenticationFailed extends ImapEvent {} diff --git a/src/Event/AuthenticationSucceeded.php b/src/Event/AuthenticationSucceeded.php new file mode 100644 index 00000000..1fd5d162 --- /dev/null +++ b/src/Event/AuthenticationSucceeded.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class AuthenticationSucceeded extends ImapEvent {} diff --git a/src/Event/CacheDeleted.php b/src/Event/CacheDeleted.php new file mode 100644 index 00000000..7c8f15ea --- /dev/null +++ b/src/Event/CacheDeleted.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class CacheDeleted extends DiagnosticEvent {} diff --git a/src/Event/CacheRetrieved.php b/src/Event/CacheRetrieved.php new file mode 100644 index 00000000..c65d852c --- /dev/null +++ b/src/Event/CacheRetrieved.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class CacheRetrieved extends DiagnosticEvent {} diff --git a/src/Event/CacheStored.php b/src/Event/CacheStored.php new file mode 100644 index 00000000..1f5755a3 --- /dev/null +++ b/src/Event/CacheStored.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class CacheStored extends DiagnosticEvent {} diff --git a/src/Event/CapabilityIgnored.php b/src/Event/CapabilityIgnored.php new file mode 100644 index 00000000..c9cbb896 --- /dev/null +++ b/src/Event/CapabilityIgnored.php @@ -0,0 +1,25 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class CapabilityIgnored extends DiagnosticEvent {} diff --git a/src/Event/CapabilityNegotiated.php b/src/Event/CapabilityNegotiated.php new file mode 100644 index 00000000..3254dc1c --- /dev/null +++ b/src/Event/CapabilityNegotiated.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class CapabilityNegotiated extends ImapEvent {} diff --git a/src/Event/ConnectionClosed.php b/src/Event/ConnectionClosed.php new file mode 100644 index 00000000..65a0a5ba --- /dev/null +++ b/src/Event/ConnectionClosed.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class ConnectionClosed extends ImapEvent {} diff --git a/src/Event/ConnectionEstablished.php b/src/Event/ConnectionEstablished.php new file mode 100644 index 00000000..2297be8d --- /dev/null +++ b/src/Event/ConnectionEstablished.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class ConnectionEstablished extends ImapEvent {} diff --git a/src/Event/DiagnosticEvent.php b/src/Event/DiagnosticEvent.php new file mode 100644 index 00000000..72fdd4d4 --- /dev/null +++ b/src/Event/DiagnosticEvent.php @@ -0,0 +1,28 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +abstract class DiagnosticEvent extends ImapEvent {} diff --git a/src/Event/ImapEvent.php b/src/Event/ImapEvent.php new file mode 100644 index 00000000..3377f7ba --- /dev/null +++ b/src/Event/ImapEvent.php @@ -0,0 +1,46 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +abstract class ImapEvent +{ + public function __construct( + private readonly string $message = '', + private readonly array $context = [], + ) {} + + public function getMessage(): string + { + return $this->message; + } + + /** + * @return array + */ + public function getContext(): array + { + return $this->context; + } +} diff --git a/src/Event/MailboxExpunged.php b/src/Event/MailboxExpunged.php new file mode 100644 index 00000000..cfa00ed5 --- /dev/null +++ b/src/Event/MailboxExpunged.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class MailboxExpunged extends ImapEvent {} diff --git a/src/Event/MailboxSelected.php b/src/Event/MailboxSelected.php new file mode 100644 index 00000000..8a849259 --- /dev/null +++ b/src/Event/MailboxSelected.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class MailboxSelected extends ImapEvent {} diff --git a/src/Event/SlowCommand.php b/src/Event/SlowCommand.php new file mode 100644 index 00000000..2a75e0ae --- /dev/null +++ b/src/Event/SlowCommand.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class SlowCommand extends ImapEvent {} diff --git a/src/Exception/AuthenticationException.php b/src/Exception/AuthenticationException.php new file mode 100644 index 00000000..a1dd3f12 --- /dev/null +++ b/src/Exception/AuthenticationException.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class AuthenticationException extends MailboxProtocolException {} diff --git a/src/Exception/CapabilityNotSupportedException.php b/src/Exception/CapabilityNotSupportedException.php new file mode 100644 index 00000000..f6d71daf --- /dev/null +++ b/src/Exception/CapabilityNotSupportedException.php @@ -0,0 +1,24 @@ + + * @copyright 2012-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class CapabilityNotSupportedException extends ImapProtocolException {} diff --git a/src/Exception/ConnectionException.php b/src/Exception/ConnectionException.php new file mode 100644 index 00000000..cb7de7b9 --- /dev/null +++ b/src/Exception/ConnectionException.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class ConnectionException extends MailboxProtocolException {} diff --git a/src/Exception/ImapProtocolException.php b/src/Exception/ImapProtocolException.php new file mode 100644 index 00000000..90f9212a --- /dev/null +++ b/src/Exception/ImapProtocolException.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class ImapProtocolException extends MailboxProtocolException {} diff --git a/src/Exception/MailboxNotFoundException.php b/src/Exception/MailboxNotFoundException.php new file mode 100644 index 00000000..ad0321b5 --- /dev/null +++ b/src/Exception/MailboxNotFoundException.php @@ -0,0 +1,24 @@ + + * @copyright 2012-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class MailboxNotFoundException extends ImapProtocolException {} diff --git a/src/Exception/MailboxProtocolException.php b/src/Exception/MailboxProtocolException.php new file mode 100644 index 00000000..6726e2ba --- /dev/null +++ b/src/Exception/MailboxProtocolException.php @@ -0,0 +1,28 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class MailboxProtocolException extends RuntimeException {} diff --git a/src/Exception/Pop3ProtocolException.php b/src/Exception/Pop3ProtocolException.php new file mode 100644 index 00000000..d10497bc --- /dev/null +++ b/src/Exception/Pop3ProtocolException.php @@ -0,0 +1,24 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class Pop3ProtocolException extends MailboxProtocolException {} diff --git a/src/Exception/ServerResponseException.php b/src/Exception/ServerResponseException.php new file mode 100644 index 00000000..fed126fb --- /dev/null +++ b/src/Exception/ServerResponseException.php @@ -0,0 +1,41 @@ + + * @copyright 2012-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class ServerResponseException extends MailboxProtocolException +{ + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null, + public readonly ?string $command = null, + public readonly ?string $status = null, + public readonly ?string $responseText = null, + ) { + parent::__construct($message, $code, $previous); + } +} diff --git a/src/FilteredEventDispatcher.php b/src/FilteredEventDispatcher.php new file mode 100644 index 00000000..bb840676 --- /dev/null +++ b/src/FilteredEventDispatcher.php @@ -0,0 +1,50 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +final class FilteredEventDispatcher implements EventDispatcherInterface +{ + /** + * @param class-string[] $suppress Event classes to swallow silently. + */ + public function __construct( + private readonly EventDispatcherInterface $inner, + private readonly array $suppress = [DiagnosticEvent::class], + ) {} + + public function dispatch(object $event): object + { + foreach ($this->suppress as $class) { + if ($event instanceof $class) { + return $event; + } + } + + return $this->inner->dispatch($event); + } +} diff --git a/src/ImapAclAware.php b/src/ImapAclAware.php new file mode 100644 index 00000000..92058c39 --- /dev/null +++ b/src/ImapAclAware.php @@ -0,0 +1,46 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface ImapAclAware +{ + /** + * @return object Acl value object + */ + public function getACL(string $mailbox): object; + + public function setACL(string $mailbox, string $identifier, array $options): void; + + public function deleteACL(string $mailbox, string $identifier): void; + + /** + * @return object AclRights value object + */ + public function listACLRights(string $mailbox, string $identifier): object; + + /** + * @return object AclRights value object + */ + public function getMyACLRights(string $mailbox): object; +} diff --git a/src/ImapMetadataAware.php b/src/ImapMetadataAware.php new file mode 100644 index 00000000..c9c138ef --- /dev/null +++ b/src/ImapMetadataAware.php @@ -0,0 +1,31 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface ImapMetadataAware +{ + public function getMetadata(string $mailbox, array $entries, array $options = []): array; + + public function setMetadata(string $mailbox, array $data): void; +} diff --git a/src/ImapProtocol.php b/src/ImapProtocol.php new file mode 100644 index 00000000..f4dccb8d --- /dev/null +++ b/src/ImapProtocol.php @@ -0,0 +1,69 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface ImapProtocol extends MailboxProtocol +{ + public function getCapability(): CapabilityInterface; + + public function openMailbox(string $mailbox, OpenMode $mode): void; + + public function createMailbox(string $mailbox): void; + + public function deleteMailbox(string $mailbox): void; + + public function renameMailbox(string $old, string $new): void; + + public function subscribeMailbox(string $mailbox, bool $subscribe = true): void; + + public function listMailboxes(string $pattern, MailboxListMode $mode, array $options = []): array; + + public function close(array $options = []): void; + + /** + * @param object $query SearchQuery + * @return object SearchResult + */ + public function search(string $mailbox, object $query, array $options = []): object; + + /** + * @return object ThreadResult + */ + public function thread(string $mailbox, array $options = []): object; + + public function copy(string $source, string $dest, array $options = []): MessageIdSet; + + public function move(string $source, string $dest, array $options = []): MessageIdSet; + + public function append(string $mailbox, array $data, array $options = []): MessageIdSet; + + /** + * @return object NamespaceList + */ + public function getNamespaces(): object; + + public function unselect(): void; +} diff --git a/src/ImapQuotaAware.php b/src/ImapQuotaAware.php new file mode 100644 index 00000000..fe69fd66 --- /dev/null +++ b/src/ImapQuotaAware.php @@ -0,0 +1,33 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface ImapQuotaAware +{ + public function setQuota(string $root, array $resources): void; + + public function getQuota(string $root): array; + + public function getQuotaRoot(string $mailbox): array; +} diff --git a/src/MailboxListMode.php b/src/MailboxListMode.php new file mode 100644 index 00000000..aa167cc9 --- /dev/null +++ b/src/MailboxListMode.php @@ -0,0 +1,31 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum MailboxListMode: int +{ + case Subscribed = 1; + case SubscribedExists = 2; + case Unsubscribed = 3; + case All = 4; + case AllSubscribed = 5; +} diff --git a/src/MailboxProtocol.php b/src/MailboxProtocol.php new file mode 100644 index 00000000..802cb07a --- /dev/null +++ b/src/MailboxProtocol.php @@ -0,0 +1,53 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface MailboxProtocol +{ + public function login(): void; + + public function logout(): void; + + public function noop(): void; + + /** + * @return object MailboxStatus value object + */ + public function status(string $mailbox, int $flags): object; + + /** + * @param object $query FetchQuery + * @return Generator + */ + public function fetch(string $mailbox, MessageIdSet $ids, object $query): Generator; + + public function store(string $mailbox, array $options): MessageIdSet; + + public function expunge(string $mailbox, array $options): MessageIdSet; + + public function getIdsOb(mixed $ids = null, bool $sequence = false): MessageIdSet; +} diff --git a/src/MessageContent.php b/src/MessageContent.php new file mode 100644 index 00000000..d5c86a61 --- /dev/null +++ b/src/MessageContent.php @@ -0,0 +1,35 @@ + + * @copyright 2011-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface MessageContent +{ + public function getFullMsg(): Horde_Stream; + + public function getHeaderText(string|int $id = 0): Horde_Stream; + + public function getBodyText(string|int $id = 0): Horde_Stream; +} diff --git a/src/MessageIdSet.php b/src/MessageIdSet.php new file mode 100644 index 00000000..e0fcd834 --- /dev/null +++ b/src/MessageIdSet.php @@ -0,0 +1,42 @@ + + * + * @author Michael Slusarz + * @copyright 2011-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface MessageIdSet extends Countable, IteratorAggregate +{ + public function isEmpty(): bool; + + /** + * @return array + */ + public function toArray(): array; + + public function __toString(): string; +} diff --git a/src/MessageMetadata.php b/src/MessageMetadata.php new file mode 100644 index 00000000..0a3be042 --- /dev/null +++ b/src/MessageMetadata.php @@ -0,0 +1,42 @@ + + * @copyright 2011-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface MessageMetadata +{ + public function getUid(): int|string; + + /** + * @return string[] + */ + public function getFlags(): array; + + public function getSize(): int; + + public function getImapDate(): DateTimeImmutable; + + public function getSeq(): ?int; + + public function getModSeq(): ?int; +} diff --git a/src/OpenMode.php b/src/OpenMode.php new file mode 100644 index 00000000..5f94909b --- /dev/null +++ b/src/OpenMode.php @@ -0,0 +1,29 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum OpenMode: int +{ + case Readonly = 1; + case ReadWrite = 2; + case Auto = 3; +} diff --git a/src/ParsedAccess.php b/src/ParsedAccess.php new file mode 100644 index 00000000..95ed8426 --- /dev/null +++ b/src/ParsedAccess.php @@ -0,0 +1,52 @@ + + * @copyright 2011-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface ParsedAccess +{ + /** + * @return object Envelope value object + */ + public function getEnvelope(): object; + + /** + * @return object Headers value object + */ + public function getHeaders(string $label): object; + + /** + * Yields headers one at a time from the stream. + * + * @return Generator + */ + public function getHeadersIterator(string $label): Generator; + + /** + * @return object BodyStructure value object + */ + public function getStructure(): object; +} diff --git a/src/PartAccess.php b/src/PartAccess.php new file mode 100644 index 00000000..eea20291 --- /dev/null +++ b/src/PartAccess.php @@ -0,0 +1,41 @@ + + * @copyright 2011-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface PartAccess +{ + public function getBodyPart(string $id): Horde_Stream; + + public function getMimeHeader(string $id): Horde_Stream; + + /** + * Yields parts lazily from BODYSTRUCTURE. + * + * @return Generator + */ + public function getParts(): Generator; +} diff --git a/src/PasswordInterface.php b/src/PasswordInterface.php new file mode 100644 index 00000000..a3a065b1 --- /dev/null +++ b/src/PasswordInterface.php @@ -0,0 +1,32 @@ + + * @copyright 2013-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +interface PasswordInterface +{ + /** + * Return the password for the server connection. + */ + public function getPassword(): string; +} diff --git a/src/SearchResultType.php b/src/SearchResultType.php new file mode 100644 index 00000000..b76c85ae --- /dev/null +++ b/src/SearchResultType.php @@ -0,0 +1,33 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum SearchResultType: int +{ + case Count = 1; + case Match = 2; + case Max = 3; + case Min = 4; + case Save = 5; + /** RFC 6203 */ + case Relevancy = 6; +} diff --git a/src/SecureMode.php b/src/SecureMode.php new file mode 100644 index 00000000..313a979b --- /dev/null +++ b/src/SecureMode.php @@ -0,0 +1,32 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum SecureMode: string +{ + case None = ''; + case Ssl = 'ssl'; + case Tls = 'tls'; + case Tlsv1 = 'tlsv1'; +} diff --git a/src/SortCriteria.php b/src/SortCriteria.php new file mode 100644 index 00000000..82b80bfb --- /dev/null +++ b/src/SortCriteria.php @@ -0,0 +1,46 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum SortCriteria: int +{ + case Arrival = 1; + case Cc = 2; + case Date = 3; + case From = 4; + case Reverse = 5; + case Size = 6; + case Subject = 7; + case To = 8; + case Thread = 9; + /** RFC 5957 */ + case DisplayFrom = 10; + /** RFC 5957 */ + case DisplayTo = 11; + case Sequence = 12; + /** RFC 6203 */ + case Relevancy = 13; + case DisplayFromFallback = 14; + case DisplayToFallback = 15; +} diff --git a/src/SpecialUse.php b/src/SpecialUse.php new file mode 100644 index 00000000..9318481e --- /dev/null +++ b/src/SpecialUse.php @@ -0,0 +1,33 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum SpecialUse: string +{ + case All = '\\All'; + case Archive = '\\Archive'; + case Drafts = '\\Drafts'; + case Flagged = '\\Flagged'; + case Junk = '\\Junk'; + case Sent = '\\Sent'; + case Trash = '\\Trash'; +} diff --git a/src/SystemFlag.php b/src/SystemFlag.php new file mode 100644 index 00000000..652f9267 --- /dev/null +++ b/src/SystemFlag.php @@ -0,0 +1,43 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum SystemFlag: string +{ + /** RFC 3501 section 2.3.2 */ + case Answered = '\\answered'; + case Deleted = '\\deleted'; + case Draft = '\\draft'; + case Flagged = '\\flagged'; + case Recent = '\\recent'; + case Seen = '\\seen'; + + /** RFC 3503 section 3.3 */ + case MdnSent = '$mdnsent'; + + /** RFC 5550 section 2.8 */ + case Forwarded = '$forwarded'; + + /** RFC 5788 registered keywords */ + case Junk = '$junk'; + case NotJunk = '$notjunk'; +} diff --git a/src/ThreadAlgorithm.php b/src/ThreadAlgorithm.php new file mode 100644 index 00000000..0cb43da6 --- /dev/null +++ b/src/ThreadAlgorithm.php @@ -0,0 +1,29 @@ + + * @copyright 2008-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +enum ThreadAlgorithm: int +{ + case OrderedSubject = 1; + case References = 2; + case Refs = 3; +} From 0057c0375683385e143066cb8475dbc52c4fbaf1 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sun, 12 Apr 2026 20:10:49 +0200 Subject: [PATCH 3/4] test: Modern implementation's test suite --- phpunit.xml.dist | 1 + .../Src/CapabilityInterfaceTest.php | 63 ++++ .../Integration/Src/ComposedInterfaceTest.php | 278 ++++++++++++++++++ test/Integration/Src/ImapAclAwareTest.php | 66 +++++ .../Integration/Src/ImapMetadataAwareTest.php | 47 +++ test/Integration/Src/ImapProtocolTest.php | 183 ++++++++++++ test/Integration/Src/ImapQuotaAwareTest.php | 49 +++ test/Integration/Src/MailboxProtocolTest.php | 113 +++++++ test/Integration/Src/MessageContentTest.php | 68 +++++ test/Integration/Src/MessageIdSetTest.php | 75 +++++ test/Integration/Src/MessageMetadataTest.php | 103 +++++++ test/Integration/Src/ParsedAccessTest.php | 69 +++++ test/Integration/Src/PartAccessTest.php | 76 +++++ .../Integration/Src/PasswordInterfaceTest.php | 38 +++ test/Stub/Base.php | 81 ++--- test/Stub/StubMessageIdSet.php | 45 +++ test/Unit/Base/BaseConstructorTest.php | 3 +- .../Unit/Src/ConnectionConfigEdgeCaseTest.php | 104 +++++++ test/Unit/Src/ConnectionConfigTest.php | 71 +++++ test/Unit/Src/EnumTest.php | 152 ++++++++++ test/Unit/Src/EventHierarchyTest.php | 97 ++++++ test/Unit/Src/ExceptionHierarchyTest.php | 124 ++++++++ .../FilteredEventDispatcherEdgeCaseTest.php | 165 +++++++++++ test/Unit/Src/FilteredEventDispatcherTest.php | 74 +++++ test/Unit/Src/ImapEventEdgeCaseTest.php | 156 ++++++++++ test/Unit/Src/ServerResponseExceptionTest.php | 116 ++++++++ 26 files changed, 2358 insertions(+), 59 deletions(-) create mode 100644 test/Integration/Src/CapabilityInterfaceTest.php create mode 100644 test/Integration/Src/ComposedInterfaceTest.php create mode 100644 test/Integration/Src/ImapAclAwareTest.php create mode 100644 test/Integration/Src/ImapMetadataAwareTest.php create mode 100644 test/Integration/Src/ImapProtocolTest.php create mode 100644 test/Integration/Src/ImapQuotaAwareTest.php create mode 100644 test/Integration/Src/MailboxProtocolTest.php create mode 100644 test/Integration/Src/MessageContentTest.php create mode 100644 test/Integration/Src/MessageIdSetTest.php create mode 100644 test/Integration/Src/MessageMetadataTest.php create mode 100644 test/Integration/Src/ParsedAccessTest.php create mode 100644 test/Integration/Src/PartAccessTest.php create mode 100644 test/Integration/Src/PasswordInterfaceTest.php create mode 100644 test/Stub/StubMessageIdSet.php create mode 100644 test/Unit/Src/ConnectionConfigEdgeCaseTest.php create mode 100644 test/Unit/Src/ConnectionConfigTest.php create mode 100644 test/Unit/Src/EnumTest.php create mode 100644 test/Unit/Src/EventHierarchyTest.php create mode 100644 test/Unit/Src/ExceptionHierarchyTest.php create mode 100644 test/Unit/Src/FilteredEventDispatcherEdgeCaseTest.php create mode 100644 test/Unit/Src/FilteredEventDispatcherTest.php create mode 100644 test/Unit/Src/ImapEventEdgeCaseTest.php create mode 100644 test/Unit/Src/ServerResponseExceptionTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index aeaa9076..04b955ad 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,6 +29,7 @@ lib + src diff --git a/test/Integration/Src/CapabilityInterfaceTest.php b/test/Integration/Src/CapabilityInterfaceTest.php new file mode 100644 index 00000000..af81a385 --- /dev/null +++ b/test/Integration/Src/CapabilityInterfaceTest.php @@ -0,0 +1,63 @@ +createImplementation(); + $this->assertTrue($cap->query('IMAP4rev1')); + $this->assertFalse($cap->query('CONDSTORE')); + } + + public function testQueryWithParameter(): void + { + $cap = $this->createImplementation(); + $this->assertTrue($cap->query('AUTH', 'PLAIN')); + $this->assertFalse($cap->query('AUTH', 'CRAM-MD5')); + } + + public function testGetParamsReturnsStringArray(): void + { + $cap = $this->createImplementation(); + $this->assertSame(['PLAIN', 'LOGIN'], $cap->getParams('AUTH')); + } + + public function testGetParamsEmptyArray(): void + { + $cap = $this->createImplementation(); + $this->assertSame([], $cap->getParams('UNKNOWN')); + } +} diff --git a/test/Integration/Src/ComposedInterfaceTest.php b/test/Integration/Src/ComposedInterfaceTest.php new file mode 100644 index 00000000..e43f7678 --- /dev/null +++ b/test/Integration/Src/ComposedInterfaceTest.php @@ -0,0 +1,278 @@ +assertInstanceOf(ImapProtocol::class, $stub); + $this->assertInstanceOf(ImapQuotaAware::class, $stub); + $this->assertInstanceOf(ImapAclAware::class, $stub); + $this->assertInstanceOf(ImapMetadataAware::class, $stub); + + // Exercise one method from each interface + $stub->login(); + $stub->openMailbox('INBOX', OpenMode::ReadWrite); + $stub->setQuota('', []); + $stub->deleteACL('INBOX', 'user'); + $stub->setMetadata('INBOX', []); + $this->addToAssertionCount(1); + } + + public function testMessageCanImplementMetadataAndContent(): void + { + $stub = new class implements MessageMetadata, MessageContent { + public function getUid(): int|string + { + return 1; + } + public function getFlags(): array + { + return []; + } + public function getSize(): int + { + return 0; + } + public function getImapDate(): DateTimeImmutable + { + return new DateTimeImmutable(); + } + public function getSeq(): ?int + { + return null; + } + public function getModSeq(): ?int + { + return null; + } + public function getFullMsg(): Horde_Stream + { + return new Horde_Stream(); + } + public function getHeaderText(string|int $id = 0): Horde_Stream + { + return new Horde_Stream(); + } + public function getBodyText(string|int $id = 0): Horde_Stream + { + return new Horde_Stream(); + } + }; + + $this->assertInstanceOf(MessageMetadata::class, $stub); + $this->assertInstanceOf(MessageContent::class, $stub); + $this->assertSame(1, $stub->getUid()); + $this->assertInstanceOf(Horde_Stream::class, $stub->getFullMsg()); + } + + public function testMessageCanImplementAllFourLayers(): void + { + $stub = new class implements MessageMetadata, MessageContent, PartAccess, ParsedAccess { + public function getUid(): int|string + { + return 42; + } + public function getFlags(): array + { + return ['\\Seen']; + } + public function getSize(): int + { + return 2048; + } + public function getImapDate(): DateTimeImmutable + { + return new DateTimeImmutable(); + } + public function getSeq(): ?int + { + return 1; + } + public function getModSeq(): ?int + { + return null; + } + public function getFullMsg(): Horde_Stream + { + return new Horde_Stream(); + } + public function getHeaderText(string|int $id = 0): Horde_Stream + { + return new Horde_Stream(); + } + public function getBodyText(string|int $id = 0): Horde_Stream + { + return new Horde_Stream(); + } + public function getBodyPart(string $id): Horde_Stream + { + return new Horde_Stream(); + } + public function getMimeHeader(string $id): Horde_Stream + { + return new Horde_Stream(); + } + public function getParts(): Generator + { + yield '1' => new stdClass(); + } + public function getEnvelope(): object + { + return new stdClass(); + } + public function getHeaders(string $label): object + { + return new stdClass(); + } + public function getHeadersIterator(string $label): Generator + { + yield new stdClass(); + } + public function getStructure(): object + { + return new stdClass(); + } + }; + + $this->assertInstanceOf(MessageMetadata::class, $stub); + $this->assertInstanceOf(MessageContent::class, $stub); + $this->assertInstanceOf(PartAccess::class, $stub); + $this->assertInstanceOf(ParsedAccess::class, $stub); + + $this->assertSame(42, $stub->getUid()); + $this->assertInstanceOf(Horde_Stream::class, $stub->getBodyPart('1')); + $this->assertIsObject($stub->getEnvelope()); + } +} diff --git a/test/Integration/Src/ImapAclAwareTest.php b/test/Integration/Src/ImapAclAwareTest.php new file mode 100644 index 00000000..cc075d39 --- /dev/null +++ b/test/Integration/Src/ImapAclAwareTest.php @@ -0,0 +1,66 @@ +assertIsObject($this->createImplementation()->getACL('INBOX')); + } + + public function testSetACLReturnsVoid(): void + { + $this->createImplementation()->setACL('INBOX', 'user', ['rights' => 'lrs']); + $this->addToAssertionCount(1); + } + + public function testDeleteACLReturnsVoid(): void + { + $this->createImplementation()->deleteACL('INBOX', 'user'); + $this->addToAssertionCount(1); + } + + public function testListACLRightsReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->listACLRights('INBOX', 'user')); + } + + public function testGetMyACLRightsReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->getMyACLRights('INBOX')); + } + + public function testInstanceOfImapAclAware(): void + { + $this->assertInstanceOf(ImapAclAware::class, $this->createImplementation()); + } +} diff --git a/test/Integration/Src/ImapMetadataAwareTest.php b/test/Integration/Src/ImapMetadataAwareTest.php new file mode 100644 index 00000000..ce2ad660 --- /dev/null +++ b/test/Integration/Src/ImapMetadataAwareTest.php @@ -0,0 +1,47 @@ + 'test']; + } + + public function setMetadata(string $mailbox, array $data): void {} + }; + } + + public function testGetMetadataReturnsArray(): void + { + $this->assertIsArray($this->createImplementation()->getMetadata('INBOX', ['/shared/comment'])); + } + + public function testGetMetadataWithOptions(): void + { + $result = $this->createImplementation()->getMetadata('INBOX', ['/shared/comment'], ['DEPTH' => 0]); + $this->assertIsArray($result); + } + + public function testSetMetadataReturnsVoid(): void + { + $this->createImplementation()->setMetadata('INBOX', ['/shared/comment' => 'test']); + $this->addToAssertionCount(1); + } + + public function testInstanceOfImapMetadataAware(): void + { + $this->assertInstanceOf(ImapMetadataAware::class, $this->createImplementation()); + } +} diff --git a/test/Integration/Src/ImapProtocolTest.php b/test/Integration/Src/ImapProtocolTest.php new file mode 100644 index 00000000..5ab0c6d7 --- /dev/null +++ b/test/Integration/Src/ImapProtocolTest.php @@ -0,0 +1,183 @@ + new stdClass(); + } + public function store(string $mailbox, array $options): MessageIdSet + { + return new StubMessageIdSet(); + } + public function expunge(string $mailbox, array $options): MessageIdSet + { + return new StubMessageIdSet(); + } + public function getIdsOb(mixed $ids = null, bool $sequence = false): MessageIdSet + { + return new StubMessageIdSet(); + } + + // ImapProtocol methods + public function getCapability(): CapabilityInterface + { + return new class implements CapabilityInterface { + public function query(string $capability, ?string $parameter = null): bool + { + return false; + } + public function getParams(string $capability): array + { + return []; + } + }; + } + public function openMailbox(string $mailbox, OpenMode $mode): void {} + public function createMailbox(string $mailbox): void {} + public function deleteMailbox(string $mailbox): void {} + public function renameMailbox(string $old, string $new): void {} + public function subscribeMailbox(string $mailbox, bool $subscribe = true): void {} + public function listMailboxes(string $pattern, MailboxListMode $mode, array $options = []): array + { + return []; + } + public function close(array $options = []): void {} + public function search(string $mailbox, object $query, array $options = []): object + { + return new stdClass(); + } + public function thread(string $mailbox, array $options = []): object + { + return new stdClass(); + } + public function copy(string $source, string $dest, array $options = []): MessageIdSet + { + return new StubMessageIdSet(); + } + public function move(string $source, string $dest, array $options = []): MessageIdSet + { + return new StubMessageIdSet(); + } + public function append(string $mailbox, array $data, array $options = []): MessageIdSet + { + return new StubMessageIdSet(); + } + public function getNamespaces(): object + { + return new stdClass(); + } + public function unselect(): void {} + }; + } + + public function testImplementsMailboxProtocol(): void + { + $this->assertInstanceOf(MailboxProtocol::class, $this->createImplementation()); + } + + public function testImplementsImapProtocol(): void + { + $this->assertInstanceOf(ImapProtocol::class, $this->createImplementation()); + } + + public function testGetCapabilityReturnsCapabilityInterface(): void + { + $this->assertInstanceOf(CapabilityInterface::class, $this->createImplementation()->getCapability()); + } + + public function testOpenMailboxAcceptsOpenModeEnum(): void + { + $this->createImplementation()->openMailbox('INBOX', OpenMode::ReadWrite); + $this->addToAssertionCount(1); + } + + public function testCreateDeleteRenameMailbox(): void + { + $stub = $this->createImplementation(); + $stub->createMailbox('Test'); + $stub->renameMailbox('Test', 'Archive'); + $stub->deleteMailbox('Archive'); + $this->addToAssertionCount(1); + } + + public function testSubscribeMailbox(): void + { + $stub = $this->createImplementation(); + $stub->subscribeMailbox('INBOX', true); + $stub->subscribeMailbox('INBOX'); + $this->addToAssertionCount(1); + } + + public function testListMailboxesAcceptsMailboxListModeEnum(): void + { + $result = $this->createImplementation()->listMailboxes('*', MailboxListMode::All); + $this->assertIsArray($result); + } + + public function testSearchReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->search('INBOX', new stdClass())); + } + + public function testThreadReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->thread('INBOX')); + } + + public function testCopyReturnsMessageIdSet(): void + { + $this->assertInstanceOf(MessageIdSet::class, $this->createImplementation()->copy('INBOX', 'Archive')); + } + + public function testMoveReturnsMessageIdSet(): void + { + $this->assertInstanceOf(MessageIdSet::class, $this->createImplementation()->move('INBOX', 'Archive')); + } + + public function testAppendReturnsMessageIdSet(): void + { + $this->assertInstanceOf(MessageIdSet::class, $this->createImplementation()->append('INBOX', [])); + } + + public function testGetNamespacesReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->getNamespaces()); + } + + public function testCloseAndUnselect(): void + { + $stub = $this->createImplementation(); + $stub->close(); + $stub->unselect(); + $this->addToAssertionCount(1); + } +} diff --git a/test/Integration/Src/ImapQuotaAwareTest.php b/test/Integration/Src/ImapQuotaAwareTest.php new file mode 100644 index 00000000..a0b23ad7 --- /dev/null +++ b/test/Integration/Src/ImapQuotaAwareTest.php @@ -0,0 +1,49 @@ + [1024, 10240]]; + } + public function getQuotaRoot(string $mailbox): array + { + return ['INBOX' => '']; + } + }; + } + + public function testSetQuotaReturnsVoid(): void + { + $this->createImplementation()->setQuota('', ['STORAGE' => 10240]); + $this->addToAssertionCount(1); + } + + public function testGetQuotaReturnsArray(): void + { + $this->assertIsArray($this->createImplementation()->getQuota('')); + } + + public function testGetQuotaRootReturnsArray(): void + { + $this->assertIsArray($this->createImplementation()->getQuotaRoot('INBOX')); + } + + public function testInstanceOfImapQuotaAware(): void + { + $this->assertInstanceOf(ImapQuotaAware::class, $this->createImplementation()); + } +} diff --git a/test/Integration/Src/MailboxProtocolTest.php b/test/Integration/Src/MailboxProtocolTest.php new file mode 100644 index 00000000..56657bba --- /dev/null +++ b/test/Integration/Src/MailboxProtocolTest.php @@ -0,0 +1,113 @@ + new stdClass(); + } + + public function store(string $mailbox, array $options): MessageIdSet + { + return new StubMessageIdSet(); + } + + public function expunge(string $mailbox, array $options): MessageIdSet + { + return new StubMessageIdSet(); + } + + public function getIdsOb(mixed $ids = null, bool $sequence = false): MessageIdSet + { + return new StubMessageIdSet(is_array($ids) ? $ids : []); + } + }; + } + + public function testLoginReturnsVoid(): void + { + $this->createImplementation()->login(); + $this->addToAssertionCount(1); + } + + public function testLogoutReturnsVoid(): void + { + $this->createImplementation()->logout(); + $this->addToAssertionCount(1); + } + + public function testNoopReturnsVoid(): void + { + $this->createImplementation()->noop(); + $this->addToAssertionCount(1); + } + + public function testStatusReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->status('INBOX', 0)); + } + + public function testFetchReturnsGenerator(): void + { + $gen = $this->createImplementation()->fetch('INBOX', new StubMessageIdSet([1]), new stdClass()); + $this->assertInstanceOf(Generator::class, $gen); + } + + public function testFetchYieldsResults(): void + { + $gen = $this->createImplementation()->fetch('INBOX', new StubMessageIdSet([1]), new stdClass()); + $items = iterator_to_array($gen); + $this->assertCount(1, $items); + } + + public function testStoreReturnsMessageIdSet(): void + { + $this->assertInstanceOf(MessageIdSet::class, $this->createImplementation()->store('INBOX', [])); + } + + public function testExpungeReturnsMessageIdSet(): void + { + $this->assertInstanceOf(MessageIdSet::class, $this->createImplementation()->expunge('INBOX', [])); + } + + public function testGetIdsObReturnsMessageIdSet(): void + { + $this->assertInstanceOf(MessageIdSet::class, $this->createImplementation()->getIdsOb()); + } + + public function testGetIdsObWithParameters(): void + { + $ids = $this->createImplementation()->getIdsOb([1, 2, 3], true); + $this->assertInstanceOf(MessageIdSet::class, $ids); + } + + public function testStubInstanceOfMailboxProtocol(): void + { + $this->assertInstanceOf(MailboxProtocol::class, $this->createImplementation()); + } +} diff --git a/test/Integration/Src/MessageContentTest.php b/test/Integration/Src/MessageContentTest.php new file mode 100644 index 00000000..33778695 --- /dev/null +++ b/test/Integration/Src/MessageContentTest.php @@ -0,0 +1,68 @@ +add("Subject: Test $id"); + return $s; + } + + public function getBodyText(string|int $id = 0): Horde_Stream + { + return new Horde_Stream(); + } + }; + } + + public function testGetFullMsgReturnsHordeStream(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getFullMsg()); + } + + public function testGetHeaderTextWithDefaultId(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getHeaderText()); + } + + public function testGetHeaderTextWithIntId(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getHeaderText(1)); + } + + public function testGetHeaderTextWithStringId(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getHeaderText('1.2')); + } + + public function testGetBodyTextReturnsHordeStream(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getBodyText()); + } + + public function testStreamContainsContent(): void + { + $stub = $this->createImplementation(); + $stream = $stub->getHeaderText(5); + $this->assertStringContainsString('Subject: Test 5', (string) $stream); + } +} diff --git a/test/Integration/Src/MessageIdSetTest.php b/test/Integration/Src/MessageIdSetTest.php new file mode 100644 index 00000000..709ef85d --- /dev/null +++ b/test/Integration/Src/MessageIdSetTest.php @@ -0,0 +1,75 @@ +assertCount(3, $set); + } + + public function testIteratorAggregateContract(): void + { + $set = new StubMessageIdSet([1, 2, 3]); + $collected = []; + foreach ($set as $id) { + $collected[] = $id; + } + $this->assertSame([1, 2, 3], $collected); + } + + public function testIsEmptyWhenEmpty(): void + { + $set = new StubMessageIdSet(); + $this->assertTrue($set->isEmpty()); + } + + public function testIsEmptyWhenNotEmpty(): void + { + $set = new StubMessageIdSet([42]); + $this->assertFalse($set->isEmpty()); + } + + public function testToArrayReturnsArray(): void + { + $set = new StubMessageIdSet([1, 2, 3]); + $this->assertSame([1, 2, 3], $set->toArray()); + } + + public function testToStringReturnsString(): void + { + $set = new StubMessageIdSet([1, 2, 3]); + $this->assertSame('1,2,3', (string) $set); + } + + public function testStringIdSet(): void + { + $set = new StubMessageIdSet(['abc', 'def']); + $this->assertCount(2, $set); + $this->assertSame(['abc', 'def'], $set->toArray()); + $this->assertSame('abc,def', (string) $set); + $this->assertFalse($set->isEmpty()); + } + + public function testCountZeroWhenEmpty(): void + { + $set = new StubMessageIdSet(); + $this->assertSame(0, count($set)); + } + + public function testInstanceOfMessageIdSet(): void + { + $set = new StubMessageIdSet(); + $this->assertInstanceOf(MessageIdSet::class, $set); + } +} diff --git a/test/Integration/Src/MessageMetadataTest.php b/test/Integration/Src/MessageMetadataTest.php new file mode 100644 index 00000000..0186407e --- /dev/null +++ b/test/Integration/Src/MessageMetadataTest.php @@ -0,0 +1,103 @@ +uid; + } + public function getFlags(): array + { + return $this->flags; + } + public function getSize(): int + { + return $this->size; + } + public function getImapDate(): DateTimeImmutable + { + return $this->date; + } + public function getSeq(): ?int + { + return $this->seq; + } + public function getModSeq(): ?int + { + return $this->modSeq; + } + }; + } + + public function testAllMethodReturnTypes(): void + { + $stub = $this->createImplementation(); + + $this->assertSame(42, $stub->getUid()); + $this->assertSame(['\\Seen'], $stub->getFlags()); + $this->assertSame(1024, $stub->getSize()); + $this->assertSame('2026-01-15 10:30:00', $stub->getImapDate()->format('Y-m-d H:i:s')); + $this->assertSame(7, $stub->getSeq()); + $this->assertSame(12345, $stub->getModSeq()); + } + + public function testStringUid(): void + { + $stub = $this->createImplementation(uid: 'msg-abc'); + $this->assertIsString($stub->getUid()); + $this->assertSame('msg-abc', $stub->getUid()); + } + + public function testNullableSeq(): void + { + $stub = $this->createImplementation(seq: null); + $this->assertNull($stub->getSeq()); + } + + public function testNullableModSeq(): void + { + $stub = $this->createImplementation(modSeq: null); + $this->assertNull($stub->getModSeq()); + } + + public function testEmptyFlags(): void + { + $stub = $this->createImplementation(flags: []); + $this->assertSame([], $stub->getFlags()); + } + + public function testDateTimeImmutableType(): void + { + $stub = $this->createImplementation(); + $this->assertInstanceOf(DateTimeImmutable::class, $stub->getImapDate()); + } +} diff --git a/test/Integration/Src/ParsedAccessTest.php b/test/Integration/Src/ParsedAccessTest.php new file mode 100644 index 00000000..2e3edee5 --- /dev/null +++ b/test/Integration/Src/ParsedAccessTest.php @@ -0,0 +1,69 @@ +assertIsObject($this->createImplementation()->getEnvelope()); + } + + public function testGetHeadersReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->getHeaders('from')); + } + + public function testGetHeadersIteratorReturnsGenerator(): void + { + $gen = $this->createImplementation()->getHeadersIterator('to'); + $this->assertInstanceOf(Generator::class, $gen); + } + + public function testGetStructureReturnsObject(): void + { + $this->assertIsObject($this->createImplementation()->getStructure()); + } + + public function testGetHeadersIteratorYieldsMultipleHeaders(): void + { + $items = iterator_to_array($this->createImplementation()->getHeadersIterator('cc')); + $this->assertCount(3, $items); + } +} diff --git a/test/Integration/Src/PartAccessTest.php b/test/Integration/Src/PartAccessTest.php new file mode 100644 index 00000000..34787b48 --- /dev/null +++ b/test/Integration/Src/PartAccessTest.php @@ -0,0 +1,76 @@ + new stdClass(); + yield '1.1' => new stdClass(); + } + }; + } + + public function testGetBodyPartReturnsHordeStream(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getBodyPart('1.1')); + } + + public function testGetMimeHeaderReturnsHordeStream(): void + { + $this->assertInstanceOf(Horde_Stream::class, $this->createImplementation()->getMimeHeader('1')); + } + + public function testGetPartsReturnsGenerator(): void + { + $gen = $this->createImplementation()->getParts(); + $this->assertInstanceOf(Generator::class, $gen); + $this->assertCount(2, iterator_to_array($gen)); + } + + public function testGetPartsEmptyGenerator(): void + { + $stub = new class implements PartAccess { + public function getBodyPart(string $id): Horde_Stream + { + return new Horde_Stream(); + } + + public function getMimeHeader(string $id): Horde_Stream + { + return new Horde_Stream(); + } + + public function getParts(): Generator + { + yield from []; + } + }; + + $this->assertSame([], iterator_to_array($stub->getParts())); + } +} diff --git a/test/Integration/Src/PasswordInterfaceTest.php b/test/Integration/Src/PasswordInterfaceTest.php new file mode 100644 index 00000000..1abf562d --- /dev/null +++ b/test/Integration/Src/PasswordInterfaceTest.php @@ -0,0 +1,38 @@ +assertInstanceOf(PasswordInterface::class, $pw); + $this->assertSame('s3cret', $pw->getPassword()); + } + + public function testEmptyPasswordReturn(): void + { + $pw = new class implements PasswordInterface { + public function getPassword(): string + { + return ''; + } + }; + + $this->assertSame('', $pw->getPassword()); + } +} diff --git a/test/Stub/Base.php b/test/Stub/Base.php index d8a4dae0..1b0fb4ed 100644 --- a/test/Stub/Base.php +++ b/test/Stub/Base.php @@ -41,35 +41,25 @@ public function initCache($current = false): bool return $this->_initCache($current); } - protected function _initCapability() - { - } + protected function _initCapability() {} - protected function _noop() - { - } + protected function _noop() {} protected function _getNamespaces() { return []; } - protected function _connect() - { - } + protected function _connect() {} protected function _login() { return true; } - protected function _logout() - { - } + protected function _logout() {} - protected function _sendID($info) - { - } + protected function _sendID($info) {} protected function _getID() { @@ -89,30 +79,24 @@ protected function _getLanguage($list) protected function _openMailbox( Horde_Imap_Client_Mailbox $mailbox, $mode - ) { - } + ) {} protected function _createMailbox( Horde_Imap_Client_Mailbox $mailbox, $opts - ) { - } + ) {} - protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox) - { - } + protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox) {} protected function _renameMailbox( Horde_Imap_Client_Mailbox $old, Horde_Imap_Client_Mailbox $new - ) { - } + ) {} protected function _subscribeMailbox( Horde_Imap_Client_Mailbox $mailbox, $subscribe - ) { - } + ) {} protected function _listMailboxes($pattern, $mode, $options) { @@ -132,47 +116,35 @@ protected function _append( return new Horde_Imap_Client_Ids(); } - protected function _check() - { - } + protected function _check() {} - protected function _close($options) - { - } + protected function _close($options) {} - protected function _expunge($options) - { - } + protected function _expunge($options) {} protected function _search($query, $options) { return []; } - protected function _setComparator($comparator) - { - } + protected function _setComparator($comparator) {} protected function _getComparator() { return []; } - protected function _thread($options) - { - } + protected function _thread($options) {} protected function _fetch( Horde_Imap_Client_Fetch_Results $results, $queries - ) { - } + ) {} protected function _vanished( $modseq, Horde_Imap_Client_Ids $ids - ) { - } + ) {} protected function _store($options) { @@ -189,8 +161,7 @@ protected function _copy( protected function _setQuota( Horde_Imap_Client_Mailbox $root, $resources - ) { - } + ) {} protected function _getQuota(Horde_Imap_Client_Mailbox $root) { @@ -211,24 +182,19 @@ protected function _setACL( Horde_Imap_Client_Mailbox $mailbox, $identifier, $options - ) { - } + ) {} protected function _deleteACL( Horde_Imap_Client_Mailbox $mailbox, $identifier - ) { - } + ) {} protected function _listACLRights( Horde_Imap_Client_Mailbox $mailbox, $identifier - ) { - } + ) {} - protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox) - { - } + protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox) {} protected function _getMetadata( Horde_Imap_Client_Mailbox $mailbox, @@ -241,6 +207,5 @@ protected function _getMetadata( protected function _setMetadata( Horde_Imap_Client_Mailbox $mailbox, $data - ) { - } + ) {} } diff --git a/test/Stub/StubMessageIdSet.php b/test/Stub/StubMessageIdSet.php new file mode 100644 index 00000000..a8844f16 --- /dev/null +++ b/test/Stub/StubMessageIdSet.php @@ -0,0 +1,45 @@ + $ids */ + public function __construct( + private readonly array $ids = [], + ) {} + + public function isEmpty(): bool + { + return $this->ids === []; + } + + public function toArray(): array + { + return $this->ids; + } + + public function __toString(): string + { + return implode(',', array_map('strval', $this->ids)); + } + + public function count(): int + { + return count($this->ids); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->ids); + } +} diff --git a/test/Unit/Base/BaseConstructorTest.php b/test/Unit/Base/BaseConstructorTest.php index 0075df71..a2b668dd 100644 --- a/test/Unit/Base/BaseConstructorTest.php +++ b/test/Unit/Base/BaseConstructorTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; use Horde\Imap\Client\Test\Stub\Base; use Horde_Imap_Client; +use Horde_Imap_Client_Cache_Backend; /** * Tests for Horde_Imap_Client_Base constructor logic. @@ -118,7 +119,7 @@ public function testCacheFieldsEmptyWhenNoCacheSet(): void public function testCacheFieldsDefaultWhenBackendProvided(): void { - $backend = $this->createMock(\Horde_Imap_Client_Cache_Backend::class); + $backend = $this->createMock(Horde_Imap_Client_Cache_Backend::class); $ob = $this->create([ 'cache' => ['backend' => $backend], ]); diff --git a/test/Unit/Src/ConnectionConfigEdgeCaseTest.php b/test/Unit/Src/ConnectionConfigEdgeCaseTest.php new file mode 100644 index 00000000..080c52d9 --- /dev/null +++ b/test/Unit/Src/ConnectionConfigEdgeCaseTest.php @@ -0,0 +1,104 @@ +expectException(Error::class); + $cfg->username = 'other'; + } + + public function testReadonlyPasswordThrowsError(): void + { + $cfg = new ConnectionConfig('user', 'pass'); + $this->expectException(Error::class); + $cfg->password = 'other'; + } + + public function testReadonlyPortThrowsError(): void + { + $cfg = new ConnectionConfig('user', 'pass', port: 993); + $this->expectException(Error::class); + $cfg->port = 143; + } + + public function testEmptyUsername(): void + { + $cfg = new ConnectionConfig('', 'pass'); + $this->assertSame('', $cfg->username); + } + + public function testEmptyPassword(): void + { + $cfg = new ConnectionConfig('user', ''); + $this->assertSame('', $cfg->password); + } + + public function testPortZero(): void + { + $cfg = new ConnectionConfig('user', 'pass', port: 0); + $this->assertSame(0, $cfg->port); + } + + public function testNegativeTimeout(): void + { + $cfg = new ConnectionConfig('user', 'pass', timeout: -1); + $this->assertSame(-1, $cfg->timeout); + } + + public function testContextWithNestedArrays(): void + { + $context = ['ssl' => ['verify_peer' => false, 'cafile' => '/etc/ssl/certs/ca.pem']]; + $cfg = new ConnectionConfig('user', 'pass', context: $context); + $this->assertSame($context, $cfg->context); + } + + public function testCapabilityIgnorePreservesOrder(): void + { + $ignore = ['CONDSTORE', 'QRESYNC', 'IDLE']; + $cfg = new ConnectionConfig('user', 'pass', capabilityIgnore: $ignore); + $this->assertSame($ignore, $cfg->capabilityIgnore); + } + + public function testIdArrayPreservesKeys(): void + { + $id = ['name' => 'Horde', 'version' => '6.0']; + $cfg = new ConnectionConfig('user', 'pass', id: $id); + $this->assertSame($id, $cfg->id); + } + + public function testLangMultipleEntries(): void + { + $cfg = new ConnectionConfig('user', 'pass', lang: ['en', 'de', 'fr']); + $this->assertCount(3, $cfg->lang); + $this->assertSame(['en', 'de', 'fr'], $cfg->lang); + } + + public static function secureModeProvider(): array + { + return array_map( + fn(SecureMode $m) => [$m], + SecureMode::cases(), + ); + } + + #[DataProvider('secureModeProvider')] + public function testAllSecureModes(SecureMode $mode): void + { + $cfg = new ConnectionConfig('user', 'pass', secure: $mode); + $this->assertSame($mode, $cfg->secure); + } +} diff --git a/test/Unit/Src/ConnectionConfigTest.php b/test/Unit/Src/ConnectionConfigTest.php new file mode 100644 index 00000000..cf56a11b --- /dev/null +++ b/test/Unit/Src/ConnectionConfigTest.php @@ -0,0 +1,71 @@ +assertSame('user', $cfg->username); + $this->assertSame('pass', $cfg->password); + $this->assertSame('localhost', $cfg->hostspec); + $this->assertNull($cfg->port); + $this->assertSame(SecureMode::None, $cfg->secure); + $this->assertSame(30, $cfg->timeout); + $this->assertSame(120, $cfg->readTimeout); + $this->assertNull($cfg->context); + $this->assertSame([], $cfg->capabilityIgnore); + $this->assertNull($cfg->id); + $this->assertSame([], $cfg->lang); + } + + public function testFullConstruction(): void + { + $cfg = new ConnectionConfig( + username: 'admin', + password: 'secret', + hostspec: 'mail.example.com', + port: 993, + secure: SecureMode::Ssl, + timeout: 60, + readTimeout: 300, + context: ['ssl' => ['verify_peer' => false]], + capabilityIgnore: ['CONDSTORE'], + id: ['name' => 'TestClient'], + lang: ['en'], + ); + + $this->assertSame('admin', $cfg->username); + $this->assertSame(993, $cfg->port); + $this->assertSame(SecureMode::Ssl, $cfg->secure); + $this->assertSame(60, $cfg->timeout); + $this->assertSame(['CONDSTORE'], $cfg->capabilityIgnore); + $this->assertSame(['en'], $cfg->lang); + } + + public function testPasswordInterface(): void + { + $pw = new class implements PasswordInterface { + public function getPassword(): string + { + return 'dynamic-pass'; + } + }; + + $cfg = new ConnectionConfig('user', $pw); + + $this->assertInstanceOf(PasswordInterface::class, $cfg->password); + $this->assertSame('dynamic-pass', $cfg->password->getPassword()); + } +} diff --git a/test/Unit/Src/EnumTest.php b/test/Unit/Src/EnumTest.php new file mode 100644 index 00000000..d06684a3 --- /dev/null +++ b/test/Unit/Src/EnumTest.php @@ -0,0 +1,152 @@ +assertSame('', SecureMode::None->value); + $this->assertSame('ssl', SecureMode::Ssl->value); + $this->assertSame('tls', SecureMode::Tls->value); + $this->assertSame('tlsv1', SecureMode::Tlsv1->value); + $this->assertCount(4, SecureMode::cases()); + } + + public function testOpenModeValues(): void + { + $this->assertSame(1, OpenMode::Readonly->value); + $this->assertSame(2, OpenMode::ReadWrite->value); + $this->assertSame(3, OpenMode::Auto->value); + } + + public function testMailboxListModeValues(): void + { + $this->assertSame(1, MailboxListMode::Subscribed->value); + $this->assertSame(4, MailboxListMode::All->value); + $this->assertCount(5, MailboxListMode::cases()); + } + + public function testSortCriteriaValues(): void + { + $this->assertSame(1, SortCriteria::Arrival->value); + $this->assertSame(3, SortCriteria::Date->value); + $this->assertSame(13, SortCriteria::Relevancy->value); + $this->assertSame(15, SortCriteria::DisplayToFallback->value); + $this->assertCount(15, SortCriteria::cases()); + } + + public function testSearchResultTypeValues(): void + { + $this->assertSame(1, SearchResultType::Count->value); + $this->assertSame(2, SearchResultType::Match->value); + $this->assertSame(6, SearchResultType::Relevancy->value); + $this->assertCount(6, SearchResultType::cases()); + } + + public function testThreadAlgorithmValues(): void + { + $this->assertSame(1, ThreadAlgorithm::OrderedSubject->value); + $this->assertSame(2, ThreadAlgorithm::References->value); + $this->assertSame(3, ThreadAlgorithm::Refs->value); + } + + public function testAclRightValues(): void + { + $this->assertSame('l', AclRight::Lookup->value); + $this->assertSame('r', AclRight::Read->value); + $this->assertSame('a', AclRight::Administer->value); + $this->assertCount(11, AclRight::cases()); + } + + public function testAclRightOmitsDeprecated(): void + { + $values = array_map(fn(AclRight $r) => $r->value, AclRight::cases()); + $this->assertNotContains('c', $values); + $this->assertNotContains('d', $values); + } + + public function testSystemFlagValues(): void + { + $this->assertSame('\\answered', SystemFlag::Answered->value); + $this->assertSame('\\seen', SystemFlag::Seen->value); + $this->assertSame('$mdnsent', SystemFlag::MdnSent->value); + $this->assertSame('$junk', SystemFlag::Junk->value); + $this->assertCount(10, SystemFlag::cases()); + } + + public function testSpecialUseValues(): void + { + $this->assertSame('\\All', SpecialUse::All->value); + $this->assertSame('\\Trash', SpecialUse::Trash->value); + $this->assertCount(7, SpecialUse::cases()); + } + + /** + * All int-backed enums must be constructable from their value. + */ + public static function intBackedEnumProvider(): array + { + return [ + [OpenMode::class, 1, 'Readonly'], + [MailboxListMode::class, 4, 'All'], + [SortCriteria::class, 3, 'Date'], + [SearchResultType::class, 2, 'Match'], + [ThreadAlgorithm::class, 2, 'References'], + ]; + } + + #[DataProvider('intBackedEnumProvider')] + public function testFromInt(string $enum, int $value, string $expectedName): void + { + $case = $enum::from($value); + $this->assertSame($expectedName, $case->name); + } + + /** + * All string-backed enums must be constructable from their value. + */ + public static function stringBackedEnumProvider(): array + { + return [ + [SecureMode::class, 'ssl', 'Ssl'], + [AclRight::class, 'l', 'Lookup'], + [SystemFlag::class, '\\answered', 'Answered'], + [SpecialUse::class, '\\Drafts', 'Drafts'], + ]; + } + + #[DataProvider('stringBackedEnumProvider')] + public function testFromString(string $enum, string $value, string $expectedName): void + { + $case = $enum::from($value); + $this->assertSame($expectedName, $case->name); + } +} diff --git a/test/Unit/Src/EventHierarchyTest.php b/test/Unit/Src/EventHierarchyTest.php new file mode 100644 index 00000000..a003f0fe --- /dev/null +++ b/test/Unit/Src/EventHierarchyTest.php @@ -0,0 +1,97 @@ + 'mail.example.com']); + $this->assertSame('connected', $event->getMessage()); + $this->assertSame(['host' => 'mail.example.com'], $event->getContext()); + } + + public function testImapEventDefaults(): void + { + $event = new ConnectionClosed(); + $this->assertSame('', $event->getMessage()); + $this->assertSame([], $event->getContext()); + } + + public static function lifecycleEventProvider(): array + { + return [ + [ConnectionEstablished::class], + [ConnectionClosed::class], + [AuthenticationSucceeded::class], + [AuthenticationFailed::class], + [CapabilityNegotiated::class], + [MailboxSelected::class], + [MailboxExpunged::class], + [AlertReceived::class], + [SlowCommand::class], + ]; + } + + #[DataProvider('lifecycleEventProvider')] + public function testLifecycleEventsExtendImapEvent(string $class): void + { + $event = new $class(); + $this->assertInstanceOf(ImapEvent::class, $event); + $this->assertNotInstanceOf(DiagnosticEvent::class, $event); + } + + public static function diagnosticEventProvider(): array + { + return [ + [CacheStored::class], + [CacheRetrieved::class], + [CacheDeleted::class], + [CapabilityIgnored::class], + ]; + } + + #[DataProvider('diagnosticEventProvider')] + public function testDiagnosticEventsExtendBoth(string $class): void + { + $event = new $class(); + $this->assertInstanceOf(DiagnosticEvent::class, $event); + $this->assertInstanceOf(ImapEvent::class, $event); + } +} diff --git a/test/Unit/Src/ExceptionHierarchyTest.php b/test/Unit/Src/ExceptionHierarchyTest.php new file mode 100644 index 00000000..797ba6c4 --- /dev/null +++ b/test/Unit/Src/ExceptionHierarchyTest.php @@ -0,0 +1,124 @@ +assertInstanceOf(RuntimeException::class, $e); + } + + public function testConnectionExtendsBase(): void + { + $e = new ConnectionException('conn'); + $this->assertInstanceOf(MailboxProtocolException::class, $e); + } + + public function testAuthenticationExtendsBase(): void + { + $e = new AuthenticationException('auth'); + $this->assertInstanceOf(MailboxProtocolException::class, $e); + } + + public function testImapProtocolExtendsBase(): void + { + $e = new ImapProtocolException('imap'); + $this->assertInstanceOf(MailboxProtocolException::class, $e); + } + + public function testMailboxNotFoundExtendsImapProtocol(): void + { + $e = new MailboxNotFoundException('mbox'); + $this->assertInstanceOf(ImapProtocolException::class, $e); + $this->assertInstanceOf(MailboxProtocolException::class, $e); + } + + public function testCapabilityNotSupportedExtendsImapProtocol(): void + { + $e = new CapabilityNotSupportedException('cap'); + $this->assertInstanceOf(ImapProtocolException::class, $e); + } + + public function testPop3ProtocolExtendsBase(): void + { + $e = new Pop3ProtocolException('pop3'); + $this->assertInstanceOf(MailboxProtocolException::class, $e); + } + + public function testServerResponseCarriesData(): void + { + $e = new ServerResponseException( + message: 'NO [TRYCREATE]', + code: 0, + previous: null, + command: 'APPEND', + status: 'NO', + responseText: '[TRYCREATE] Mailbox does not exist', + ); + + $this->assertSame('APPEND', $e->command); + $this->assertSame('NO', $e->status); + $this->assertSame('[TRYCREATE] Mailbox does not exist', $e->responseText); + $this->assertSame('NO [TRYCREATE]', $e->getMessage()); + $this->assertInstanceOf(MailboxProtocolException::class, $e); + } + + public function testServerResponseDefaults(): void + { + $e = new ServerResponseException(); + $this->assertNull($e->command); + $this->assertNull($e->status); + $this->assertNull($e->responseText); + $this->assertSame('', $e->getMessage()); + } + + /** + * Catching MailboxProtocolException must catch all subtypes. + */ + public function testBroadCatchCoversAll(): void + { + $exceptions = [ + new ConnectionException(), + new AuthenticationException(), + new ImapProtocolException(), + new MailboxNotFoundException(), + new CapabilityNotSupportedException(), + new Pop3ProtocolException(), + new ServerResponseException(), + ]; + + foreach ($exceptions as $e) { + $caught = false; + try { + throw $e; + } catch (MailboxProtocolException) { + $caught = true; + } + $this->assertTrue($caught, get_class($e) . ' not caught by MailboxProtocolException'); + } + } +} diff --git a/test/Unit/Src/FilteredEventDispatcherEdgeCaseTest.php b/test/Unit/Src/FilteredEventDispatcherEdgeCaseTest.php new file mode 100644 index 00000000..a6d18bb2 --- /dev/null +++ b/test/Unit/Src/FilteredEventDispatcherEdgeCaseTest.php @@ -0,0 +1,165 @@ + [new CacheStored()], + 'CacheRetrieved' => [new CacheRetrieved()], + 'CacheDeleted' => [new CacheDeleted()], + 'CapabilityIgnored' => [new CapabilityIgnored()], + ]; + } + + #[DataProvider('diagnosticSubclassProvider')] + public function testSuppressDiagnosticBaseSuppressesSubclass(object $event): void + { + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->never())->method('dispatch'); + + $dispatcher = new FilteredEventDispatcher($inner); + $result = $dispatcher->dispatch($event); + $this->assertSame($event, $result); + } + + public function testMultipleSuppressClasses(): void + { + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->never())->method('dispatch'); + + $dispatcher = new FilteredEventDispatcher($inner, [ + ConnectionEstablished::class, + ConnectionClosed::class, + ]); + + $dispatcher->dispatch(new ConnectionEstablished()); + $dispatcher->dispatch(new ConnectionClosed()); + } + + public function testMultipleSuppressLetOtherEventsThrough(): void + { + $event = new CacheStored(); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->once()) + ->method('dispatch') + ->with($event) + ->willReturn($event); + + $dispatcher = new FilteredEventDispatcher($inner, [ + ConnectionEstablished::class, + ConnectionClosed::class, + ]); + + $dispatcher->dispatch($event); + } + + public function testInnerDispatcherExceptionPropagates(): void + { + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->method('dispatch')->willThrowException(new RuntimeException('bus failure')); + + $dispatcher = new FilteredEventDispatcher($inner); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('bus failure'); + $dispatcher->dispatch(new ConnectionEstablished()); + } + + public function testNonImapEventPassesThroughWithDefaultSuppress(): void + { + $obj = new stdClass(); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->once()) + ->method('dispatch') + ->with($obj) + ->willReturn($obj); + + $dispatcher = new FilteredEventDispatcher($inner); + $result = $dispatcher->dispatch($obj); + $this->assertSame($obj, $result); + } + + public function testSuppressReturnsSameEventInstance(): void + { + $inner = $this->createMock(EventDispatcherInterface::class); + $event = new CacheStored('msg'); + + $dispatcher = new FilteredEventDispatcher($inner); + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $result); + } + + public function testPassthroughReturnsSameEventInstance(): void + { + $event = new ConnectionEstablished(); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->method('dispatch')->willReturn($event); + + $dispatcher = new FilteredEventDispatcher($inner); + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $result); + } + + public function testInnerDispatcherCanReturnDifferentObject(): void + { + $input = new ConnectionEstablished('in'); + $output = new ConnectionEstablished('out'); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->method('dispatch')->willReturn($output); + + $dispatcher = new FilteredEventDispatcher($inner); + $result = $dispatcher->dispatch($input); + + $this->assertSame($output, $result); + $this->assertNotSame($input, $result); + } + + public function testDispatcherImplementsPsrInterface(): void + { + $inner = $this->createMock(EventDispatcherInterface::class); + $dispatcher = new FilteredEventDispatcher($inner); + + $this->assertInstanceOf(EventDispatcherInterface::class, $dispatcher); + } + + public function testEmptySuppressWithDiagnosticEventPassesThrough(): void + { + $event = new CapabilityIgnored(); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->once()) + ->method('dispatch') + ->with($event) + ->willReturn($event); + + $dispatcher = new FilteredEventDispatcher($inner, []); + $dispatcher->dispatch($event); + } +} diff --git a/test/Unit/Src/FilteredEventDispatcherTest.php b/test/Unit/Src/FilteredEventDispatcherTest.php new file mode 100644 index 00000000..c42cb04e --- /dev/null +++ b/test/Unit/Src/FilteredEventDispatcherTest.php @@ -0,0 +1,74 @@ +createMock(EventDispatcherInterface::class); + $inner->expects($this->never())->method('dispatch'); + + $dispatcher = new FilteredEventDispatcher($inner); + $event = new CacheStored('stored', ['key' => 'uid:42']); + + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $result); + } + + public function testPassesLifecycleEventThrough(): void + { + $event = new ConnectionEstablished('connected'); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->once()) + ->method('dispatch') + ->with($event) + ->willReturn($event); + + $dispatcher = new FilteredEventDispatcher($inner); + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $result); + } + + public function testEmptySuppressPassesEverything(): void + { + $event = new CacheStored('stored'); + + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->once()) + ->method('dispatch') + ->with($event) + ->willReturn($event); + + $dispatcher = new FilteredEventDispatcher($inner, []); + $result = $dispatcher->dispatch($event); + + $this->assertSame($event, $result); + } + + public function testCustomSuppressList(): void + { + $inner = $this->createMock(EventDispatcherInterface::class); + $inner->expects($this->never())->method('dispatch'); + + $dispatcher = new FilteredEventDispatcher($inner, [ConnectionEstablished::class]); + $event = new ConnectionEstablished('connected'); + + $result = $dispatcher->dispatch($event); + $this->assertSame($event, $result); + } +} diff --git a/test/Unit/Src/ImapEventEdgeCaseTest.php b/test/Unit/Src/ImapEventEdgeCaseTest.php new file mode 100644 index 00000000..bdd979d6 --- /dev/null +++ b/test/Unit/Src/ImapEventEdgeCaseTest.php @@ -0,0 +1,156 @@ + ['host' => 'mx.example.com', 'port' => 993], + 'tls' => ['version' => 'TLSv1.3'], + ]; + $event = new ConnectionEstablished('connected', $ctx); + + $this->assertSame('mx.example.com', $event->getContext()['server']['host']); + $this->assertSame(993, $event->getContext()['server']['port']); + $this->assertSame('TLSv1.3', $event->getContext()['tls']['version']); + } + + public function testContextWithNumericKeys(): void + { + $ctx = [0 => 'first', 1 => 'second']; + $event = new ConnectionEstablished('msg', $ctx); + $this->assertSame($ctx, $event->getContext()); + } + + public function testContextWithMixedValueTypes(): void + { + $ctx = [ + 'int' => 42, + 'float' => 3.14, + 'bool' => true, + 'null' => null, + 'array' => [1, 2, 3], + ]; + $event = new ConnectionEstablished('msg', $ctx); + $result = $event->getContext(); + + $this->assertSame(42, $result['int']); + $this->assertSame(3.14, $result['float']); + $this->assertTrue($result['bool']); + $this->assertNull($result['null']); + $this->assertSame([1, 2, 3], $result['array']); + } + + public function testEmptyStringMessageExplicit(): void + { + $event = new ConnectionEstablished(''); + $this->assertSame('', $event->getMessage()); + } + + public function testLongMessage(): void + { + $msg = str_repeat('a', 10000); + $event = new ConnectionEstablished($msg); + $this->assertSame(10000, strlen($event->getMessage())); + } + + public function testGetMessageReturnsSameValue(): void + { + $event = new ConnectionEstablished('test'); + $this->assertSame($event->getMessage(), $event->getMessage()); + } + + public function testGetContextReturnsSameStructure(): void + { + $event = new ConnectionEstablished('test', ['k' => 'v']); + $this->assertSame($event->getContext(), $event->getContext()); + } + + public function testDiagnosticEventInheritsMessageAndContext(): void + { + $event = new CacheStored('cache hit', ['key' => 'uid:1']); + $this->assertSame('cache hit', $event->getMessage()); + $this->assertSame(['key' => 'uid:1'], $event->getContext()); + } + + public static function lifecycleEventClassProvider(): array + { + $classes = [ + ConnectionEstablished::class, + ConnectionClosed::class, + AuthenticationSucceeded::class, + AuthenticationFailed::class, + CapabilityNegotiated::class, + MailboxSelected::class, + MailboxExpunged::class, + AlertReceived::class, + SlowCommand::class, + ]; + $pairs = []; + for ($i = 0; $i < count($classes); $i++) { + for ($j = $i + 1; $j < count($classes); $j++) { + $a = (new ReflectionClass($classes[$i]))->getShortName(); + $b = (new ReflectionClass($classes[$j]))->getShortName(); + $pairs["$a vs $b"] = [$classes[$i], $classes[$j]]; + } + } + return $pairs; + } + + #[DataProvider('lifecycleEventClassProvider')] + public function testEachLifecycleEventIsDistinctClass(string $a, string $b): void + { + $this->assertNotSame($a, $b); + } + + public static function diagnosticEventClassProvider(): array + { + $classes = [ + CacheStored::class, + CacheRetrieved::class, + CacheDeleted::class, + CapabilityIgnored::class, + ]; + $pairs = []; + for ($i = 0; $i < count($classes); $i++) { + for ($j = $i + 1; $j < count($classes); $j++) { + $a = (new ReflectionClass($classes[$i]))->getShortName(); + $b = (new ReflectionClass($classes[$j]))->getShortName(); + $pairs["$a vs $b"] = [$classes[$i], $classes[$j]]; + } + } + return $pairs; + } + + #[DataProvider('diagnosticEventClassProvider')] + public function testEachDiagnosticEventIsDistinctClass(string $a, string $b): void + { + $this->assertNotSame($a, $b); + } +} diff --git a/test/Unit/Src/ServerResponseExceptionTest.php b/test/Unit/Src/ServerResponseExceptionTest.php new file mode 100644 index 00000000..74fd6eaf --- /dev/null +++ b/test/Unit/Src/ServerResponseExceptionTest.php @@ -0,0 +1,116 @@ +assertSame($prev, $e->getPrevious()); + $this->assertSame('root cause', $e->getPrevious()->getMessage()); + } + + public function testErrorCode(): void + { + $e = new ServerResponseException('fail', 42); + $this->assertSame(42, $e->getCode()); + } + + public function testAllPropertiesSimultaneously(): void + { + $e = new ServerResponseException( + message: 'NO access denied', + code: 99, + previous: null, + command: 'SELECT', + status: 'NO', + responseText: 'Access denied', + ); + + $this->assertSame('NO access denied', $e->getMessage()); + $this->assertSame(99, $e->getCode()); + $this->assertNull($e->getPrevious()); + $this->assertSame('SELECT', $e->command); + $this->assertSame('NO', $e->status); + $this->assertSame('Access denied', $e->responseText); + } + + public function testCommandNullByDefault(): void + { + $e = new ServerResponseException('msg'); + $this->assertNull($e->command); + } + + public function testStatusNullByDefault(): void + { + $e = new ServerResponseException('msg'); + $this->assertNull($e->status); + } + + public function testResponseTextNullByDefault(): void + { + $e = new ServerResponseException('msg'); + $this->assertNull($e->responseText); + } + + public function testReadonlyCommandThrowsError(): void + { + $e = new ServerResponseException('msg', 0, null, 'SELECT'); + $this->expectException(Error::class); + $e->command = 'FETCH'; + } + + public function testReadonlyStatusThrowsError(): void + { + $e = new ServerResponseException('msg', 0, null, null, 'NO'); + $this->expectException(Error::class); + $e->status = 'OK'; + } + + public function testReadonlyResponseTextThrowsError(): void + { + $e = new ServerResponseException('msg', 0, null, null, null, 'text'); + $this->expectException(Error::class); + $e->responseText = 'other'; + } + + public function testEmptyStringProperties(): void + { + $e = new ServerResponseException('', 0, null, '', '', ''); + $this->assertSame('', $e->command); + $this->assertSame('', $e->status); + $this->assertSame('', $e->responseText); + $this->assertSame('', $e->getMessage()); + } + + public function testIsThrowable(): void + { + $this->assertInstanceOf(Throwable::class, new ServerResponseException()); + } + + public function testCatchByMailboxProtocolException(): void + { + $caught = false; + try { + throw new ServerResponseException('test'); + } catch (MailboxProtocolException $e) { + $caught = true; + $this->assertSame('test', $e->getMessage()); + } + $this->assertTrue($caught); + } +} From 3f6f38ba114f70d0b853c5ab2c10ffd58e727571 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Sun, 12 Apr 2026 20:11:31 +0200 Subject: [PATCH 4/4] chore: metadata .horde.yml add PSR dependencies directly --- .horde.yml | 3 +++ composer.json | 70 ++++++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/.horde.yml b/.horde.yml index 563ffaf4..d4e88859 100644 --- a/.horde.yml +++ b/.horde.yml @@ -28,6 +28,7 @@ dependencies: required: php: ^8.1 composer: + horde/eventdispatcher: ^1 horde/exception: ^3 horde/mail: ^3 horde/mime: ^3 @@ -51,6 +52,8 @@ dependencies: horde/pack: ^2 horde/stringprep: ^2 horde/support: ^3 + psr/event-dispatcher: ^1 + psr/simple-cache: ^3 optional: composer: horde/cache: ^3 diff --git a/composer.json b/composer.json index d2e6b0dc..61bf55a9 100644 --- a/composer.json +++ b/composer.json @@ -17,47 +17,53 @@ "time": "2026-04-12", "repositories": [], "require": { - "horde/horde-installer-plugin": "dev-FRAMEWORK_6_0 || ^3 || ^2", + "horde/horde-installer-plugin": "^3 || ^2", "php": "^8.1", - "horde/exception": "^3 || dev-FRAMEWORK_6_0", - "horde/mail": "^3 || dev-FRAMEWORK_6_0", - "horde/mime": "^3 || dev-FRAMEWORK_6_0", - "horde/socket_client": "^3 || dev-FRAMEWORK_6_0", - "horde/stream": "^2 || dev-FRAMEWORK_6_0", - "horde/secret": "^3 || dev-FRAMEWORK_6_0", - "horde/stream_filter": "^3 || dev-FRAMEWORK_6_0", - "horde/translation": "^3 || dev-FRAMEWORK_6_0", - "horde/util": "^3 || dev-FRAMEWORK_6_0", + "horde/eventdispatcher": "^1", + "horde/exception": "^3", + "horde/mail": "^3", + "horde/mime": "^3", + "horde/socket_client": "^3", + "horde/stream": "^2", + "horde/secret": "^3", + "horde/stream_filter": "^3", + "horde/translation": "^3", + "horde/util": "^3", "ext-hash": "*", "ext-json": "*" }, "require-dev": { - "horde/cache": "^3 || dev-FRAMEWORK_6_0", - "horde/compress_fast": "^2 || dev-FRAMEWORK_6_0", - "horde/crypt_blowfish": "^2 || dev-FRAMEWORK_6_0", - "horde/db": "^3 || dev-FRAMEWORK_6_0", - "horde/hashtable": "^2 || dev-FRAMEWORK_6_0", - "horde/mongo": "^2 || dev-FRAMEWORK_6_0", - "horde/pack": "^2 || dev-FRAMEWORK_6_0", - "horde/stringprep": "^2 || dev-FRAMEWORK_6_0", - "horde/support": "^3 || dev-FRAMEWORK_6_0" + "horde/cache": "^3", + "horde/compress_fast": "^2", + "horde/crypt_blowfish": "^2", + "horde/db": "^3", + "horde/hashtable": "^2", + "horde/mongo": "^2", + "horde/pack": "^2", + "horde/stringprep": "^2", + "horde/support": "^3", + "psr/event-dispatcher": "^1", + "psr/simple-cache": "^3" }, "suggest": { - "horde/cache": "^3 || dev-FRAMEWORK_6_0", - "horde/compress_fast": "^2 || dev-FRAMEWORK_6_0", - "horde/crypt_blowfish": "^2 || dev-FRAMEWORK_6_0", - "horde/db": "^3 || dev-FRAMEWORK_6_0", - "horde/hashtable": "^2 || dev-FRAMEWORK_6_0", - "horde/mongo": "^2 || dev-FRAMEWORK_6_0", - "horde/pack": "^2 || dev-FRAMEWORK_6_0", - "horde/stringprep": "^2 || dev-FRAMEWORK_6_0", - "horde/support": "^3 || dev-FRAMEWORK_6_0", + "horde/cache": "^3", + "horde/compress_fast": "^2", + "horde/crypt_blowfish": "^2", + "horde/db": "^3", + "horde/hashtable": "^2", + "horde/mongo": "^2", + "horde/pack": "^2", + "horde/stringprep": "^2", + "horde/support": "^3", "ext-intl": "*", "ext-mbstring": "*" }, "autoload": { "psr-0": { "Horde_Imap_Client": "lib/" + }, + "psr-4": { + "Horde\\Imap\\Client\\": "src/" } }, "autoload-dev": { @@ -71,9 +77,5 @@ "horde/horde-installer-plugin": true } }, - "extra": { - "branch-alias": { - "dev-FRAMEWORK_6_0": "3.x-dev" - } - } -} \ No newline at end of file + "minimum-stability": "dev" +}