From 9697b0241deb600ad8a2f43364e6361d71bbfbec Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Sat, 24 Oct 2026 10:30:38 -0400 Subject: [PATCH] Prevent authentication after a failed STARTTLS handshake --- src/Connection/ImapConnection.php | 6 +++- tests/Unit/Connection/ImapConnectionTest.php | 26 ++++++++++++++ tests/Unit/MailboxTest.php | 37 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/Connection/ImapConnection.php b/src/Connection/ImapConnection.php index fed3fd3..84589d9 100644 --- a/src/Connection/ImapConnection.php +++ b/src/Connection/ImapConnection.php @@ -216,7 +216,11 @@ public function startTls(): void $this->assertTaggedResponse($tag); - $this->stream->setSocketSetCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + if ($this->stream->setSocketSetCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT) !== true) { + $this->disconnect(); + + throw new ImapConnectionFailedException('Unable to establish TLS encryption.'); + } } /** diff --git a/tests/Unit/Connection/ImapConnectionTest.php b/tests/Unit/Connection/ImapConnectionTest.php index d99f718..b77bdda 100644 --- a/tests/Unit/Connection/ImapConnectionTest.php +++ b/tests/Unit/Connection/ImapConnectionTest.php @@ -166,6 +166,32 @@ $connection->startTls(); })->throws(ImapCommandException::class, 'IMAP command "TAG1 STARTTLS" failed. Response: "TAG1 BAD TLS negotiation failed"'); +test('start tls disconnects when encryption is not established', function (bool|int $result) { + $stream = new class($result) extends FakeStream + { + public function __construct(protected bool|int $result) {} + + public function setSocketSetCrypto(bool $enabled, ?int $method): bool|int + { + return $this->result; + } + }; + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Begin TLS negotiation now', + ]); + + $connection = new ImapConnection($stream); + $connection->connect('imap.example.com'); + + expect(fn () => $connection->startTls())->toThrow(ImapConnectionFailedException::class); + expect($connection->connected())->toBeFalse(); + + $stream->assertWritten('TAG1 STARTTLS'); + $stream->assertNotWritten('LOGOUT'); +})->with([false, 0]); + test('done', function () { $stream = new FakeStream; $stream->open(); diff --git a/tests/Unit/MailboxTest.php b/tests/Unit/MailboxTest.php index 8e5f873..f8788d3 100644 --- a/tests/Unit/MailboxTest.php +++ b/tests/Unit/MailboxTest.php @@ -1,7 +1,9 @@ throws(ImapCommandException::class, 'IMAP command "TAG1 LOGIN [redacted] [redacted]" failed. Response: "TAG1 BAD Authentication failed"'); +test('connect does not authenticate when the tls handshake fails', function (bool|int $result, string $authentication) { + $stream = new class($result) extends FakeStream + { + public function __construct(protected bool|int $result) {} + + public function setSocketSetCrypto(bool $enabled, ?int $method): bool|int + { + return $this->result; + } + }; + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Begin TLS negotiation now', + 'TAG2 OK Authenticated', + ]); + + $mailbox = Mailbox::make([ + 'host' => 'imap.example.com', + 'encryption' => 'starttls', + 'authentication' => $authentication, + 'username' => 'foo', + 'password' => 'bar', + ]); + + expect(fn () => $mailbox->connect(new ImapConnection($stream))) + ->toThrow(ImapConnectionFailedException::class); + + expect($mailbox->connected())->toBeFalse(); + + $stream->assertWritten('TAG1 STARTTLS'); + $stream->assertNotWritten('LOGIN'); + $stream->assertNotWritten('AUTHENTICATE'); +})->with([false, 0])->with(['plain', 'oauth']); + test('folders', function () { $mailbox = Mailbox::make();