get_thread
Retrieve all emails in the same conversation thread as a specified email using subject matching and header analysis. Works with iCloud Mail to group related messages for better email organization.
Instructions
Find all emails in the same thread as a given email. Uses subject matching + References/In-Reply-To header filtering. Note: iCloud does not support server-side threading — results are approximate.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| uid | Yes | Email UID to find the thread for | |
| mailbox | No | Mailbox to search (default INBOX) |
Implementation Reference
- lib/imap.js:1135-1228 (handler)Implementation of the get_thread function which retrieves email threads by matching Message-ID, In-Reply-To, and References headers within a set of candidates found via subject search.
export async function getThread(uid, mailbox = 'INBOX', creds = null) { const THREAD_CANDIDATE_CAP = 100; const client = createRateLimitedClient(creds); await client.connect(); await client.mailboxOpen(mailbox); // Fetch target email's envelope + raw headers for threading const meta = await client.fetchOne(uid, { envelope: true, flags: true, headers: new Set(['references', 'in-reply-to']) }, { uid: true }); if (!meta) throw new Error(`Email UID ${uid} not found`); const targetMessageId = meta.envelope?.messageId ?? null; const rawRefs = extractRawHeader(meta.headers, 'references'); const rawInReplyTo = extractRawHeader(meta.headers, 'in-reply-to'); // Build full reference set for this email const threadRefs = new Set(); if (targetMessageId) threadRefs.add(targetMessageId.trim()); if (rawInReplyTo) threadRefs.add(rawInReplyTo.trim()); if (rawRefs) { rawRefs.split(/\s+/).filter(s => s.startsWith('<') && s.endsWith('>')).forEach(r => threadRefs.add(r)); } const normalizedSubject = stripSubjectPrefixes(meta.envelope?.subject ?? ''); // SEARCH SUBJECT for candidates (iCloud doesn't support SEARCH HEADER) let candidateUids = []; if (normalizedSubject) { const raw = await client.search({ subject: normalizedSubject }, { uid: true }); candidateUids = Array.isArray(raw) ? raw : []; } const candidatesCapped = candidateUids.length > THREAD_CANDIDATE_CAP; if (candidatesCapped) candidateUids = candidateUids.slice(-THREAD_CANDIDATE_CAP); // Fetch envelopes + headers for candidates to filter by References overlap const threadEmails = []; if (candidateUids.length > 0) { for await (const msg of client.fetch(candidateUids, { envelope: true, flags: true, headers: new Set(['references', 'in-reply-to']) }, { uid: true })) { const msgId = msg.envelope?.messageId ?? null; const msgRefs = extractRawHeader(msg.headers, 'references'); const msgInReplyTo = extractRawHeader(msg.headers, 'in-reply-to'); // Build this message's reference set const msgRefSet = new Set(); if (msgId) msgRefSet.add(msgId.trim()); if (msgInReplyTo) msgRefSet.add(msgInReplyTo.trim()); if (msgRefs) msgRefs.split(/\s+/).filter(s => s.startsWith('<')).forEach(r => msgRefSet.add(r)); // Include if there's any Reference chain overlap const hasOverlap = (msgId && threadRefs.has(msgId.trim())) || [...threadRefs].some(r => msgRefSet.has(r)); if (hasOverlap) { threadEmails.push({ uid: msg.uid, subject: msg.envelope?.subject, from: msg.envelope?.from?.[0]?.address, date: msg.envelope?.date, seen: msg.flags?.has('\\Seen') ?? false, flagged: msg.flags?.has('\\Flagged') ?? false, messageId: msgId }); } } } await client.logout(); // Sort by date ascending threadEmails.sort((a, b) => { const da = a.date ? new Date(a.date).getTime() : 0; const db = b.date ? new Date(b.date).getTime() : 0; return da - db; }); return { uid, subject: normalizedSubject || meta.envelope?.subject, count: threadEmails.length, emails: threadEmails, ...(candidatesCapped && { candidatesCapped: true, note: `Subject search returned more than ${THREAD_CANDIDATE_CAP} candidates — thread results may be incomplete` }) }; }