import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
initStderrTee("email");
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { emailSetup } from "./tools/email-setup.js";
import { emailRead } from "./tools/email-read.js";
import { emailSend } from "./tools/email-send.js";
import { emailSearch } from "./tools/email-search.js";
import { emailOtpExtract } from "./tools/email-otp-extract.js";
import { emailStatus } from "./tools/email-status.js";
import { emailReply } from "./tools/email-reply.js";
import { emailGraphQuery } from "./tools/email-graph-query.js";
import { emailFetch } from "./tools/email-fetch.js";
import { emailIngest } from "./tools/email-ingest.js";
import { describeProvider } from "./lib/providers.js";
import { closeDriver } from "./lib/neo4j.js";
const server = new McpServer({
    name: "maxy-email",
    version: "0.1.0",
});
// Plugin-system boot tolerance — see Task 202.
// Module-init must never throw; plugin-system spawn enumerates tools/list
// before ACCOUNT_ID is set. Per-call refusal preserves account isolation.
const accountId = process.env.ACCOUNT_ID ?? null;
process.stderr.write(`[email] boot accountId=${accountId ?? "null"}\n`);
function refuseNoAccount(toolName) {
    process.stderr.write(`[email] tool=${toolName} refuse reason=no-account-context\n`);
    return {
        content: [
            {
                type: "text",
                text: `${toolName} cannot execute: this MCP instance was launched without ACCOUNT_ID (plugin-system metadata-only mode). Per-account writes require ACCOUNT_ID at spawn.`,
            },
        ],
        isError: true,
    };
}
// ===================================================================
// email-provider-info — pre-password provider lookup
// ===================================================================
// Pure inference over the static provider map: no graph, no credentials,
// no per-account state. Intentionally does NOT call refuseNoAccount — it
// must answer in plugin-system metadata-only mode too, and there is nothing
// account-scoped to protect. Call this BEFORE asking for any password so the
// provider's app-password / IMAP-enable requirement is surfaced first.
eagerTool(server, "email-provider-info", "Given just an email address, return the provider's IMAP/SMTP host settings and — crucially — its app-password / trusted-application / IMAP-enable requirement, BEFORE any password is requested. Most providers (Gmail, iCloud, StartMail, Fastmail, etc.) reject the normal login password and need a credential minted in the provider's own settings UI. Relay that requirement to the operator first, then ask for the app password. Returns 'unknown' (operator must supply hosts) or 'unsupported' (provider blocks IMAP/SMTP, e.g. Tuta) where applicable.", {
    email: z.string().email().describe("The email address to look up (only the domain is used)."),
}, async ({ email }) => {
    const info = describeProvider(email);
    let text;
    if (info.status === "known") {
        const d = info.defaults;
        const lines = [
            `Provider recognised for ${info.domain}. Inferred connection settings:`,
            `  IMAP: ${d.imapHost}:${d.imapPort} (${d.imapSecurity})`,
            `  SMTP: ${d.smtpHost}:${d.smtpPort} (${d.smtpSecurity})`,
        ];
        if (info.notes) {
            lines.push(`\nApp-password / setup requirement (relay this BEFORE asking for a password): ${info.notes}`);
        }
        else {
            lines.push(`\nNo special app-password step is known for this provider — the account password usually works. If the connection later fails on authentication, the operator may still need to enable IMAP or mint an app password in the provider's settings.`);
        }
        lines.push(`\nThe hosts above are inferred — pass only the password (and agentAddress if it differs) to email-setup; the hosts do not need to be re-entered.`);
        text = lines.join("\n");
    }
    else if (info.status === "unsupported") {
        text = info.reason;
    }
    else {
        text = `No host settings are known for ${info.domain ?? "this address"}. Ask the operator for the IMAP host/port and SMTP host/port (their provider's "mail client" or "IMAP/SMTP" help page lists these), along with the password or app-specific password, then pass all of them to email-setup.`;
    }
    return { content: [{ type: "text", text }] };
});
// ===================================================================
// email-setup — collect and validate email credentials
// ===================================================================
eagerTool(server, "email-setup", "Set up the agent's dedicated email account. Validates IMAP and SMTP connectivity before storing credentials. Infers host settings from known providers (Gmail, Outlook, iCloud, Yahoo, Protonmail).", {
    email: z.string().email().describe("The email address for the agent's account (used for IMAP/SMTP authentication)"),
    password: z.string().min(1).describe("The password or app-specific password"),
    agentAddress: z.string().email().optional().describe("The address the agent operates as (for catchall domains where auth email differs from the agent's alias). Defaults to email if not set."),
    imapHost: z.string().optional().describe("IMAP server hostname (auto-inferred for known providers)"),
    imapPort: z.number().int().optional().describe("IMAP port (default: 993 for TLS)"),
    imapSecurity: z.enum(["tls", "starttls"]).optional().describe("IMAP security mode (default: tls)"),
    smtpHost: z.string().optional().describe("SMTP server hostname (auto-inferred for known providers)"),
    smtpPort: z.number().int().optional().describe("SMTP port (default: 587 for STARTTLS)"),
    smtpSecurity: z.enum(["tls", "starttls"]).optional().describe("SMTP security mode (default: starttls)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-setup");
    try {
        const result = await emailSetup({ ...params, accountId });
        const parts = [result.message];
        if (result.providerNotes) {
            parts.push(`\nProvider note: ${result.providerNotes}`);
        }
        return {
            content: [{ type: "text", text: parts.join("") }],
            isError: !result.success,
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Email setup failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-read — fetch recent inbox messages
// ===================================================================
eagerTool(server, "email-read", "List recent messages (from, subject, date — no body content). Supports filtering by sender, subject, date range, and recipient. Supports pagination via before_uid cursor and folder selection (inbox or sent). Max 50 messages per request.", {
    sender: z.string().optional().describe("Filter by sender address or domain"),
    subjectPattern: z.string().optional().describe("Regex pattern to match subject lines"),
    since: z.string().optional().describe("ISO date — only messages after this date"),
    before: z.string().optional().describe("ISO date — only messages before this date"),
    to: z.string().optional().describe("Filter by recipient address"),
    folder: z.enum(["inbox", "sent"]).optional().describe("Mailbox folder (default: inbox)"),
    before_uid: z.coerce.number().int().optional().describe("Pagination cursor — return messages older than this UID (from previous response's next_before_uid)"),
    limit: z.coerce.number().int().optional().describe("Max messages to return (default: 20, max: 50)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-read");
    try {
        const text = await emailRead({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Failed to read email: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-send — send an email via SMTP
// ===================================================================
eagerTool(server, "email-send", "Send an email from the agent's email account via SMTP.", {
    to: z.string().email().describe("Recipient email address"),
    subject: z.string().describe("Email subject line"),
    body: z.string().describe("Plain text email body"),
    html: z.string().optional().describe("Optional HTML email body"),
    attachments: z
        .array(z.string())
        .optional()
        .describe("Absolute local file paths to attach as SMTP attachments. Each must be a regular file inside the account directory and ≤25 MB. Omit for a text-only message."),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-send");
    try {
        const text = await emailSend({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Failed to send email: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-search — search inbox by criteria
// ===================================================================
eagerTool(server, "email-search", "Search messages by sender, subject, body, or date range. Returns message list (from, subject, date — no body content). Supports pagination via before_uid cursor and folder selection (inbox or sent). Max 50 messages per request.", {
    sender: z.string().optional().describe("Search by sender address or domain"),
    subject: z.string().optional().describe("Search by subject text"),
    body: z.string().optional().describe("Search by keyword in body"),
    since: z.string().optional().describe("ISO date — only messages after this date"),
    before: z.string().optional().describe("ISO date — only messages before this date"),
    to: z.string().optional().describe("Filter by recipient address"),
    folder: z.enum(["inbox", "sent"]).optional().describe("Mailbox folder (default: inbox)"),
    before_uid: z.coerce.number().int().optional().describe("Pagination cursor — return messages older than this UID (from previous response's next_before_uid)"),
    limit: z.coerce.number().int().optional().describe("Max results (default: 20, max: 50)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-search");
    try {
        const text = await emailSearch({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Email search failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-otp-extract — poll for and extract OTP verification codes
// ===================================================================
eagerTool(server, "email-otp-extract", "Poll the agent's email inbox for a verification code (OTP) from a specific sender. Searches recent messages first, then polls at intervals until a matching email arrives or the timeout is reached. Used by other skills during service authentication.", {
    sender: z.string().describe("Expected sender domain (e.g. cloudflare.com, stripe.com)"),
    subjectPattern: z.string().optional().describe("Optional regex pattern for subject line"),
    timeout: z.number().int().optional().describe("Seconds to poll before giving up (default: 60)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-otp-extract");
    try {
        const result = await emailOtpExtract({ ...params, accountId });
        return {
            content: [{ type: "text", text: result.message }],
            isError: !result.success,
        };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `OTP extraction failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-status — check email connection health
// ===================================================================
eagerTool(server, "email-status", "Check the health of the agent's email configuration — IMAP and SMTP connectivity, authentication validity, and inbox status.", {}, async () => {
    if (!accountId)
        return refuseNoAccount("email-status");
    try {
        const text = await emailStatus({ accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Email status check failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-reply — reply to an existing email in-thread
// ===================================================================
eagerTool(server, "email-reply", "Reply to an email in-thread. Looks up the original message by Message-ID via IMAP (searching INBOX then Sent), sets In-Reply-To and References headers for correct threading, and sends via SMTP. Use this instead of email-send when responding to a received email.", {
    messageId: z.string().min(1).describe("The Message-ID of the email being replied to (from email-read output)"),
    body: z.string().min(1).describe("Plain text reply body"),
    html: z.string().optional().describe("Optional HTML reply body"),
    replyAll: z.boolean().optional().describe("If true, reply to all original recipients (To + CC). Default: false (reply to sender only)."),
    attachments: z
        .array(z.string())
        .optional()
        .describe("Absolute local file paths to attach as SMTP attachments. Each must be a regular file inside the account directory and ≤25 MB. Omit for a text-only reply."),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-reply");
    try {
        const text = await emailReply({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Failed to send reply: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-graph-query — query stored email :ConversationArchive rows in the graph
// ===================================================================
eagerTool(server, "email-graph-query", "Query email threads stored in the graph as :ConversationArchive {source:'email'} with :Section chunks. Without a query, lists archives sorted by last-message date (filterable by participant address and date range). With a query, runs semantic search over :Section chunks and ranks parent archives. Use this for email history and recall — not for checking the live inbox (use email-read/email-search for that).", {
    query: z.string().optional().describe("Natural-language search query (e.g. 'threads about cabbages'). When omitted, lists all stored email archives."),
    sender: z.string().optional().describe("Filter by participant email (case-insensitive substring match against :Person/:AdminUser linked via :PARTICIPANT_IN)"),
    dateFrom: z.string().optional().describe("ISO date — only archives with at least one :Section whose lastMessageAt is on or after this date"),
    dateTo: z.string().optional().describe("ISO date — only archives with at least one :Section whose firstMessageAt is on or before this date"),
    offset: z.coerce.number().int().optional().describe("Pagination offset for list mode (default: 0)"),
    limit: z.coerce.number().int().optional().describe("Max results to return (default: 20, max: 50)"),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-graph-query");
    try {
        const text = await emailGraphQuery({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Email graph query failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-fetch — operator-initiated inbox triage (no graph writes)
// ===================================================================
eagerTool(server, "email-fetch", "List new inbox messages since the last ingested UID, metadata + body preview only. Writes nothing to the graph. Used by the email-ingest skill: the operator reviews each message returned here and then calls email-ingest with a per-message disposition (ingest or discard).", {
    since_uid: z.coerce.number().int().optional().describe("UID to fetch after (exclusive). Defaults to the stored high-water mark — usually what you want."),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-fetch");
    try {
        const text = await emailFetch({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Email fetch failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// email-ingest — write operator-approved messages from the pending batch
// ===================================================================
eagerTool(server, "email-ingest", "Apply the operator's per-message decisions to the batch returned by the most recent email-fetch. Every Message-ID from that batch must appear in decisions with a disposition of 'ingest' (write to graph) or 'discard' (skip). Advances the inbox high-water mark only after a successful write. Refuses if any pending Message-ID is missing a decision.", {
    decisions: z.array(z.object({
        messageId: z.string().min(1).describe("Message-ID from the email-fetch listing (angle brackets optional)."),
        disposition: z.enum(["ingest", "discard"]).describe("'ingest' writes the message into a :ConversationArchive {source:'email'} thread (per Task 426); 'discard' skips it without writing."),
    })).min(1).describe("One entry per Message-ID returned by the preceding email-fetch call."),
}, async (params) => {
    if (!accountId)
        return refuseNoAccount("email-ingest");
    try {
        const text = await emailIngest({ ...params, accountId });
        return { content: [{ type: "text", text }] };
    }
    catch (err) {
        return {
            content: [
                {
                    type: "text",
                    text: `Email ingest failed: ${err instanceof Error ? err.message : String(err)}`,
                },
            ],
            isError: true,
        };
    }
});
// ===================================================================
// Lifecycle
// ===================================================================
process.on("SIGINT", async () => {
    await closeDriver();
    process.exit(0);
});
const transport = new StdioServerTransport();
await server.connect(transport);
//# sourceMappingURL=index.js.map