From bdba6e7d89faeb45b94a3faf682f42a823cd6ed0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:59:40 +0000 Subject: [PATCH] feat: prove IMAP TLS literal cancellation releases the decoder before retry and rejects the previously pinned EOF-only reader --- .../smtpimap/imap/sync_repair_wire_test.go | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/client/smtpimap/imap/sync_repair_wire_test.go b/internal/client/smtpimap/imap/sync_repair_wire_test.go index 1e8f53174..f8a3c264e 100644 --- a/internal/client/smtpimap/imap/sync_repair_wire_test.go +++ b/internal/client/smtpimap/imap/sync_repair_wire_test.go @@ -414,11 +414,35 @@ func TestStartTLSSetupStillRejectsUntrustedCertificate(t *testing.T) { } } +type literalReadSignalConn struct { + net.Conn + ready chan struct{} + sawShort atomic.Bool + signaled atomic.Bool +} + +func (c *literalReadSignalConn) Read(p []byte) (int, error) { + if c.sawShort.Load() && c.signaled.CompareAndSwap(false, true) { + c.ready <- struct{}{} + } + n, err := c.Conn.Read(p) + if strings.Contains(string(p[:n]), "short") { + c.sawShort.Store(true) + } + return n, err +} + func TestFetchBodyTLSLiteralFailureReleasesDecoderAndRetries(t *testing.T) { var attempts atomic.Int32 + bodyStarted := make(chan struct{}, 1) + literalReadReady := make(chan struct{}, 1) + unblockServer := make(chan struct{}) + defer close(unblockServer) c, _ := bodyRepairFixture(t, func(conn net.Conn, tag string) bool { if attempts.Add(1) == 1 { _, _ = io.WriteString(conn, "* 1 FETCH (UID 1 BODY[1] {13}\r\nshort") + bodyStarted <- struct{}{} + <-unblockServer return false } _, _ = fmt.Fprintf(conn, "* 1 FETCH (UID 1 BODY[1] {13}\r\ncomplete body)\r\n%s OK body\r\n", tag) @@ -451,7 +475,7 @@ func TestFetchBodyTLSLiteralFailureReleasesDecoderAndRetries(t *testing.T) { _ = raw.Close() t.Fatal(err) } - c.client = imapclient.New(secure, nil) + c.client = imapclient.New(&literalReadSignalConn{Conn: secure, ready: literalReadReady}, nil) c.conn = conn c.transport = raw c.selected.Store(false) @@ -472,6 +496,17 @@ func TestFetchBodyTLSLiteralFailureReleasesDecoderAndRetries(t *testing.T) { result := make(chan *errx.MailError, 1) go func() { result <- c.FetchBody(fetched[0]) }() select { + case <-bodyStarted: + case <-time.After(time.Second): + t.Fatal("TLS literal did not start") + } + select { + case <-literalReadReady: + case <-time.After(time.Second): + t.Fatal("TLS literal reader did not block on remaining bytes") + } + _ = c.transport.Close() + select { case err := <-result: if err == nil { t.Fatal("TLS literal failure swallowed")