Skip to main content

Email Threading Overview

Overview

RustMailer introduces a simple yet effective threading mechanism for organizing emails. Instead of relying on full message content, threading is derived entirely from message metadata. Each email is assigned a thread_id (u64), allowing efficient grouping, retrieval, and display of conversations similar to how Gmail or Outlook organize threads.


Understanding Message-ID, In-Reply-To, and References

RustMailer relies on these standard email headers to reconstruct conversation threads:

  • Message-ID

    • A globally unique identifier for each email.
    • Serves as the primary key for threading — every message can be referenced by its Message-ID.
    • Example: <1234@example.com> uniquely identifies one email, regardless of folder or server.
  • In-Reply-To

    • Contains the Message-ID of the immediate parent message.
    • Used to attach a reply directly to the message it is responding to.
    • Example: If message B replies to message A, B's In-Reply-To header will contain A's Message-ID.
  • References

    • Contains an ordered list of ancestor Message-IDs for the entire conversation.
    • Helps reconstruct the full conversation history, even across multiple branches.
    • Example: If message C replies to B (which in turn replies to A), C's References might contain [A's ID, B's ID].

By combining these headers, RustMailer can reliably:

  1. Detect parent-child relationships between messages.
  2. Build a thread tree for each conversation.
  3. Handle missing or broken headers gracefully by falling back to Message-ID or subject heuristics.

Thread ID Computation

The thread_id is computed from available metadata using the following rules:

pub fn compute_thread_id(&self) -> u64 {
// If the email has both `In-Reply-To` and `References`,
// use the first reference as the thread anchor.
if self.in_reply_to.is_some() && self.references.as_ref().map_or(false, |r| !r.is_empty()) {
return calculate_hash!(&self.references.as_ref().unwrap()[0]);
}

// Otherwise, fall back to the email's own Message-ID.
if let Some(message_id) = self.message_id.as_ref() {
return calculate_hash!(message_id);
}

// If neither is available, generate a random fallback ID.
id!(128)
}
  • References/In-Reply-To

    • If present, the thread is anchored to the earliest message reference.
  • Message-ID

    • Used as a fallback when references are missing.
  • Random ID

    • Ensures uniqueness if metadata is incomplete.

This guarantees that every message belongs to exactly one thread.


Storage Model

  1. Email Metadata Table

    • Each email record includes a thread_id field (type: u64).
    • A secondary index is maintained on thread_id, enabling fast retrieval of all messages in a given thread.
  2. Latest Message Index

    • For each thread, the most recent email’s metadata is stored in a dedicated index table.
    • This allows efficient construction of paginated thread lists, where each entry represents the newest message in its thread.

API Evolution

Before v1.3.0

  • Only provided the endpoint:

    • /list-messages/:account_id

      • Lists all messages in chronological order.
      • Documentation: List Messages

Since v1.3.0

  • New Threading APIs:
  1. /list-threads/:account_id

    • Returns a paginated list of threads.
    • Each entry corresponds to the latest message in the thread.
    • Documentation: List Threads
  2. /get-thread-messages/:account_id

    • Fetches all messages belonging to a specific thread via thread_id.
    • Email bodies are retrieved separately based on metadata.
    • Documentation: Get Thread Messages

Usage Pattern

  • Thread List View

    • Fetch the latest message from each thread via /list-threads/:account_id.
    • Supports pagination for scalable UI rendering.
  • Thread Expansion

    • Use /get-thread-messages/:account_id to fetch all messages in a thread.
    • Actual email content bodies are retrieved lazily via metadata-driven APIs.

Benefits

  • Lightweight: Works entirely on metadata without requiring full message parsing.
  • Efficient Retrieval: Secondary indexing ensures fast access to thread contents.
  • Scalable: Separating “latest message” index supports Gmail/Outlook-style thread lists.
  • Extensible: Easy to integrate with UI/UX patterns like expandable conversations.