search_emails
Search iCloud Mail by keywords or specific fields like sender, subject, or date with filters for unread status, attachments, and domain.
Instructions
Search emails by keyword or targeted field queries, with optional filters for date, read status, domain, and more
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Search keyword (matches subject, sender, body — use OR across all fields) | |
| subjectQuery | No | Match only in subject field | |
| bodyQuery | No | Match only in body field | |
| fromQuery | No | Match only in from/sender field | |
| queryMode | No | How to combine subjectQuery/bodyQuery/fromQuery: or (default) or and | |
| mailbox | No | Mailbox to search (default INBOX) | |
| limit | No | Max results (default 10) | |
| includeSnippet | No | If true, include a 200-char body preview snippet for each result (max 10 emails) | |
| sender | No | Match exact sender email address | |
| domain | No | Match any sender from this domain (e.g. substack.com) | |
| subject | No | Keyword to match in subject | |
| before | No | Only emails before this date (YYYY-MM-DD) | |
| since | No | Only emails since this date (YYYY-MM-DD) | |
| unread | No | True for unread only, false for read only | |
| flagged | No | True for flagged only, false for unflagged only | |
| larger | No | Only emails larger than this size in KB | |
| smaller | No | Only emails smaller than this size in KB | |
| hasAttachment | No | Only emails with attachments (client-side BODYSTRUCTURE scan — must be combined with other filters that narrow results to under 500 emails first) | |
| account | No | Account name to use (e.g. 'icloud', 'gmail'). Defaults to first configured account. Use list_accounts to see available accounts. |
Implementation Reference
- lib/imap.js:1583-1669 (handler)The handler function 'searchEmails' implements searching for emails using IMAP based on queries and filters.
export async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {}, options = {}, creds = null) { const { queryMode = 'or', subjectQuery, bodyQuery, fromQuery, includeSnippet = false } = options; const client = createRateLimitedClient(creds); await client.connect(); await client.mailboxOpen(mailbox); // Build text query let textQuery; const targetedParts = []; if (subjectQuery) targetedParts.push({ subject: subjectQuery }); if (bodyQuery) targetedParts.push({ body: bodyQuery }); if (fromQuery) targetedParts.push({ from: fromQuery }); if (targetedParts.length > 0) { // Targeted field queries if (queryMode === 'and') { textQuery = Object.assign({}, ...targetedParts); // IMAP AND is implicit } else { textQuery = targetedParts.length === 1 ? targetedParts[0] : { or: targetedParts }; } } else if (query) { // Original OR across subject/from/body textQuery = { or: [{ subject: query }, { from: query }, { body: query }] }; } else { textQuery = null; } const extraQuery = buildQuery(filters); const hasExtra = Object.keys(extraQuery).length > 0 && !extraQuery.all; const finalQuery = textQuery ? (hasExtra ? { ...textQuery, ...extraQuery } : textQuery) : (hasExtra ? extraQuery : { all: true }); let uids = (await client.search(finalQuery, { uid: true })) ?? []; if (!Array.isArray(uids)) uids = []; if (filters.hasAttachment) { if (uids.length > ATTACHMENT_SCAN_LIMIT) { await client.logout(); return { total: null, showing: 0, emails: [], error: `hasAttachment requires narrower filters first — ${uids.length} candidates exceeds scan limit of ${ATTACHMENT_SCAN_LIMIT}. Add from/since/before/subject filters to reduce the set.` }; } uids = await filterUidsByAttachment(client, uids); } const emails = []; const recentUids = uids.slice(-limit).reverse(); for (const uid of recentUids) { const msg = await client.fetchOne(uid, { envelope: true, flags: true }, { uid: true }); if (msg) { emails.push({ uid, subject: msg.envelope.subject, from: msg.envelope.from?.[0]?.address, date: msg.envelope.date, flagged: msg.flags.has('\\Flagged'), seen: msg.flags.has('\\Seen') }); } } // Fetch body snippets if requested (max 10 emails to avoid timeout) if (includeSnippet && emails.length > 0) { for (const email of emails.slice(0, 10)) { try { const meta = await client.fetchOne(email.uid, { bodyStructure: true }, { uid: true }); if (!meta?.bodyStructure) continue; const textPart = findTextPart(meta.bodyStructure); if (!textPart) continue; const imapKey = textPart.partId ?? 'TEXT'; const partMsg = await client.fetchOne(email.uid, { bodyParts: [{ key: imapKey, start: 0, maxLength: 400 }] }, { uid: true }); const buf = partMsg?.bodyParts?.get(imapKey) ?? partMsg?.bodyParts?.get(imapKey.toUpperCase()) ?? partMsg?.bodyParts?.get(imapKey.toLowerCase()); if (!buf) continue; const decoded = decodeTransferEncoding(buf, textPart.encoding); let text = await decodeCharset(decoded, textPart.charset); if (textPart.type === 'text/html') text = stripHtml(text); email.snippet = text.replace(/\s+/g, ' ').slice(0, 200).trim(); } catch { /* skip snippet on error */ } } } await client.logout(); return { total: uids.length, showing: emails.length, emails }; }