From 40407e061d24e5ea6d83cbe0726a7019813ac7a1 Mon Sep 17 00:00:00 2001 From: rissrice2105-agent <289161642+rissrice2105-agent@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:59:47 -0600 Subject: [PATCH] fix(mailbox): write messages before IMAP append wait --- internal/mailbox/imap.go | 5 +- internal/mailbox/imap_test.go | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 internal/mailbox/imap_test.go diff --git a/internal/mailbox/imap.go b/internal/mailbox/imap.go index 8b5b768..910697c 100644 --- a/internal/mailbox/imap.go +++ b/internal/mailbox/imap.go @@ -229,7 +229,10 @@ func (t *imapTransport) Send(_ context.Context, from string, d Draft) (SendResul } // Best-effort copy to Sent so the message shows in the member's mailbox. t.mu.Lock() - _, _ = t.c.Append(Sent, int64(len(msg)), nil).Wait() // ignore APPEND errors + appendCmd := t.c.Append(Sent, int64(len(msg)), nil) + _, _ = appendCmd.Write(msg) + _ = appendCmd.Close() + _, _ = appendCmd.Wait() t.mu.Unlock() return SendResult{MessageID: msgID}, nil } diff --git a/internal/mailbox/imap_test.go b/internal/mailbox/imap_test.go new file mode 100644 index 0000000..3f39502 --- /dev/null +++ b/internal/mailbox/imap_test.go @@ -0,0 +1,91 @@ +package mailbox + +import ( + "context" + "net" + "testing" + + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/emersion/go-imap/v2/imapserver/imapmemserver" +) + +func newFakeIMAP(t *testing.T) string { + t.Helper() + + backend := imapmemserver.New() + user := imapmemserver.NewUser("alice", "secret") + if err := user.Create(Inbox, nil); err != nil { + t.Fatal(err) + } + if err := user.Create(Sent, nil); err != nil { + t.Fatal(err) + } + backend.AddUser(user) + + server := imapserver.New(&imapserver.Options{ + NewSession: func(*imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) { + return backend.NewSession(), nil, nil + }, + InsecureAuth: true, + Caps: imap.CapSet{ + imap.CapIMAP4rev1: {}, + imap.CapIMAP4rev2: {}, + }, + }) + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + go func() { _ = server.Serve(listener) }() + t.Cleanup(func() { + _ = server.Close() + _ = listener.Close() + }) + return listener.Addr().String() +} + +func TestIMAPSendCopiesMessageToSent(t *testing.T) { + smtpServer := newFakeSMTP(t) + transport, err := NewIMAPTransport(IMAPConfig{ + IMAPAddr: newFakeIMAP(t), + SMTPAddr: smtpServer.addr, + Username: "alice", + Password: "secret", + Plaintext: true, + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = transport.Close() }) + + _, err = transport.Send(context.Background(), "alice@example.test", Draft{ + To: []Address{{Address: "bob@example.test"}}, + Subject: "Saved message", + Text: "message body", + }) + if err != nil { + t.Fatal(err) + } + + messages, err := transport.ListMessages(context.Background(), ListOptions{Mailbox: Sent}) + if err != nil { + t.Fatal(err) + } + if len(messages) != 1 { + t.Fatalf("Sent contains %d messages; want 1", len(messages)) + } + if messages[0].Subject != "Saved message" { + t.Fatalf("Sent subject = %q; want %q", messages[0].Subject, "Saved message") + } + message, found, err := transport.ReadMessage(context.Background(), Sent, messages[0].UID) + if err != nil { + t.Fatal(err) + } + if !found { + t.Fatal("saved message not found in Sent") + } + if message.Text != "message body" { + t.Fatalf("Sent body = %q; want %q", message.Text, "message body") + } +}