Introduction

Email threading relies on three headers: Message-ID, In-Reply-To, and References. When an application sends emails without proper Message-ID headers or fails to set In-Reply-To and References on replies, email clients cannot group messages into conversations. Each email appears as a separate thread, confusing users and breaking the expected email experience in Gmail, Outlook, and other clients.

Symptoms

  • Replies appear as new conversations in Gmail/Outlook
  • Email clients show each message individually instead of grouped
  • Gmail shows "This message is not part of an existing conversation"
  • Message-ID header is missing or duplicated
  • All emails show the same Message-ID (application using a static value)
  • In-Reply-To references a non-existent Message-ID

Common Causes

  • Application email library not generating unique Message-ID per message
  • Using a hardcoded Message-ID for all outgoing emails
  • Reply emails not including In-Reply-To header with original Message-ID
  • References header not accumulating the chain of Message-ID values
  • MTA stripping custom headers during relay
  • Application generating invalid Message-ID format (missing angle brackets)

Step-by-Step Fix

  1. 1.Verify current Message-ID header:
  2. 2.```bash
  3. 3.# Send a test email and check headers
  4. 4.echo -e "Subject: Threading Test\n\nTest body" | \
  5. 5.sendmail -v user@example.com 2>&1 | grep "Message-ID"

# Or check a received email: openssl s_client -connect imap.example.com:993 -quiet # After login: # A1 FETCH 1 (BODY[HEADER]) ```

  1. 1.Generate proper Message-ID in application code:
  2. 2.```python
  3. 3.import uuid
  4. 4.import socket

def generate_message_id(): """Generate RFC 5322 compliant Message-ID.""" msg_id = f"<{uuid.uuid4().hex}@{socket.getfqdn()}>" return msg_id # Example: <a1b2c3d4e5f6@myserver.example.com> ```

  1. 1.Set threading headers for a reply email (Python):
  2. 2.```python
  3. 3.from email.mime.text import MIMEText
  4. 4.from email.utils import make_msgid, formatdate

def send_reply(original_message_id, original_references, to_addr, subject, body): msg = MIMEText(body) msg['From'] = 'support@example.com' msg['To'] = to_addr msg['Subject'] = f"Re: {subject.lstrip('Re: ').lstrip('re: ')}" msg['Date'] = formatdate(localtime=True) msg['Message-ID'] = make_msgid(domain='example.com')

# Threading headers msg['In-Reply-To'] = original_message_id

# Build References chain refs = [] if original_references: refs = original_references.split() refs.append(original_message_id) msg['References'] = ' '.join(refs)

return msg ```

  1. 1.For PHPMailer:
  2. 2.```php
  3. 3.// Original email
  4. 4.$originalMessageId = $mail->generateId(); // PHPMailer auto-generates
  5. 5.$mail->send();

// Reply - must include threading headers $reply = new PHPMailer(true); $reply->addCustomHeader('In-Reply-To', $originalMessageId); $reply->addCustomHeader('References', $originalMessageId); $reply->Subject = "Re: " . preg_replace('/^Re:\s*/i', '', $originalSubject); ```

  1. 1.For Node.js nodemailer:
  2. 2.```javascript
  3. 3.// Original email
  4. 4.const originalResult = await transporter.sendMail({
  5. 5.from: '"Support" <support@example.com>',
  6. 6.to: 'user@example.com',
  7. 7.subject: 'Your ticket #12345',
  8. 8.text: 'We received your request.',
  9. 9.headers: {
  10. 10.'Message-ID': <ticket-12345-${Date.now()}@example.com>
  11. 11.}
  12. 12.});

// Reply await transporter.sendMail({ from: '"Support" <support@example.com>', to: 'user@example.com', subject: 'Re: Your ticket #12345', text: 'Update on your ticket...', headers: { 'In-Reply-To': originalResult.messageId, 'References': originalResult.messageId } }); ```

  1. 1.Validate Message-ID format:
  2. 2.```bash
  3. 3.# Message-ID must be in angle brackets with local@domain format
  4. 4.# Valid: <unique-string@hostname.example.com>
  5. 5.# Invalid: unique-string (no brackets, no domain)
  6. 6.# Invalid: <static-id> (same for all messages, no domain)
  7. 7.# Invalid: <> (empty)

# Test with RFC 5322 regex: echo "<a1b2c3d4@myserver.example.com>" | \ grep -P '^<[^@]+@[^>]+>$' && echo "Valid" || echo "Invalid" ```

Prevention

  • Always generate unique Message-ID for every outgoing email
  • Include In-Reply-To and References headers on all replies
  • Use a consistent Message-ID domain (your sending domain)
  • Never hardcode Message-ID values
  • Test email threading in Gmail, Outlook, and Apple Mail before deployment
  • Use established email libraries that handle threading automatically