/**
 * Sending tools: send_email, reply_to_email, forward_email, send_test_email.
 *
 * Handlers preserve behavior 1:1 with the pre-refactor switch cases in
 * src/index.ts — identical validation, identical error shapes, identical
 * SMTP service calls. No behavior changes.
 */
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { isValidEmail, optionalFolderHint, validateAttachments, sanitizeAttachments, requireNumericEmailId } from "../utils/helpers.js";
import { logger } from "../utils/logger.js";
import { escapeHtml } from "../services/smtp-service.js";
const ACTION_RESULT_SCHEMA = {
    type: "object",
    properties: {
        success: { type: "boolean" },
        messageId: { type: "string" },
        reason: { type: "string" },
    },
    required: ["success"],
};
export const defs = [
    {
        name: "send_email",
        title: "Send Email",
        description: "Send an email via Proton Mail SMTP (through Proton Bridge). Supports To/CC/BCC (comma-separated), plain text or HTML body, priority (high/normal/low), reply-to, and base64-encoded attachments. Returns messageId on success.",
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
        inputSchema: {
            type: "object",
            properties: {
                to: { type: "string", description: "Recipient address(es), comma-separated" },
                cc: { type: "string", description: "CC addresses, comma-separated" },
                bcc: { type: "string", description: "BCC addresses, comma-separated" },
                subject: { type: "string", description: "Email subject line" },
                body: { type: "string", description: "Email body (plain text or HTML)" },
                isHtml: { type: "boolean", description: "Set true if body is HTML", default: false },
                priority: {
                    type: "string",
                    enum: ["high", "normal", "low"],
                    description: "Message priority header",
                },
                replyTo: { type: "string", description: "Reply-to address (must be valid email)" },
                attachments: {
                    type: "array",
                    description: "Attachments as objects with filename, content (base64), contentType",
                },
            },
            required: ["to", "subject", "body"],
        },
        outputSchema: ACTION_RESULT_SCHEMA,
    },
    {
        name: "reply_to_email",
        title: "Reply to Email",
        description: "Send a reply to an existing email. Fetches the original to pre-fill To, Re:-prefixed subject, and thread references. Use replyAll to include original CC recipients.",
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
        inputSchema: {
            type: "object",
            properties: {
                emailId: { type: "string", description: "UID of the email to reply to" },
                folder: { type: "string", description: "Folder the original message lives in. Providing this avoids UID collisions across folders." },
                body: { type: "string", description: "Reply body (plain text or HTML)" },
                isHtml: { type: "boolean", default: false },
                replyAll: {
                    type: "boolean",
                    default: false,
                    description: "Include all original CC recipients",
                },
            },
            required: ["emailId", "body"],
        },
        outputSchema: ACTION_RESULT_SCHEMA,
    },
    {
        name: "forward_email",
        title: "Forward Email",
        description: "Forward an email to a new recipient. Original message is included as quoted content. Standard email headers (From, Date, Subject) are preserved in the forward body. Optionally prepend a message before the forwarded content.",
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
        inputSchema: {
            type: "object",
            properties: {
                emailId: { type: "string", description: "UID of the email to forward" },
                folder: { type: "string", description: "Folder the original message lives in. Providing this avoids UID collisions across folders." },
                to: { type: "string", description: "Recipient address(es), comma-separated" },
                message: { type: "string", description: "Optional message to prepend before the forwarded content" },
            },
            required: ["emailId", "to"],
        },
        outputSchema: ACTION_RESULT_SCHEMA,
    },
    {
        name: "send_test_email",
        title: "Send Test Email",
        description: "Send a test email to verify SMTP is working. Returns messageId on success. Use before relying on send_email in automated workflows.",
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
        inputSchema: {
            type: "object",
            properties: {
                to: { type: "string", description: "Recipient address for the test" },
                customMessage: { type: "string", description: "Optional custom message body" },
            },
            required: ["to"],
        },
        outputSchema: ACTION_RESULT_SCHEMA,
    },
];
export const handlers = {
    send_email: async (ctx) => {
        const { args, smtpService, actionOk, MAX_SUBJECT_LENGTH, MAX_BODY_LENGTH } = ctx;
        const seAttErr = validateAttachments(args.attachments);
        if (seAttErr)
            throw new McpError(ErrorCode.InvalidParams, seAttErr);
        if (!args.to || typeof args.to !== "string" || !args.to.trim()) {
            throw new McpError(ErrorCode.InvalidParams, "'to' must be a non-empty string with at least one recipient address.");
        }
        if (!args.body || typeof args.body !== "string" || !args.body.trim()) {
            throw new McpError(ErrorCode.InvalidParams, "'body' must be a non-empty string.");
        }
        if (args.body.length > MAX_BODY_LENGTH) {
            throw new McpError(ErrorCode.InvalidParams, `'body' must not exceed ${MAX_BODY_LENGTH} bytes (${MAX_BODY_LENGTH / 1024 / 1024} MB).`);
        }
        if (args.isHtml !== undefined && typeof args.isHtml !== "boolean") {
            throw new McpError(ErrorCode.InvalidParams, "'isHtml' must be a boolean when provided.");
        }
        // SMTP-013: `subject` is in the schema's `required` list, but MCP transports
        // don't reliably enforce inputSchema — enforce it here so a `{to, body}` call
        // can't reach nodemailer with an empty Subject header.
        if (args.subject === undefined || typeof args.subject !== "string" || !args.subject.trim()) {
            throw new McpError(ErrorCode.InvalidParams, "'subject' must be a non-empty string.");
        }
        if (args.subject.length > MAX_SUBJECT_LENGTH) {
            throw new McpError(ErrorCode.InvalidParams, `'subject' must not exceed ${MAX_SUBJECT_LENGTH} characters (RFC 2822 limit).`);
        }
        const VALID_PRIORITIES = new Set(["high", "normal", "low"]);
        if (args.priority !== undefined && !VALID_PRIORITIES.has(args.priority)) {
            throw new McpError(ErrorCode.InvalidParams, `'priority' must be one of "high", "normal", or "low".`);
        }
        if (args.replyTo !== undefined && (typeof args.replyTo !== "string" || !isValidEmail(args.replyTo))) {
            throw new McpError(ErrorCode.InvalidParams, `'replyTo' must be a valid email address.`);
        }
        if (args.cc !== undefined && typeof args.cc !== "string") {
            throw new McpError(ErrorCode.InvalidParams, "'cc' must be a string when provided.");
        }
        if (args.bcc !== undefined && typeof args.bcc !== "string") {
            throw new McpError(ErrorCode.InvalidParams, "'bcc' must be a string when provided.");
        }
        const result = await smtpService.sendEmail({
            to: args.to,
            cc: args.cc,
            bcc: args.bcc,
            subject: args.subject,
            body: args.body,
            isHtml: args.isHtml,
            priority: args.priority,
            replyTo: args.replyTo,
            attachments: sanitizeAttachments(args.attachments),
        });
        if (!result.success) {
            return { content: [{ type: "text", text: "Email delivery failed" }], isError: true };
        }
        return actionOk(result.messageId);
    },
    reply_to_email: async (ctx) => {
        const { args, imapService, smtpService, config, actionOk, MAX_BODY_LENGTH, MAX_SUBJECT_LENGTH } = ctx;
        const emailId = requireNumericEmailId(args.emailId);
        if (!args.body || typeof args.body !== "string" || !args.body.trim()) {
            throw new McpError(ErrorCode.InvalidParams, "'body' must be a non-empty string.");
        }
        if (args.body.length > MAX_BODY_LENGTH) {
            throw new McpError(ErrorCode.InvalidParams, `'body' must not exceed ${MAX_BODY_LENGTH} bytes (${MAX_BODY_LENGTH / 1024 / 1024} MB).`);
        }
        if (args.isHtml !== undefined && typeof args.isHtml !== "boolean") {
            throw new McpError(ErrorCode.InvalidParams, "'isHtml' must be a boolean when provided.");
        }
        if (args.replyAll !== undefined && typeof args.replyAll !== "boolean") {
            throw new McpError(ErrorCode.InvalidParams, "'replyAll' must be a boolean when provided.");
        }
        const original = await imapService.getEmailById(emailId, optionalFolderHint(args.folder));
        if (!original) {
            return { content: [{ type: "text", text: "Original email not found" }], isError: true };
        }
        const replyToAddress = original.from.match(/<([^>]+)>/)?.[1] ?? original.from.trim();
        const cleanSubject = original.subject.replace(/[\r\n\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
        const replySubjectRaw = cleanSubject.toLowerCase().startsWith("re:")
            ? cleanSubject
            : `Re: ${cleanSubject}`;
        // SMTP-009: cap to MAX_SUBJECT_LENGTH like the forward path — a 998-char
        // original + "Re: " prefix otherwise bypasses the send_email guard.
        const subject = replySubjectRaw.length > MAX_SUBJECT_LENGTH
            ? replySubjectRaw.slice(0, MAX_SUBJECT_LENGTH)
            : replySubjectRaw;
        const ccAddresses = [];
        if (args.replyAll) {
            const self = config.smtp.username.toLowerCase();
            const addrs = [...(original.to ?? []), ...(original.cc ?? [])];
            for (const a of addrs) {
                const addr = (a.match(/<([^>]+)>/)?.[1] ?? a).trim().toLowerCase();
                if (addr && addr !== self && addr !== replyToAddress.toLowerCase()) {
                    if (isValidEmail(addr))
                        ccAddresses.push(addr);
                }
            }
        }
        const result = await smtpService.sendEmail({
            to: replyToAddress,
            cc: ccAddresses.length > 0 ? ccAddresses.join(", ") : undefined,
            subject,
            body: args.body,
            isHtml: args.isHtml,
            inReplyTo: original.inReplyTo,
            references: original.references,
        });
        if (!result.success) {
            return { content: [{ type: "text", text: "Email delivery failed" }], isError: true };
        }
        if (result.success) {
            await imapService.setFlag(emailId, '\\Answered').catch((err) => logger.warn(`reply_to_email: failed to set \\Answered on ${emailId}: ${err instanceof Error ? err.message : String(err)}`, 'Sending'));
        }
        return actionOk(result.messageId);
    },
    forward_email: async (ctx) => {
        const { args, imapService, smtpService, actionOk, MAX_SUBJECT_LENGTH, MAX_BODY_LENGTH } = ctx;
        const fwdId = requireNumericEmailId(args.emailId);
        if (!args.to || typeof args.to !== "string" || !args.to.trim()) {
            throw new McpError(ErrorCode.InvalidParams, "'to' must be a non-empty string with at least one recipient address.");
        }
        const fwdOriginal = await imapService.getEmailById(fwdId, optionalFolderHint(args.folder));
        if (!fwdOriginal) {
            return { content: [{ type: "text", text: "Original email not found" }], isError: true };
        }
        const fwdCleanSubject = fwdOriginal.subject.replace(/[\r\n\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
        const fwdSubjectRaw = fwdCleanSubject.toLowerCase().startsWith("fwd:")
            ? fwdCleanSubject
            : `Fwd: ${fwdCleanSubject}`;
        const fwdSubject = fwdSubjectRaw.length > MAX_SUBJECT_LENGTH
            ? fwdSubjectRaw.slice(0, MAX_SUBJECT_LENGTH)
            : fwdSubjectRaw;
        // Strip control chars from the embedded original From/To so a CRLF in a
        // display name can't break the quoted header block's visual structure.
        const stripCtl = (s) => s.replace(/[\r\n\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
        const fwdHeader = [
            "---------- Forwarded message ----------",
            `From: ${stripCtl(fwdOriginal.from)}`,
            `Date: ${fwdOriginal.date.toISOString()}`,
            `Subject: ${fwdCleanSubject}`,
            `To: ${stripCtl((fwdOriginal.to ?? []).join(", "))}`,
            "",
        ].join("\n");
        if (args.message !== undefined && typeof args.message !== "string") {
            throw new McpError(ErrorCode.InvalidParams, "'message' must be a string when provided.");
        }
        // SMTP-010: isHtml is inherited from the forwarded original, so when the
        // original is HTML, args.message is spliced into an HTML body unescaped —
        // letting an agent inject markup/script. Escape it in the HTML case.
        const rawUserMessage = args.message ?? "";
        const safeUserMessage = fwdOriginal.isHtml ? escapeHtml(rawUserMessage) : rawUserMessage;
        const userMessage = args.message ? `${safeUserMessage}\n\n` : "";
        const fwdBody = `${userMessage}${fwdHeader}\n${fwdOriginal.body ?? ""}`;
        if (fwdBody.length > MAX_BODY_LENGTH) {
            throw new McpError(ErrorCode.InvalidParams, `Forwarded body must not exceed ${MAX_BODY_LENGTH} bytes (${MAX_BODY_LENGTH / 1024 / 1024} MB).`);
        }
        const fwdResult = await smtpService.sendEmail({
            to: args.to,
            subject: fwdSubject,
            body: fwdBody,
            isHtml: fwdOriginal.isHtml,
            // Keep the forward in the original thread (parity with reply_to_email).
            inReplyTo: fwdOriginal.inReplyTo,
            references: fwdOriginal.references,
        });
        if (!fwdResult.success) {
            return { content: [{ type: "text", text: "Forward failed" }], isError: true };
        }
        if (fwdResult.success) {
            // Best-effort flag; log (don't fail the send) so a flag-set failure isn't invisible.
            await imapService.setFlag(fwdId, '$Forwarded').catch((err) => logger.warn(`forward_email: failed to set $Forwarded on ${fwdId}: ${err instanceof Error ? err.message : String(err)}`, 'Sending'));
        }
        return actionOk(fwdResult.messageId);
    },
    send_test_email: async (ctx) => {
        const { args, smtpService, actionOk } = ctx;
        if (!isValidEmail(args.to)) {
            throw new McpError(ErrorCode.InvalidParams, `Invalid recipient email address: ${args.to}`);
        }
        if (args.customMessage !== undefined && typeof args.customMessage !== "string") {
            throw new McpError(ErrorCode.InvalidParams, "'customMessage' must be a string when provided.");
        }
        const result = await smtpService.sendTestEmail(args.to, args.customMessage);
        if (!result.success) {
            return { content: [{ type: "text", text: "Test email failed" }], isError: true };
        }
        return actionOk(result.messageId);
    },
};
const mod = { defs, handlers };
export default mod;
//# sourceMappingURL=sending.js.map