From adb6679ed4965e329e46ca9175c09ec3c90ccf20 Mon Sep 17 00:00:00 2001 From: FromSi Date: Mon, 18 May 2026 00:14:22 +0500 Subject: [PATCH] fix(pop3): batch delete in one session (#1292) POP3 batch deletion now sends requests like this: ```text CONNECT AUTH UIDL DELE 1 DELE 2 DELE 3 QUIT ``` `QUIT` is sent once at the end, so all `DELE` commands are committed in one POP3 session. Closes #555 Previously, batch deletion sent requests like this: ```text CONNECT AUTH UIDL DELE 1 QUIT CONNECT AUTH UIDL DELE 2 QUIT CONNECT AUTH UIDL DELE 3 QUIT ``` So deleting multiple emails repeated the full connection/authentication cycle for every message. Signed-off-by: drew --- backend/pop3/pop3.go | 91 ++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/backend/pop3/pop3.go b/backend/pop3/pop3.go index c9f4ca6ab7ce05cbefa1c716b0cb124ed1943fa9..a883cf347e76ab35cba049188dca142d4db28246 100644 --- a/backend/pop3/pop3.go +++ b/backend/pop3/pop3.go @@ -188,25 +188,8 @@ func (p *Provider) MarkAsUnread(_ context.Context, _ string, _ uint32) error { return nil } -func (p *Provider) DeleteEmail(_ context.Context, _ string, uid uint32) error { - conn, err := p.connect() - if err != nil { - return err - } - - msgID, err := p.findMessageByUID(conn, uid) - if err != nil { - conn.Quit() - return err - } - - if err := conn.Dele(msgID); err != nil { - conn.Quit() - return fmt.Errorf("pop3 dele: %w", err) - } - - // Quit commits the deletion - return conn.Quit() +func (p *Provider) DeleteEmail(ctx context.Context, folder string, uid uint32) error { + return p.DeleteEmails(ctx, folder, []uint32{uid}) } func (p *Provider) ArchiveEmail(_ context.Context, _ string, _ uint32) error { @@ -217,14 +200,34 @@ func (p *Provider) MoveEmail(_ context.Context, _ uint32, _, _ string) error { return backend.ErrNotSupported } -func (p *Provider) DeleteEmails(ctx context.Context, folder string, uids []uint32) error { - // POP3 doesn't support batch - loop through individual operations +func (p *Provider) DeleteEmails(_ context.Context, _ string, uids []uint32) error { + if len(uids) == 0 { + return nil + } + + conn, err := p.connect() + if err != nil { + return err + } + + messageIDsByUID, err := p.buildMessageIDsByUID(conn) + if err != nil { + conn.Quit() + return err + } + for _, uid := range uids { - if err := p.DeleteEmail(ctx, folder, uid); err != nil { - return err + msgID, ok := messageIDsByUID[uid] + if !ok { + return fmt.Errorf("pop3: message with UID %d not found", uid) + } + + if err := conn.Dele(msgID); err != nil { + return fmt.Errorf("pop3 dele: %w", err) } } - return nil + + return conn.Quit() } func (p *Provider) ArchiveEmails(_ context.Context, _ string, _ []uint32) error { @@ -261,31 +264,40 @@ func (p *Provider) Close() error { return nil } -// Verify interface compliance at compile time. -var _ backend.Provider = (*Provider)(nil) - -// findMessageByUID finds a POP3 message ID by matching the UID hash. -func (p *Provider) findMessageByUID(conn *pop3client.Conn, uid uint32) (int, error) { +func (p *Provider) buildMessageIDsByUID(conn *pop3client.Conn) (map[uint32]int, error) { msgs, err := conn.Uidl(0) if err != nil { msgs, err = conn.List(0) if err != nil { - return 0, fmt.Errorf("pop3 list: %w", err) + return nil, fmt.Errorf("pop3 list: %w", err) } + + messageIDsByUID := make(map[uint32]int, len(msgs)) for _, m := range msgs { - if hashUID(fmt.Sprintf("%d", m.ID)) == uid { - return m.ID, nil - } + messageIDsByUID[hashUID(fmt.Sprintf("%d", m.ID))] = m.ID } - return 0, fmt.Errorf("pop3: message with UID %d not found", uid) + return messageIDsByUID, nil } + messageIDsByUID := make(map[uint32]int, len(msgs)) for _, m := range msgs { - if hashUID(m.UID) == uid { - return m.ID, nil - } + messageIDsByUID[hashUID(m.UID)] = m.ID } - return 0, fmt.Errorf("pop3: message with UID %d not found", uid) + return messageIDsByUID, nil +} + +// findMessageByUID finds a POP3 message ID by matching the UID hash. +func (p *Provider) findMessageByUID(conn *pop3client.Conn, uid uint32) (int, error) { + messageIDsByUID, err := p.buildMessageIDsByUID(conn) + if err != nil { + return 0, err + } + + msgID, ok := messageIDsByUID[uid] + if !ok { + return 0, fmt.Errorf("pop3: message with UID %d not found", uid) + } + return msgID, nil } // hashUID converts a POP3 UIDL string to a uint32 hash. @@ -300,6 +312,9 @@ func hashUID(uidl string) uint32 { return hash } +// Verify interface compliance at compile time. +var _ backend.Provider = (*Provider)(nil) + // entityToEmail converts message headers to a backend.Email. func entityToEmail(header *message.Header, msgInfo pop3client.MessageID, accountID string) backend.Email { from := header.Get("From")