From 95ddbf5619aa468bc29e4b54ff59696ffea8945e Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Sun, 5 Jul 2026 13:59:56 +0700 Subject: [PATCH] Update copy.go --- internal/imapx/copy.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/imapx/copy.go b/internal/imapx/copy.go index 2966273..9b0eeb3 100644 --- a/internal/imapx/copy.go +++ b/internal/imapx/copy.go @@ -231,11 +231,18 @@ func streamOne(src, dst *imapclient.Client, dstFolder string, uid imap.UID, flag } appendCmd := dst.Append(dstFolder, int64(len(body)), &imap.AppendOptions{Flags: keepFlags(flags), Time: internalDate}) - if _, err := io.Copy(appendCmd, bytes.NewReader(body)); err != nil { - return err + // Append acquires go-imap's per-client encoder mutex and holds it until + // Close() calls enc.end(). Close() MUST run on every path: if io.Copy + // fails mid-write (server stall, idle timeout), returning without Close() + // leaks the mutex and the NEXT Append on this client deadlocks forever on + // beginCommand. Close() is idempotent and always releases the lock. + _, copyErr := io.Copy(appendCmd, bytes.NewReader(body)) + closeErr := appendCmd.Close() + if copyErr != nil { + return fmt.Errorf("append body uid %v: %w", uid, copyErr) } - if err := appendCmd.Close(); err != nil { - return err + if closeErr != nil { + return closeErr } _, err := appendCmd.Wait() return err