#26099·twenty

IMAP: drafts pile up in the thread because every client autosave creates a new row and old drafts are never removed

Author: admin-laicadevCreated Sep 16, 2026Updated Sep 17, 2026
Labelstype: bug

Summary

On a self-hosted instance with an IMAP mailbox, a single reply appears as several draft rows in the email thread. Each save in the mail client adds one more row, and the old rows never disappear.

The IMAP driver never reports deleted (expunged) messages, and the import path only inserts, so it never updates or removes an existing row.

Environment

  • Twenty v2.39.0, self-hosted with Docker, image twentycrm/twenty:v2.39.0
  • Mail server: Dovecot, IMAP over TLS on port 993
  • Mail client: hoster webmail client (not sure what the use)
  • The server advertises CONDSTORE, QRESYNC, UIDPLUS and MOVE after login

What I see

The thread timeline in twenty shows each draft autosave as a new row. The IMAP messages behind those rows no longer exist. A UID SEARCH for that subject in Drafts returns nothing.

Expected behavior

One draft row that holds the latest version, removed once the mail is sent or the draft deleted.

Technical inputs

Reproduce:

  1. Connect an IMAP mailbox whose client rewrites a draft on every save, meaning it appends a new message and expunges the old one. The trace below shows what that looks like.
  2. Write a mail in the client and save it a few times, or let it autosave.
  3. Let Twenty run its message import between the saves.
  4. Open the thread in Twenty.

IMAP trace:

Read-only test with EXAMINE and UID FETCH on one new draft.

Time (UTC) Drafts HIGHESTMODSEQ Event
12:40:15 643 new uid 2884, flags (\Seen \Draft), message-id <1297733633.357470...>
12:41:20 651 new uid 2886 <239981072.357555...>, uid 2884 gone
12:42:30 660 new uid 2889 <500785936.357672...>, uid 2886 gone
12:44:05 666 new uid 2891 <1955339940.357847...>, uid 2889 gone
13:08:06 672 new uid 2893 <856918606.359828...>, uid 2891 gone

UIDVALIDITY stayed at 1752657694 for the whole test. Every save creates a new UID and a new Message-ID, and the server deletes the previous one. The client skipped some UIDs in between, 2885, 2887 and 2888, which were further autosaves. The folder holds one message, but it already sits at UID 2893, so this client has created and deleted roughly 2,900 drafts in that folder.

Neither the external id, which is folder:uid, nor headerMessageId stays stable across a draft save. Every save looks like a new message to Twenty.

Happy to share more data privately if it helps fix the bug.

Claudes suggestion:

haven't checked it, because I'm not that good of a coder. Paths below are relative to packages/twenty-server/src/modules/messaging/.

message-import-manager/drivers/imap/services/imap-get-message-list.service.ts returns a hardcoded messageExternalIdsToDelete: [] in both return paths, at L109-115 and L157-163. It builds external ids as ${prefix}:${uid} at L153-155.

message-import-manager/drivers/imap/services/imap-sync.service.ts only looks forward. fetchNewMessageUids at L57-77 does this:

const lastSyncedUid = previousCursor?.highestUid ?? 0;
if (lastSyncedUid >= maxUid) { return []; }
const uidRange = `${lastSyncedUid + 1}:${maxUid}`;

validateUidValidity at L37-55 only throws when UIDVALIDITY changes. The driver handles no expunge and no QRESYNC VANISHED response, although this server advertises QRESYNC.

The import then only inserts. services/messaging-message-list-fetch.service.ts at L162-181 skips every external id that already has an association, and services/messaging-message.service.ts at L160-201 and L261 inserts only when neither a message, matched by headerMessageId, nor an association exists. It never updates isDraft, text or receivedAt on a row that already exists.

Five saves therefore produce five rows, and Twenty deletes or refreshes none of them.