What it means
Email has no database. A message is copied from server to server, rewritten, filed under different folder names, given different local numbers by every system that stores it, and delivered to people who then move it around. The one thing that stays constant through all of that is the Message-ID header: a single string, assigned once, that names this particular message for the rest of its life.
RFC 5322 defines the syntax as msg-id = "<" id-left "@" id-right ">". The angle brackets are part of the grammar, not decoration, and there is no whitespace inside them. The left half is whatever the generator wants, as long as it is unique within the right half, and the right half is a domain the generator controls. That split is how global uniqueness is achieved without any central registry.
Everything interesting about email conversations is built on top of it. Threading, deduplication, bounce correlation, reply tracking in a CRM, and the ability to say "this is the same message the customer is complaining about" are all lookups on this one value.
How threading actually works
A reply does not contain its parent. It contains a pointer: In-Reply-To holds the parent identifier, and References holds the whole ancestry from the root of the thread down to that parent, space separated and in order. A client builds the tree by walking those pointers, which is why a thread survives being read on a phone, archived on a laptop and forwarded to a colleague.
The classical algorithm, published by Jamie Zawinski and still the basis of most implementations, prefers References precisely because it is redundant. If one message in the middle loses its In-Reply-To, the chain still reconnects through the ancestry list. Only when both are missing does it fall back to normalising subjects, and that fallback is where every wrong-thread bug comes from: two unrelated messages titled Invoice merge into one conversation, and a customer sees another customer name in their thread view.
The deduplication problem
If you are syncing a mailbox rather than just reading one, the Message-ID stops being a threading detail and becomes the primary correctness mechanism. IMAP gives every message a UID, but a UID is scoped to one folder on one server and is only meaningful alongside that folder UIDVALIDITY value. Move a message from the inbox to an archive folder and it gets a new UID. Rebuild the mailbox and every UID can change at once.
Gmail makes this sharper: over IMAP the same message appears in All Mail and again in every label folder it carries. A synchroniser keyed on folder plus UID stores three copies of one email, then shows the customer three copies of the same reply. Keyed on account plus Message-ID it stores one, and correctly recognises the message again after it is moved.
The Sent folder is a second trap. When you send through a provider API, the copy that lands in the mailbox and the record you wrote in your own database are only the same message if you controlled the identifier. Generate it yourself, put it in the outgoing message, store it against the conversation, and the Sent-folder sync recognises its own message instead of creating a duplicate that looks like a reply from the customer.
Bounce and delivery correlation
A delivery status notification, defined in RFC 3464, is a multipart message. The machine-readable part reports the failing recipient and the status code, and the third part usually carries either the whole original message or just its headers. Either way the original Message-ID is in there, which is what lets you attach a bounce that arrives six hours later to the exact send that caused it rather than to the most recent message to that address.
Provider webhooks give you the same correlation through their own identifier. Store both: the provider id, because that is what their support team can search, and the Message-ID, because that is what appears in the recipient own mail client and in any header dump they send you. See bounce rate for what to do with the codes once you have matched them.
Generating one properly
- Use a UUID or a random 128-bit value for the left part. Timestamps and sequence numbers leak volume and collide across processes.
- Use your sending domain for the right part, not the internal hostname of the container that happened to run the job.
- Never reuse one. A resend is a new message, so it gets a new identifier and references the old one.
- Keep the brackets out of the database column. Store the bare value and add the brackets when you write the header. Mixed storage is the reason a lookup silently misses.
- Index it. Every inbound message triggers a lookup by this value; without an index the sync degrades as the mailbox grows.
Related concepts
- IMAP IDLE: how new messages arrive in the first place, and why UIDs are not enough.
- Bounce rate: correlating an asynchronous DSN back to the send that caused it.
- DKIM: sign the Message-ID header so a relay cannot rewrite your threading.
- Webhook: how a provider tells you about delivery events keyed on this identifier.
- Drip campaign: a sequence that replies in-thread needs the previous identifier to do it.
How Pinlyx handles it
The mailbox sync stores messages keyed on the workspace, the mailbox and the Message-ID, so a message that appears in All Mail and under two Gmail labels becomes a single conversation entry. Outbound mail is given its identifier before it reaches the relay and that value is written to the conversation immediately, which is what lets the Sent-folder sync run on its own cursor without duplicating what we just sent. Replies are matched by walking References first and In-Reply-To second, with subject matching disabled entirely, because a CRM merging two customers into one thread is a data leak rather than a cosmetic bug.