GLOSSARY

What is a Message-ID?

Message-ID is the globally unique identifier RFC 5322 requires on every email, written as an addr-spec inside angle brackets, and it is the key that In-Reply-To and References use to reconstruct a conversation thread across every client and server that touches the message.

Free forever plan · No credit card required · Cancel anytime

Quick definition

Message-ID is the globally unique identifier RFC 5322 requires on every email, written as an addr-spec inside angle brackets, and it is the key that In-Reply-To and References use to reconstruct a conversation thread across every client and server that touches the message.

In one line: <unique-string@yourdomain.com> is the only stable name an email ever has.

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.

Wire format

One thread, three messages, six headers.

Read down the References values and the tree draws itself.

A three-message conversation

# 1. you send the first message
Message-ID: <7f1b2c9a-4e33-4f0b-9a1e-5f7c0d2a11b8@pinlyx.com>
Subject: Onboarding call next week?

# 2. the customer replies from Gmail
Message-ID: <CAF9x2mQ0nJ8y+Wr3TkPq7z@mail.gmail.com>
In-Reply-To: <7f1b2c9a-4e33-4f0b-9a1e-5f7c0d2a11b8@pinlyx.com>
References: <7f1b2c9a-4e33-4f0b-9a1e-5f7c0d2a11b8@pinlyx.com>
Subject: Re: Onboarding call next week?

# 3. you reply again, in thread
Message-ID: <c41d0a77-9b18-42e6-8e0a-2b6f4d9c3a10@pinlyx.com>
In-Reply-To: <CAF9x2mQ0nJ8y+Wr3TkPq7z@mail.gmail.com>
References: <7f1b2c9a-4e33-4f0b-9a1e-5f7c0d2a11b8@pinlyx.com>
 <CAF9x2mQ0nJ8y+Wr3TkPq7z@mail.gmail.com>
Subject: Re: Onboarding call next week?

The References header on message three is folded across two lines with a leading space. That is correct RFC 5322 folding, and a naive header parser that splits on newline loses the second identifier.

The deduplication key, in SQL

-- one row per real message, no matter how many folders it appears in
CREATE UNIQUE INDEX ux_email_messages_mailbox_msgid
  ON email_messages ("MailboxId", "MessageId");

-- the fallback for messages that arrive without a Message-ID at all
ALTER TABLE email_messages
  ADD COLUMN "Fingerprint" TEXT
  GENERATED ALWAYS AS (
    md5("SentAt"::text || "FromAddress" || "Subject" || "SizeBytes"::text)
  ) STORED;

Scope the uniqueness to the mailbox, not globally. Two people in the same workspace can legitimately hold copies of the same message, and collapsing them hides one of the two conversations.

Resolution order

Which header wins when they disagree.

HeaderRoleHow to use it
Message-IDIdentitySet once by the first system that handles the message and never changed afterwards. Every other threading header is a reference to one of these.
In-Reply-ToDirect parentThe Message-ID of the single message being replied to. Usually one value. It is the strongest signal, but it is missing more often than you would like.
ReferencesAncestryThe full chain from root to parent, space separated and in order. Threading algorithms prefer this because it survives a missing In-Reply-To in the middle of the chain.
SubjectLast resortNormalised by stripping Re:, Fwd: and their localised variants. This is the fallback that merges two unrelated threads both called Invoice, so use it only when both other headers are absent.
Watch out for

Subject-based threading in a shared inbox is a data leak.

Falling back to a normalised subject when both threading headers are missing is defensible in a personal mail client, where every message belongs to the same person anyway. In a team inbox holding conversations with hundreds of different customers, it merges two people who both wrote Invoice question into one thread, and each of them can then read the other message.

Turn it off. A message that cannot be threaded should start its own conversation, which is a small annoyance, rather than join the wrong one, which is an incident.

Message-ID: FAQ

The questions that come up while building or debugging a mailbox sync.

Whichever of them acts first. If you hand an SMTP relay a message with no Message-ID header, the relay generates one, and you never learn what it chose unless the API returns it. That is how a Sent copy in the mailbox and the record in your database end up with different identifiers, and how a reply then fails to thread onto anything. Generate the identifier yourself before submission, store it, and send it.
By specification yes, in practice no. Some clients reuse an identifier across resends, some appliances rewrite the header, mailing list software occasionally regenerates it, and a small number of senders emit a constant string. Treat it as a very good deduplication key rather than a primary key: scope it to the account, and keep a fallback fingerprint of date, from, subject and size for messages where it is missing or obviously reused.
Formally the part before the at sign is case sensitive and the domain part is not, because the syntax borrows from addr-spec. Practically, compare the whole value exactly as it arrived after stripping the angle brackets and any surrounding whitespace, and never lowercase it. Lowercasing is safe against almost every real generator and unsafe against the few that use base64, which is exactly the population you cannot afford to collide.
A domain you control, because that is what makes global uniqueness achievable: your side guarantees the left part is unique within your domain and the domain guarantees it is unique in the world. Do not use the internal hostname of the machine that generated it, which is the default in several libraries and leaks your infrastructure naming into every recipient inbox.
Because Gmail presents the same message once under each label folder and again in All Mail, and IMAP UIDs are per folder, so a sync keyed only on folder plus UID stores it several times. Deduplicate on account plus Message-ID and the copies collapse into one row, which is also what makes a thread look right when a message is later moved or archived.
Long threads produce long References headers, and RFC 5322 limits a line to 998 characters, so the header has to be folded across lines. Some clients also trim the middle of the chain when it grows. Keep the first identifier and the most recent few when you have to trim, because the root is what groups the thread and the tail is what attaches the newest reply.
Ready to ship

One message, one thread, one contact record.

Pinlyx deduplicates on Message-ID, threads on References, and never merges two customers because they used the same subject line.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.