diff --git a/lib/Horde/Imap/Client/Exception.php b/lib/Horde/Imap/Client/Exception.php index 626e86f3..deecf574 100644 --- a/lib/Horde/Imap/Client/Exception.php +++ b/lib/Horde/Imap/Client/Exception.php @@ -261,7 +261,7 @@ class Horde_Imap_Client_Exception extends Horde_Exception_Wrapped * @param string $message Error message (non-translated). * @param int $code Error code. */ - public function __construct($message = null, $code = null) + public function __construct($message = '', $code = 0) { parent::__construct($message, $code); diff --git a/lib/Horde/Imap/Client/Ids.php b/lib/Horde/Imap/Client/Ids.php index e6608fb4..a5dca82f 100644 --- a/lib/Horde/Imap/Client/Ids.php +++ b/lib/Horde/Imap/Client/Ids.php @@ -169,7 +169,7 @@ public function add($ids) } elseif ($add = $this->_resolveIds($ids)) { if (is_array($this->_ids) && !empty($this->_ids)) { foreach ($add as $val) { - $this->_ids[] = $val; + $this->_ids[] = (int) $val; } } else { $this->_ids = $add; @@ -364,10 +364,10 @@ protected function _fromSequenceString($str) $range = explode(':', $val); if (isset($range[1])) { for ($i = min($range), $j = max($range); $i <= $j; ++$i) { - $ids[] = $i; + $ids[] = (int) $i; } } else { - $ids[] = $val; + $ids[] = (int) $val; } } diff --git a/lib/Horde/Imap/Client/Socket.php b/lib/Horde/Imap/Client/Socket.php index f2021689..45873114 100644 --- a/lib/Horde/Imap/Client/Socket.php +++ b/lib/Horde/Imap/Client/Socket.php @@ -3568,21 +3568,29 @@ protected function _parseEnvelope(Horde_Imap_Client_Tokenize $data) return $ret; } - /** - */ protected function _vanished($modseq, Horde_Imap_Client_Ids $ids) { - $pipeline = $this->_pipeline( - $this->_command('UID FETCH')->add([ - strval($ids), + // Unlike _fetchCmd(), sequence IDs are rejected before reaching + // here, so we always issue UID FETCH. Chunk UIDs to avoid + // exceeding the server's command length limit. + + $modifiers = new Horde_Imap_Client_Data_Format_List([ + 'VANISHED', + 'CHANGEDSINCE', + new Horde_Imap_Client_Data_Format_Number($modseq), + ]); + + $pipeline = $this->_pipeline(); + + foreach ($ids->split($this->_capability()->cmdlength) as $val) { + $cmd = $this->_command('UID FETCH')->add([ + $val, 'UID', - new Horde_Imap_Client_Data_Format_List([ - 'VANISHED', - 'CHANGEDSINCE', - new Horde_Imap_Client_Data_Format_Number($modseq), - ]), - ]) - ); + $modifiers, + ]); + $pipeline->add($cmd); + } + $pipeline->data['vanished'] = $this->getIdsOb(); return $this->_sendCmd($pipeline)->data['vanished']; @@ -4339,13 +4347,15 @@ protected function _sendCmdChunk($pipeline, $chunk) $this->_temp['logout'] = true; $this->logout(); throw $e; - } - /* For all other issues, catch and store exception; don't - * throw until all input is read since we need to clear - * incoming queue. (For now, only store first exception.) */ - if (is_null($exception)) { - $exception = $e; + default: + /* For all other issues, catch and store exception; + * don't throw until all input is read since we need + * to clear incoming queue. (For now, only store first + * exception.) */ + if (is_null($exception)) { + $exception = $e; + } } if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) diff --git a/lib/Horde/Imap/Client/Socket/Connection/Socket.php b/lib/Horde/Imap/Client/Socket/Connection/Socket.php index 7eccfcee..4e17c4e9 100644 --- a/lib/Horde/Imap/Client/Socket/Connection/Socket.php +++ b/lib/Horde/Imap/Client/Socket/Connection/Socket.php @@ -197,7 +197,7 @@ public function read($size = null) $read_now = microtime(true); $t_read = $read_now - $read_start; if ($t_read > $this->_params['timeout']) { - $this->_params['debug']->info(sprintf('ERROR: read timeout. No data received for %d seconds.', $this->_params['read_timeout'])); + $this->_params['debug']->info(sprintf('ERROR: read timeout. No data received for %d seconds.', $this->_params['timeout'])); throw new Horde_Imap_Client_Exception( Horde_Imap_Client_Translation::r("Read timeout."), diff --git a/lib/Horde/Imap/Client/Socket/Pop3.php b/lib/Horde/Imap/Client/Socket/Pop3.php index 43647ef8..35ef7bb4 100644 --- a/lib/Horde/Imap/Client/Socket/Pop3.php +++ b/lib/Horde/Imap/Client/Socket/Pop3.php @@ -1029,7 +1029,7 @@ protected function _fetchCmd( case Horde_Imap_Client::FETCH_IMAPDATE: foreach ($seq_ids as $id) { $tmp = $this->_pop3Cache('hdrob', $id); - $results->get($lookup[$id])->setImapDate($tmp['Date']); + $results->get($lookup[$id])->setImapDate((string) $tmp['Date']); } break; diff --git a/lib/Horde/Imap/Client/Tokenize.php b/lib/Horde/Imap/Client/Tokenize.php index f298c3ff..c2ee7081 100644 --- a/lib/Horde/Imap/Client/Tokenize.php +++ b/lib/Horde/Imap/Client/Tokenize.php @@ -94,10 +94,19 @@ public function __construct($data = null) } } + public function __destruct() + { + if (isset($this->_stream)) { + $this->_stream->close(); + $this->_stream = null; + } + } + /** */ public function __clone() { + $this->_stream = null; throw new LogicException('Object can not be cloned.'); } diff --git a/test/Stub/Socket.php b/test/Stub/Socket.php index 7df3ab88..704dc0e0 100644 --- a/test/Stub/Socket.php +++ b/test/Stub/Socket.php @@ -13,7 +13,10 @@ namespace Horde\Imap\Client\Test\Stub; +use Horde_Imap_Client_Data_Capability_Imap; use Horde_Imap_Client_Data_Thread; +use Horde_Imap_Client_Ids; +use Horde_Imap_Client_Interaction_Pipeline; use Horde_Imap_Client_Interaction_Server; use Horde_Imap_Client_Socket; use Horde_Imap_Client_Tokenize; @@ -30,6 +33,19 @@ class Socket extends Horde_Imap_Client_Socket { public $fetch_results; + private bool $captureSendCmd = false; + + public ?Horde_Imap_Client_Interaction_Pipeline $capturedPipeline = null; + + protected function _sendCmd($cmd) + { + if ($this->captureSendCmd) { + $this->capturedPipeline = $cmd; + return $cmd; + } + return parent::_sendCmd($cmd); + } + public function getThreadSort($data) { return new Horde_Imap_Client_Data_Thread($this->doServerResponse($this->_pipeline(), $data)->data['threadparse'], 'uid'); @@ -96,4 +112,13 @@ public function fetch($mailbox, $query, array $options = []) { return $this->fetch_results; } + + public function doVanishedPipeline(int $modseq, Horde_Imap_Client_Ids $ids): Horde_Imap_Client_Interaction_Pipeline + { + $this->captureSendCmd = true; + $this->capturedPipeline = null; + $this->_init['capability'] = new Horde_Imap_Client_Data_Capability_Imap(); + $this->_vanished($modseq, $ids); + return $this->capturedPipeline; + } } diff --git a/test/Unit/IdsTest.php b/test/Unit/IdsTest.php index 4d742328..5b64aa57 100644 --- a/test/Unit/IdsTest.php +++ b/test/Unit/IdsTest.php @@ -352,4 +352,45 @@ public function testSerialize() ); } + public function testForcedIntForRange() + { + $ids = new Horde_Imap_Client_Ids('1:3'); + $this->assertEquals( + [1, 2, 3], + iterator_to_array($ids) + ); + + foreach (iterator_to_array($ids) as $id) { + $this->assertIsInt($id); + } + } + + public function testForcedIntForSequence() + { + $ids = new Horde_Imap_Client_Ids('1,5,7'); + $this->assertEquals( + [1, 5, 7], + iterator_to_array($ids) + ); + + foreach (iterator_to_array($ids) as $id) { + $this->assertIsInt($id); + } + } + + public function testAddingWithForcedIntConversion() + { + $ids = new Horde_Imap_Client_Ids('1,5,7'); + $ids->add('101:103'); + + $this->assertEquals( + [1, 5, 7, 101, 102, 103], + iterator_to_array($ids) + ); + + foreach (iterator_to_array($ids) as $id) { + $this->assertIsInt($id); + } + } + } diff --git a/test/Unit/SocketTest.php b/test/Unit/SocketTest.php index 8125a8ab..b43c71a9 100644 --- a/test/Unit/SocketTest.php +++ b/test/Unit/SocketTest.php @@ -17,7 +17,9 @@ use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; use Horde\Imap\Client\Test\Stub\Socket; +use Horde_Imap_Client_Data_Thread; use Horde_Imap_Client_Fetch_Results; +use Horde_Imap_Client_Ids; /** * Tests for the IMAP Socket driver. @@ -49,10 +51,10 @@ public function testSimpleThreadParse() $data = '* THREAD (1)'; $thread = $this->test_ob->getThreadSort($data); - $this->assertFalse($thread instanceof Horde_Imap_Client_Data_Thread); + $this->assertInstanceOf(Horde_Imap_Client_Data_Thread::class, $thread); $list = $thread->messageList(); - $this->assertFalse($list instanceof Horde_Imap_Client_Ids); + $this->assertInstanceOf(Horde_Imap_Client_Ids::class, $list); $this->assertEquals( [1], $list->ids @@ -92,7 +94,7 @@ public function testComplexThreadParse() $thread = $this->test_ob->getThreadSort($data); $list = $thread->messageList(); - $this->assertFalse($list instanceof Horde_Imap_Client_Ids); + $this->assertInstanceOf(Horde_Imap_Client_Ids::class, $list); $this->assertEquals( range(1, 17), $list->ids @@ -354,4 +356,22 @@ public function testParseEnvelopeBlankSubject() $this->assertNotNull($env->to); } + public function testVanishedCommand() + { + // 200 non-consecutive 4-digit UIDs, fits in one 2000-octet command. + $ids = new Horde_Imap_Client_Ids(range(1001, 1399, 2)); + $pipeline = $this->test_ob->doVanishedPipeline(12345, $ids); + + $this->assertCount(1, $pipeline); + } + + public function testVanishedCommandChunks() + { + // 500 non-consecutive 4-digit UIDs, exceeds the 2000-octet limit. + $ids = new Horde_Imap_Client_Ids(range(1001, 1999, 2)); + $pipeline = $this->test_ob->doVanishedPipeline(12345, $ids); + + $this->assertGreaterThan(1, count($pipeline)); + } + }