import { ImapFlow } from "imapflow";
const CONNECT_TIMEOUT = 10_000;
const COMMAND_TIMEOUT = 30_000;
/** Diagnostic logging to stderr (captured by claude-agent-stderr log on Pi) */
function diag(op, step, detail) {
    const ts = new Date().toISOString();
    console.error(`[email] ${op}: ${step}${detail ? ` — ${detail}` : ""} [${ts}]`);
}
/**
 * Safely close an IMAP client. Falls back to close() if logout() throws
 * (e.g., when a download stream was left open mid-consumption).
 */
async function safeLogout(client) {
    try {
        await client.logout();
    }
    catch {
        try {
            client.close();
        }
        catch { /* best effort */ }
    }
}
// ─── Hard cap for tool-layer limit enforcement ──────────────────────
export const MAX_LIMIT = 50;
/**
 * Resolve an IMAP folder name from a logical folder key.
 * - "inbox" → "INBOX"
 * - "sent" → discovered via \Sent special-use flag (RFC 6154)
 *
 * Throws if the requested folder cannot be found on this server.
 */
export async function resolveFolderName(client, folder) {
    if (folder === "inbox")
        return "INBOX";
    const mailboxes = await client.list();
    for (const mb of mailboxes) {
        if (mb.specialUse === "\\Sent") {
            return mb.path;
        }
    }
    throw new Error("Sent folder not found on this IMAP server — the server does not advertise a \\Sent special-use folder.");
}
function createClient(config, password) {
    return new ImapFlow({
        host: config.imapHost,
        port: config.imapPort,
        secure: config.imapSecurity === "tls",
        auth: {
            user: config.email,
            pass: password,
        },
        logger: false,
        tls: {
            rejectUnauthorized: true,
        },
        connectionTimeout: CONNECT_TIMEOUT,
        greetingTimeout: CONNECT_TIMEOUT,
        socketTimeout: COMMAND_TIMEOUT,
    });
}
/**
 * Strip HTML tags and decode common entities.
 * Used for extracting readable text from HTML-only email bodies.
 */
export function stripHtml(html) {
    return html
        .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
        .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
        .replace(/<[^>]+>/g, " ")
        .replace(/&amp;/g, "&")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&nbsp;/g, " ")
        .replace(/\s+/g, " ")
        .trim();
}
/** Max bytes to fetch per message body (4KB is plenty for a 500-char preview). */
const BODY_MAX_BYTES = 4096;
/** Max characters for body preview in tool output. */
const BODY_PREVIEW_LENGTH = 500;
/**
 * Decode a raw IMAP body part buffer according to its transfer encoding.
 * Returns a UTF-8 string. Falls back to raw UTF-8 for unknown encodings.
 */
export function decodeBodyPart(buf, encoding) {
    const enc = encoding?.toLowerCase().trim();
    if (!enc || enc === "7bit" || enc === "8bit" || enc === "binary") {
        return buf.toString("utf-8");
    }
    if (enc === "base64") {
        return Buffer.from(buf.toString("ascii").replace(/\s/g, ""), "base64").toString("utf-8");
    }
    if (enc === "quoted-printable") {
        return buf
            .toString("ascii")
            .replace(/=\r?\n/g, "")
            .replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
    }
    // Unknown encoding — best-effort UTF-8
    return buf.toString("utf-8");
}
/**
 * Extract a text body preview from a fetched message's inline bodyParts + bodyStructure.
 *
 * Determines the content type and transfer encoding of part "1" from
 * bodyStructure (single-part: structure itself, multipart: first childNode).
 * Skips non-text parts and multipart containers. Strips HTML. Truncates
 * to BODY_PREVIEW_LENGTH characters.
 */
export function extractBodyFromFetch(msg) {
    const buf = msg.bodyParts?.get("1");
    if (!buf || buf.length === 0)
        return "(body unavailable)";
    const structure = msg.bodyStructure;
    if (!structure)
        return "(body unavailable)";
    // Determine content type and encoding for IMAP part "1":
    // - Single-part message: bodyStructure itself describes the body
    // - Multipart message: first childNode describes part "1"
    let type;
    let encoding;
    if (structure.childNodes && structure.childNodes.length > 0) {
        const firstChild = structure.childNodes[0];
        type = firstChild.type?.toLowerCase();
        encoding = firstChild.encoding;
    }
    else {
        type = structure.type?.toLowerCase();
        encoding = structure.encoding;
    }
    // Skip non-text content (binary attachments, images, nested multipart containers)
    if (!type || !type.startsWith("text/"))
        return "(body unavailable)";
    let text = decodeBodyPart(buf, encoding);
    if (type === "text/html") {
        text = stripHtml(text);
    }
    text = text.trim();
    if (!text)
        return "(body unavailable)";
    return text.length > BODY_PREVIEW_LENGTH
        ? text.slice(0, BODY_PREVIEW_LENGTH) + "\u2026"
        : text;
}
/**
 * Test IMAP connectivity and authentication.
 * Returns null on success, error message on failure.
 */
export async function testConnection(config, password) {
    const client = createClient(config, password);
    try {
        await client.connect();
        await client.logout();
        return null;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg.includes("AUTHENTICATIONFAILED") || msg.includes("Invalid credentials") || msg.includes("LOGIN failed")) {
            return `IMAP authentication failed for ${config.email}. Check the password — most providers require an app-specific password when 2FA is enabled.`;
        }
        if (msg.includes("ECONNREFUSED")) {
            return `IMAP connection refused at ${config.imapHost}:${config.imapPort}. Check that IMAP is enabled in your email provider settings.`;
        }
        if (msg.includes("ENOTFOUND")) {
            return `IMAP host ${config.imapHost} not found. Check the hostname.`;
        }
        if (msg.includes("certificate") || msg.includes("TLS") || msg.includes("SSL")) {
            return `IMAP TLS error connecting to ${config.imapHost}:${config.imapPort}. Try port 993 (TLS) or 143 (STARTTLS).`;
        }
        if (msg.includes("ETIMEDOUT") || msg.includes("timeout")) {
            return `IMAP connection timed out to ${config.imapHost}:${config.imapPort}. Check the host and port.`;
        }
        return `IMAP connection failed: ${msg}`;
    }
}
// ─── Over-fetch multiplier for client-side TO filtering ─────────────
// When filtering by recipient client-side (instead of via IMAP SEARCH TO,
// which full-scans headers and times out on large mailboxes), we fetch
// more envelopes than requested to compensate for non-matching messages.
const TO_OVERFETCH_MULTIPLIER = 3;
// ─── Default since floor for unbounded alias searches ───────────────
const DEFAULT_SINCE_DAYS = 7;
/**
 * Check whether an IMAP envelope's TO field contains the target address.
 * Case-insensitive comparison. Strict: if the server didn't return TO
 * addresses, the message is excluded — we cannot confirm it was addressed
 * to the target.
 */
export function matchesRecipient(envelope, targetAddress) {
    if (!envelope.to || envelope.to.length === 0)
        return false;
    const target = targetAddress.toLowerCase();
    return envelope.to.some((addr) => addr.address?.toLowerCase() === target);
}
/**
 * Build IMAP SEARCH criteria for fetchRecent.
 * Never includes TO — that filter is applied client-side to avoid
 * full-mailbox header scans that timeout on large mailboxes.
 */
export function buildFetchRecentCriteria(options) {
    const { sender, since, before, to, limit = 20 } = options;
    const criteria = {};
    if (sender)
        criteria.from = sender;
    if (since)
        criteria.since = since;
    if (before)
        criteria.before = before;
    // TO is intentionally excluded — filtered client-side after fetch
    if (Object.keys(criteria).length === 0) {
        criteria.all = true;
    }
    return {
        criteria,
        clientSideTo: to,
        fetchLimit: to ? limit * TO_OVERFETCH_MULTIPLIER : limit,
    };
}
/**
 * Build IMAP SEARCH criteria for searchMessages.
 * Never includes TO — that filter is applied client-side.
 * When TO was the only filter (no other criteria provide bounding),
 * injects a since floor to prevent unbounded full-mailbox scans.
 */
export function buildSearchCriteria(options) {
    const { sender, subject, body, since, before, to } = options;
    const criteria = {};
    if (sender)
        criteria.from = sender;
    if (subject)
        criteria.subject = subject;
    if (body)
        criteria.body = body;
    if (since)
        criteria.since = since;
    if (before)
        criteria.before = before;
    // TO is intentionally excluded — filtered client-side after fetch
    // When TO was the only effective filter (nothing else bounds the search),
    // inject a date floor to prevent unbounded full-mailbox scans
    if (to && Object.keys(criteria).length === 0) {
        const sinceFloor = new Date();
        sinceFloor.setDate(sinceFloor.getDate() - DEFAULT_SINCE_DAYS);
        criteria.since = sinceFloor;
    }
    if (Object.keys(criteria).length === 0) {
        criteria.all = true;
    }
    return {
        criteria,
        clientSideTo: to,
    };
}
/**
 * Fetch recent messages from a mailbox folder.
 * Returns the most recent N messages with cursor-based pagination.
 *
 * TO filtering is applied client-side (not via IMAP SEARCH) to avoid
 * full-mailbox header scans that exceed socketTimeout on large mailboxes.
 */
export async function fetchRecent(config, password, options = {}) {
    const { limit = 20, sender, subjectPattern, since, before, to, folder = "inbox", beforeUid } = options;
    const client = createClient(config, password);
    // Compile subject regex once before the fetch loop
    let subjectRegex = null;
    if (subjectPattern) {
        try {
            subjectRegex = new RegExp(subjectPattern, "i");
        }
        catch { /* invalid — ignore */ }
    }
    const { criteria, clientSideTo, fetchLimit } = buildFetchRecentCriteria({
        sender, since, before, to, limit,
    });
    const t0 = Date.now();
    diag("fetchRecent", "start", `folder=${folder} criteria=${JSON.stringify(criteria)} fetchLimit=${fetchLimit} clientSideTo=${clientSideTo ?? "none"} beforeUid=${beforeUid ?? "none"}`);
    try {
        await client.connect();
        diag("fetchRecent", "connected", `${Date.now() - t0}ms`);
        const folderPath = await resolveFolderName(client, folder);
        diag("fetchRecent", "folder resolved", `${folder} → ${folderPath}`);
        const lock = await client.getMailboxLock(folderPath);
        diag("fetchRecent", "mailbox locked", `${Date.now() - t0}ms`);
        try {
            const t1 = Date.now();
            const searchResult = await client.search(criteria, { uid: true });
            let uids = searchResult || [];
            diag("fetchRecent", "search done", `${uids.length} UIDs in ${Date.now() - t1}ms (total ${Date.now() - t0}ms)`);
            // Apply cursor: keep only UIDs older than (less than) beforeUid
            if (beforeUid && beforeUid > 0) {
                uids = uids.filter((uid) => uid < beforeUid);
                diag("fetchRecent", "cursor applied", `${uids.length} UIDs after beforeUid=${beforeUid} filter`);
            }
            if (uids.length === 0)
                return { messages: [] };
            // Determine if more results exist beyond what we'll return
            const totalAfterCursor = uids.length;
            // Take the most recent UIDs (over-fetched when client-side TO filtering)
            const recentUids = uids.slice(-fetchLimit);
            const messages = [];
            const t2 = Date.now();
            const uidList = recentUids.map(String).join(",");
            diag("fetchRecent", "fetching envelopes", `UIDs=[${uidList}]`);
            let envelopeCount = 0;
            for await (const msg of client.fetch(uidList, {
                envelope: true,
                uid: true,
                bodyStructure: true,
                bodyParts: [{ key: "1", start: 0, maxLength: BODY_MAX_BYTES }],
            }, { uid: true })) {
                envelopeCount++;
                diag("fetchRecent", `envelope #${envelopeCount}`, `uid=${msg.uid} from=${msg.envelope?.from?.[0]?.address ?? "?"} at ${Date.now() - t2}ms`);
                // Client-side TO filter (replaces IMAP SEARCH TO)
                if (clientSideTo && !matchesRecipient(msg.envelope ?? {}, clientSideTo))
                    continue;
                const fromAddr = msg.envelope?.from?.[0]?.address ?? "(unknown)";
                const subject = msg.envelope?.subject ?? "(no subject)";
                const date = msg.envelope?.date?.toISOString() ?? "(unknown date)";
                const messageId = msg.envelope?.messageId ?? null;
                const inReplyTo = msg.envelope?.inReplyTo ?? null;
                if (subjectRegex && !subjectRegex.test(subject))
                    continue;
                // Stop once we have enough matches
                if (messages.length >= limit)
                    break;
                const bodyPreview = extractBodyFromFetch(msg);
                diag("fetchRecent", `body #${envelopeCount}`, `uid=${msg.uid} preview=${bodyPreview.length}chars type=${msg.bodyStructure?.type ?? "?"}`);
                messages.push({
                    uid: msg.uid,
                    messageId,
                    inReplyTo,
                    from: fromAddr,
                    subject,
                    date,
                    bodyPreview,
                });
            }
            diag("fetchRecent", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
            // Return newest first
            messages.reverse();
            // Calculate pagination cursor: use the oldest UID in the *fetched* set
            // (not the filtered set) to avoid skipping messages between filtered-out
            // and kept messages when client-side TO/subject filtering is active
            const result = { messages };
            if (totalAfterCursor > fetchLimit && recentUids.length > 0) {
                const oldestFetchedUid = recentUids[0]; // UIDs are sorted ascending
                result.nextBeforeUid = oldestFetchedUid;
                diag("fetchRecent", "pagination", `nextBeforeUid=${oldestFetchedUid}`);
            }
            else {
                diag("fetchRecent", "pagination", "exhausted");
            }
            return result;
        }
        finally {
            lock.release();
        }
    }
    catch (err) {
        diag("fetchRecent", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
        throw err;
    }
    finally {
        await safeLogout(client);
    }
}
/**
 * Search a mailbox folder by query criteria with cursor-based pagination.
 */
export async function searchMessages(config, password, query) {
    const { sender, subject, body, since, before, limit = 20, to, folder = "inbox", beforeUid } = query;
    const client = createClient(config, password);
    // When client-side TO filtering is active, overfetch to compensate for non-matching messages
    const fetchLimit = to ? limit * TO_OVERFETCH_MULTIPLIER : limit;
    const { criteria, clientSideTo } = buildSearchCriteria({
        sender, subject, body, since, before, to,
    });
    const t0 = Date.now();
    diag("searchMessages", "start", `folder=${folder} criteria=${JSON.stringify(criteria)} clientSideTo=${clientSideTo ?? "none"} beforeUid=${beforeUid ?? "none"}`);
    try {
        await client.connect();
        diag("searchMessages", "connected", `${Date.now() - t0}ms`);
        const folderPath = await resolveFolderName(client, folder);
        diag("searchMessages", "folder resolved", `${folder} → ${folderPath}`);
        const lock = await client.getMailboxLock(folderPath);
        diag("searchMessages", "mailbox locked", `${Date.now() - t0}ms`);
        try {
            const t1 = Date.now();
            const searchResult = await client.search(criteria, { uid: true });
            let uids = searchResult || [];
            diag("searchMessages", "search done", `${uids.length} UIDs in ${Date.now() - t1}ms (total ${Date.now() - t0}ms)`);
            // Apply cursor: keep only UIDs older than (less than) beforeUid
            if (beforeUid && beforeUid > 0) {
                uids = uids.filter((uid) => uid < beforeUid);
                diag("searchMessages", "cursor applied", `${uids.length} UIDs after beforeUid=${beforeUid} filter`);
            }
            if (uids.length === 0)
                return { messages: [] };
            const totalAfterCursor = uids.length;
            const recentUids = uids.slice(-fetchLimit);
            const messages = [];
            const t2 = Date.now();
            let envelopeCount = 0;
            for await (const msg of client.fetch(recentUids.map(String).join(","), {
                envelope: true,
                uid: true,
                bodyStructure: true,
                bodyParts: [{ key: "1", start: 0, maxLength: BODY_MAX_BYTES }],
            }, { uid: true })) {
                envelopeCount++;
                // Client-side TO filter (replaces IMAP SEARCH TO)
                if (clientSideTo && !matchesRecipient(msg.envelope ?? {}, clientSideTo))
                    continue;
                const fromAddr = msg.envelope?.from?.[0]?.address ?? "(unknown)";
                const subj = msg.envelope?.subject ?? "(no subject)";
                const date = msg.envelope?.date?.toISOString() ?? "(unknown date)";
                const messageId = msg.envelope?.messageId ?? null;
                const inReplyTo = msg.envelope?.inReplyTo ?? null;
                // Stop once we have enough matches
                if (messages.length >= limit)
                    break;
                const bodyPreview = extractBodyFromFetch(msg);
                messages.push({
                    uid: msg.uid,
                    messageId,
                    inReplyTo,
                    from: fromAddr,
                    subject: subj,
                    date,
                    bodyPreview,
                });
            }
            diag("searchMessages", "fetch done", `${envelopeCount} envelopes, ${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
            messages.reverse();
            const result = { messages };
            if (totalAfterCursor > fetchLimit && recentUids.length > 0) {
                const oldestFetchedUid = recentUids[0]; // UIDs are sorted ascending
                result.nextBeforeUid = oldestFetchedUid;
                diag("searchMessages", "pagination", `nextBeforeUid=${oldestFetchedUid}`);
            }
            else {
                diag("searchMessages", "pagination", "exhausted");
            }
            return result;
        }
        finally {
            lock.release();
        }
    }
    catch (err) {
        diag("searchMessages", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
        throw err;
    }
    finally {
        await safeLogout(client);
    }
}
/**
 * Fetch emails from INBOX since a given UID (exclusive) for incremental polling.
 * Returns the UIDVALIDITY and all matching messages with UID > sinceUid.
 *
 * When sinceUid is 0 (first poll), fetches emails from the last 7 days
 * to avoid pulling the entire mailbox on large accounts.
 *
 * Client-side TO filtering is applied when recipientAddress differs
 * from the auth email (alias/catchall scenarios).
 */
export async function fetchSinceUid(config, password, sinceUid, recipientAddress) {
    const client = createClient(config, password);
    const t0 = Date.now();
    const isFirstPoll = sinceUid === 0;
    const filterTo = recipientAddress.toLowerCase() !== config.email.toLowerCase();
    diag("fetchSinceUid", "start", `sinceUid=${sinceUid} firstPoll=${isFirstPoll} filterTo=${filterTo ? recipientAddress : "none"}`);
    try {
        await client.connect();
        diag("fetchSinceUid", "connected", `${Date.now() - t0}ms`);
        const mailbox = await client.mailboxOpen("INBOX");
        const uidValidity = Number(mailbox.uidValidity);
        diag("fetchSinceUid", "mailbox open", `uidValidity=${uidValidity} exists=${mailbox.exists}`);
        if (mailbox.exists === 0) {
            return { uidValidity, emails: [], maxUid: 0 };
        }
        // Build search criteria
        const criteria = {};
        if (isFirstPoll) {
            // First poll: fetch last 7 days only
            const sinceDate = new Date();
            sinceDate.setDate(sinceDate.getDate() - DEFAULT_SINCE_DAYS);
            criteria.since = sinceDate;
        }
        else {
            // Incremental: UID > sinceUid
            criteria.uid = `${sinceUid + 1}:*`;
        }
        const t1 = Date.now();
        const searchResult = await client.search(criteria, { uid: true });
        const uids = searchResult || [];
        diag("fetchSinceUid", "search done", `${uids.length} UIDs in ${Date.now() - t1}ms`);
        if (uids.length === 0) {
            return { uidValidity, emails: [], maxUid: sinceUid };
        }
        const emails = [];
        let maxUid = sinceUid;
        const t2 = Date.now();
        const uidList = uids.map(String).join(",");
        for await (const msg of client.fetch(uidList, {
            envelope: true,
            uid: true,
            bodyStructure: true,
            bodyParts: [{ key: "1", start: 0, maxLength: BODY_MAX_BYTES }],
        }, { uid: true })) {
            // Track highest UID for high-water mark
            const uid = Number(msg.uid);
            if (uid > maxUid)
                maxUid = uid;
            // Client-side TO filter for alias/catchall accounts
            if (filterTo && !matchesRecipient(msg.envelope ?? {}, recipientAddress))
                continue;
            const fromAddr = msg.envelope?.from?.[0]?.address ?? "(unknown)";
            const fromName = msg.envelope?.from?.[0]?.name ?? null;
            const subject = msg.envelope?.subject ?? "(no subject)";
            const date = msg.envelope?.date?.toISOString() ?? new Date().toISOString();
            const messageId = msg.envelope?.messageId ?? null;
            const inReplyTo = msg.envelope?.inReplyTo ?? null;
            const toAddresses = (msg.envelope?.to ?? []).map((a) => a.address).filter((a) => !!a);
            const ccAddresses = (msg.envelope?.cc ?? []).map((a) => a.address).filter((a) => !!a);
            const bodyPreview = extractBodyFromFetch(msg);
            emails.push({ uid, messageId, inReplyTo, toAddresses, ccAddresses, fromAddress: fromAddr, fromName, subject, date, bodyPreview });
        }
        diag("fetchSinceUid", "fetch done", `${emails.length} emails (maxUid=${maxUid}) in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
        return { uidValidity, emails, maxUid };
    }
    catch (err) {
        diag("fetchSinceUid", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
        throw err;
    }
    finally {
        await safeLogout(client);
    }
}
/**
 * Get the inbox message count.
 */
export async function getInboxCount(config, password) {
    const client = createClient(config, password);
    try {
        await client.connect();
        const mailbox = await client.mailboxOpen("INBOX");
        return mailbox.exists;
    }
    finally {
        await safeLogout(client);
    }
}
// ─── Auto-respond types and fetch ────────────────────────────────────
/** Headers used to detect auto-replies (RFC 3834) and mailing lists. */
const AUTO_REPLY_HEADER_FIELDS = [
    "Auto-Submitted",
    "X-Auto-Response-Suppress",
    "Precedence",
    "List-Unsubscribe",
    "List-Id",
];
/**
 * Parse raw IMAP header text into a map of lowercased header names → values.
 * Handles multi-line continuation (lines starting with whitespace).
 */
export function parseRawHeaders(raw) {
    const headers = new Map();
    const lines = raw.split(/\r?\n/);
    let currentKey = "";
    let currentValue = "";
    for (const line of lines) {
        if (line === "")
            break; // End of headers
        if (/^\s/.test(line)) {
            // Continuation of previous header
            currentValue += " " + line.trim();
        }
        else {
            // Flush previous header
            if (currentKey) {
                const existing = headers.get(currentKey) ?? [];
                existing.push(currentValue);
                headers.set(currentKey, existing);
            }
            const colonIdx = line.indexOf(":");
            if (colonIdx > 0) {
                currentKey = line.slice(0, colonIdx).trim().toLowerCase();
                currentValue = line.slice(colonIdx + 1).trim();
            }
            else {
                currentKey = "";
                currentValue = "";
            }
        }
    }
    // Flush last header
    if (currentKey) {
        const existing = headers.get(currentKey) ?? [];
        existing.push(currentValue);
        headers.set(currentKey, existing);
    }
    return headers;
}
/**
 * Detect whether a message is an auto-reply based on RFC 3834 headers.
 *
 * Checks:
 * - Auto-Submitted: any value other than "no"
 * - X-Auto-Response-Suppress: present
 * - Precedence: bulk, junk, or list
 * - List-Unsubscribe or List-Id: present (mailing list)
 */
export function detectAutoReply(headers) {
    const autoSubmitted = headers.get("auto-submitted");
    if (autoSubmitted && autoSubmitted.some((v) => v.trim().toLowerCase() !== "no")) {
        return true;
    }
    if (headers.has("x-auto-response-suppress"))
        return true;
    const precedence = headers.get("precedence");
    if (precedence) {
        const val = precedence[0]?.trim().toLowerCase();
        if (val === "bulk" || val === "junk" || val === "list")
            return true;
    }
    if (headers.has("list-unsubscribe") || headers.has("list-id"))
        return true;
    return false;
}
/**
 * Fetch new messages for auto-respond processing.
 *
 * Returns messages with UIDs greater than `sinceUid`, filtered by the
 * agent's address (TO header). Includes envelope, body, and auto-reply
 * detection headers in a single IMAP FETCH.
 *
 * Unlike fetchRecent (optimised for tool output with pagination),
 * this function fetches everything needed for auto-respond in one pass:
 * envelope for threading, body for agent input, headers for loop guards.
 */
export async function fetchForAutoRespond(config, password, options) {
    const { sinceUid, agentAddress, limit = 20 } = options;
    const client = createClient(config, password);
    const t0 = Date.now();
    diag("fetchForAutoRespond", "start", `sinceUid=${sinceUid} agentAddress=${agentAddress} limit=${limit}`);
    try {
        await client.connect();
        diag("fetchForAutoRespond", "connected", `${Date.now() - t0}ms`);
        const lock = await client.getMailboxLock("INBOX");
        diag("fetchForAutoRespond", "mailbox locked", `${Date.now() - t0}ms`);
        try {
            // Search for messages with UID > sinceUid
            const criteria = {};
            if (sinceUid > 0) {
                criteria.uid = `${sinceUid + 1}:*`;
            }
            else {
                // First poll: only fetch recent messages (last 7 days)
                const since = new Date();
                since.setDate(since.getDate() - DEFAULT_SINCE_DAYS);
                criteria.since = since;
            }
            const t1 = Date.now();
            const searchResult = await client.search(criteria, { uid: true });
            const uids = searchResult || [];
            diag("fetchForAutoRespond", "search done", `${uids.length} UIDs in ${Date.now() - t1}ms`);
            if (uids.length === 0)
                return [];
            // Take the oldest N UIDs (process in chronological order)
            const processUids = uids.slice(0, limit);
            const messages = [];
            const t2 = Date.now();
            const headerSection = `HEADER.FIELDS (${AUTO_REPLY_HEADER_FIELDS.join(" ")})`;
            for await (const msg of client.fetch(processUids.map(String).join(","), {
                envelope: true,
                uid: true,
                bodyStructure: true,
                bodyParts: [
                    { key: "1", start: 0, maxLength: BODY_MAX_BYTES },
                ],
                headers: AUTO_REPLY_HEADER_FIELDS.map((h) => h.toLowerCase()),
            }, { uid: true })) {
                // Client-side TO filter: only messages addressed to the agent
                if (!matchesRecipient(msg.envelope ?? {}, agentAddress))
                    continue;
                const envelope = msg.envelope ?? {};
                const fromAddr = envelope.from?.[0]?.address ?? "";
                const toAddrs = (envelope.to ?? []).map((a) => a.address ?? "");
                const subject = envelope.subject ?? "(no subject)";
                const date = envelope.date?.toISOString() ?? new Date().toISOString();
                const messageId = envelope.messageId ?? "";
                const inReplyTo = envelope.inReplyTo ?? null;
                // Extract References from envelope (not available in all IMAP servers)
                // Fall back to null — threading still works with just In-Reply-To
                const references = null;
                // Extract body
                const body = extractBodyFromFetch(msg);
                // Detect auto-reply from raw headers (Buffer from ImapFlow)
                const rawHeaders = msg.headers ? msg.headers.toString("utf-8") : "";
                const headerMap = parseRawHeaders(rawHeaders);
                const isAutoReply = detectAutoReply(headerMap);
                messages.push({
                    uid: msg.uid,
                    messageId,
                    from: fromAddr,
                    to: toAddrs,
                    subject,
                    date,
                    inReplyTo,
                    references,
                    body,
                    isAutoReply,
                });
            }
            diag("fetchForAutoRespond", "fetch done", `${messages.length} messages in ${Date.now() - t2}ms (total ${Date.now() - t0}ms)`);
            return messages;
        }
        finally {
            lock.release();
        }
    }
    catch (err) {
        diag("fetchForAutoRespond", "FAILED", `${err instanceof Error ? err.message : String(err)} after ${Date.now() - t0}ms`);
        throw err;
    }
    finally {
        await safeLogout(client);
    }
}
//# sourceMappingURL=imap.js.map