diff --git a/src/MessageQuery.php b/src/MessageQuery.php index 7e80d60..e6711fa 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -92,9 +92,13 @@ public function append(string $message, mixed $flags = null): int */ public function each(callable $callback, int $chunkSize = 10, int $startChunk = 1): void { - $this->chunk(fn (MessageCollection $messages) => ( - $messages->each($callback) - ), $chunkSize, $startChunk); + $this->chunk(function (MessageCollection $messages) use ($callback) { + foreach ($messages as $key => $message) { + if ($callback($message, $key) === false) { + return false; + } + } + }, $chunkSize, $startChunk); } /** @@ -134,7 +138,10 @@ public function chunk(callable $callback, int $chunkSize = 10, int $startChunk = break; } - $callback($hydrated, $page); + // If the callback returns false, break out. + if ($callback($hydrated, $page) === false) { + break; + } } // Restore the original state. diff --git a/src/Testing/FakeMessageQuery.php b/src/Testing/FakeMessageQuery.php index 88c3fad..cc0146c 100644 --- a/src/Testing/FakeMessageQuery.php +++ b/src/Testing/FakeMessageQuery.php @@ -81,7 +81,13 @@ public function append(string $message, mixed $flags = null): int */ public function each(callable $callback, int $chunkSize = 10, int $startChunk = 1): void { - $this->get()->each($callback); + $this->chunk(function (MessageCollection $messages) use ($callback) { + foreach ($messages as $key => $message) { + if ($callback($message, $key) === false) { + return false; + } + } + }, $chunkSize, $startChunk); } /** @@ -89,7 +95,22 @@ public function each(callable $callback, int $chunkSize = 10, int $startChunk = */ public function chunk(callable $callback, int $chunkSize = 10, int $startChunk = 1): void { - $this->get()->chunk($chunkSize)->each($callback); + $page = $startChunk; + + foreach ($this->get()->chunk($chunkSize) as $chunk) { + if ($page < $startChunk) { + $page++; + + continue; + } + + // If the callback returns false, break out. + if ($callback($chunk, $page) === false) { + break; + } + + $page++; + } } /** diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index 77f0bff..72b41bb 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -90,3 +90,94 @@ function query(?Mailbox $mailbox = null): MessageQuery expect($query->oldest())->toBe($query); expect($query->newest())->toBe($query); }); + +test('each breaks when callback returns false', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2 3 4 5', + 'TAG2 OK SEARCH completed', + '* 5 FETCH (UID 5 FLAGS () BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + '* 4 FETCH (UID 4 FLAGS () BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + 'TAG3 OK FETCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $processedUids = []; + + query($mailbox)->each(function ($message) use (&$processedUids) { + $processedUids[] = $message->uid(); + + // Break after processing the first message (which will be UID 5 due to desc order) + if ($message->uid() === 5) { + return false; + } + }, 2); // Use chunk size of 2 + + // Should only process the first message (UID 5 due to desc order) + expect($processedUids)->toBe([5]); +}); + +test('chunk breaks when callback returns false', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2 3 4 5', + 'TAG2 OK SEARCH completed', + '* 5 FETCH (UID 5 FLAGS () BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + '* 4 FETCH (UID 4 FLAGS () BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + 'TAG3 OK FETCH completed', + '* 3 FETCH (UID 3 FLAGS () BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + '* 2 FETCH (UID 2 FLAGS () BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + 'TAG4 OK FETCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $processedChunks = []; + + query($mailbox)->chunk(function ($messages, $page) use (&$processedChunks) { + $processedChunks[] = $page; + + // Break after processing the first chunk + if ($page === 1) { + return false; + } + }, 2); // Use chunk size of 2 + + // Should only process the first chunk (page 1) + expect($processedChunks)->toBe([1]); +}); diff --git a/tests/Unit/Testing/FakeMessageQueryTest.php b/tests/Unit/Testing/FakeMessageQueryTest.php index 18caacd..b0e4009 100644 --- a/tests/Unit/Testing/FakeMessageQueryTest.php +++ b/tests/Unit/Testing/FakeMessageQueryTest.php @@ -173,3 +173,93 @@ expect($query->find(2))->not->toBeNull(); expect($query->find(4))->not->toBeNull(); }); + +test('each breaks when callback returns false', function () { + $folder = new FakeFolder('INBOX', messages: [ + new FakeMessage(1, [], 'Message 1'), + new FakeMessage(2, [], 'Message 2'), + new FakeMessage(3, [], 'Message 3'), + new FakeMessage(4, [], 'Message 4'), + new FakeMessage(5, [], 'Message 5'), + ]); + + $query = new FakeMessageQuery($folder); + $processedUids = []; + + $query->each(function ($message) use (&$processedUids) { + $processedUids[] = $message->uid(); + + // Break after processing the third message + if ($message->uid() === 3) { + return false; + } + }, 2); // Use chunk size of 2 + + // Should process messages 1, 2, and 3, then break + expect($processedUids)->toBe([1, 2, 3]); +}); + +test('chunk breaks when callback returns false', function () { + $folder = new FakeFolder('INBOX', messages: [ + new FakeMessage(1, [], 'Message 1'), + new FakeMessage(2, [], 'Message 2'), + new FakeMessage(3, [], 'Message 3'), + new FakeMessage(4, [], 'Message 4'), + new FakeMessage(5, [], 'Message 5'), + ]); + + $query = new FakeMessageQuery($folder); + $processedChunks = []; + + $query->chunk(function ($messages, $page) use (&$processedChunks) { + $processedChunks[] = $page; + + // Break after processing the second chunk + if ($page === 2) { + return false; + } + }, 2); // Use chunk size of 2 + + // Should process chunks 1 and 2, then break (chunk 3 should not be processed) + expect($processedChunks)->toBe([1, 2]); +}); + +test('each processes all messages when callback never returns false', function () { + $folder = new FakeFolder('INBOX', messages: [ + new FakeMessage(1, [], 'Message 1'), + new FakeMessage(2, [], 'Message 2'), + new FakeMessage(3, [], 'Message 3'), + ]); + + $query = new FakeMessageQuery($folder); + $processedUids = []; + + $query->each(function ($message) use (&$processedUids) { + $processedUids[] = $message->uid(); + // Never return false + }); + + // Should process all messages + expect($processedUids)->toBe([1, 2, 3]); +}); + +test('chunk processes all chunks when callback never returns false', function () { + $folder = new FakeFolder('INBOX', messages: [ + new FakeMessage(1, [], 'Message 1'), + new FakeMessage(2, [], 'Message 2'), + new FakeMessage(3, [], 'Message 3'), + new FakeMessage(4, [], 'Message 4'), + new FakeMessage(5, [], 'Message 5'), + ]); + + $query = new FakeMessageQuery($folder); + $processedChunks = []; + + $query->chunk(function ($messages, $page) use (&$processedChunks) { + $processedChunks[] = $page; + // Never return false + }, 2); // Use chunk size of 2 + + // Should process all chunks (1, 2, 3) + expect($processedChunks)->toBe([1, 2, 3]); +});