/**
 * @bobfrankston/mailsend
 * Queue-based mail sender. Each account has its own queue directory.
 * Messages queued as .ltr (RFC 822) files, sent via SMTP with retry.
 * Supports password and OAuth2 (Gmail) authentication.
 *
 * Directory structure:
 *   {baseDir}/{accountId}/queue/    — pending .ltr files
 *   {baseDir}/{accountId}/sent/     — sent messages (optional, YYYY/MM/DD)
 *   {baseDir}/{accountId}/fail/     — failed messages for retry
 */
import * as fs from "node:fs";
import * as path from "node:path";
import { createTransport } from "nodemailer";
// ── File helpers ──
function generateFilename() {
    const now = new Date();
    const pad2 = (n) => String(n).padStart(2, "0");
    const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
    const seq = String(Math.floor(Math.random() * 10000)).padStart(4, "0");
    return `${ts}-${seq}.ltr`;
}
function datePath() {
    const dt = new Date();
    const pad2 = (n) => String(n).padStart(2, "0");
    return `${dt.getFullYear()}/${pad2(dt.getMonth() + 1)}/${pad2(dt.getDate())}`;
}
function moveToDateDir(filePath, destBase) {
    const dest = path.join(destBase, datePath());
    fs.mkdirSync(dest, { recursive: true });
    fs.renameSync(filePath, path.join(dest, path.basename(filePath)));
}
function listLtr(dir) {
    if (!fs.existsSync(dir))
        return [];
    return fs.readdirSync(dir).filter(f => f.endsWith(".ltr")).sort();
}
function walkLtrCount(dir) {
    if (!fs.existsSync(dir))
        return 0;
    let count = 0;
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        if (entry.isDirectory())
            count += walkLtrCount(path.join(dir, entry.name));
        else if (entry.name.endsWith(".ltr"))
            count++;
    }
    return count;
}
function walkLtrFiles(dir) {
    if (!fs.existsSync(dir))
        return [];
    const files = [];
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        const full = path.join(dir, entry.name);
        if (entry.isDirectory())
            files.push(...walkLtrFiles(full));
        else if (entry.name.endsWith(".ltr"))
            files.push(full);
    }
    return files;
}
// ── RFC 822 builder ──
export function buildRfc822(msg) {
    const lines = [];
    lines.push(`From: ${msg.from}`);
    lines.push(`To: ${msg.to.join(", ")}`);
    if (msg.cc?.length)
        lines.push(`Cc: ${msg.cc.join(", ")}`);
    if (msg.bcc?.length)
        lines.push(`Bcc: ${msg.bcc.join(", ")}`);
    lines.push(`Subject: ${msg.subject}`);
    lines.push(`Date: ${new Date().toUTCString()}`);
    if (msg.inReplyTo)
        lines.push(`In-Reply-To: ${msg.inReplyTo}`);
    if (msg.references)
        lines.push(`References: ${msg.references}`);
    if (msg.accountId)
        lines.push(`X-MailSend-Account: ${msg.accountId}`);
    lines.push(`MIME-Version: 1.0`);
    if (msg.html) {
        lines.push(`Content-Type: text/html; charset=UTF-8`);
        lines.push(`Content-Transfer-Encoding: 8bit`);
        lines.push("");
        lines.push(msg.html);
    }
    else {
        lines.push(`Content-Type: text/plain; charset=UTF-8`);
        lines.push(`Content-Transfer-Encoding: 8bit`);
        lines.push("");
        lines.push(msg.text || "");
    }
    return lines.join("\r\n");
}
// ── Transport cache ──
const transports = new Map();
function getTransport(config) {
    const key = `${config.host}:${config.port}:${config.user}`;
    let transport = transports.get(key);
    if (!transport) {
        const opts = {
            host: config.host,
            port: config.port,
            secure: config.secure ?? (config.port === 465),
            tls: { rejectUnauthorized: false },
        };
        if (config.auth === "password") {
            opts.auth = { user: config.user, pass: config.password };
        }
        else if (config.auth === "oauth2") {
            opts.auth = { type: "OAuth2", user: config.user };
        }
        transport = createTransport(opts);
        transports.set(key, transport);
    }
    return transport;
}
// ── MailSender ──
let singleton = null;
export class MailSender {
    config;
    processing = false;
    retryTimer = null;
    constructor(config) {
        this.config = config;
        // Ensure base directory exists
        fs.mkdirSync(config.baseDir, { recursive: true });
    }
    accountDir(accountId) {
        return path.join(this.config.baseDir, accountId);
    }
    queueDir(accountId) {
        const dir = path.join(this.accountDir(accountId), "queue");
        fs.mkdirSync(dir, { recursive: true });
        return dir;
    }
    sentDir(accountId) {
        return path.join(this.accountDir(accountId), "sent");
    }
    failDir(accountId) {
        const dir = path.join(this.accountDir(accountId), "fail");
        fs.mkdirSync(dir, { recursive: true });
        return dir;
    }
    /** Queue a message. Returns the filename. */
    queueMessage(msg) {
        const accountId = msg.accountId || this.config.defaultAccount;
        if (!accountId)
            throw new Error("No accountId and no defaultAccount configured");
        msg.accountId = accountId;
        const content = buildRfc822(msg);
        const filename = generateFilename();
        fs.writeFileSync(path.join(this.queueDir(accountId), filename), content);
        // Process immediately (non-blocking)
        this.processQueue(accountId).catch(e => console.error(`[mailsend] Process error: ${e.message}`));
        return filename;
    }
    /** Process pending messages for one account */
    async processQueue(accountId) {
        if (this.processing)
            return;
        this.processing = true;
        try {
            const smtpConfig = this.config.accounts[accountId];
            if (!smtpConfig) {
                console.error(`[mailsend] No SMTP config for: ${accountId}`);
                return;
            }
            const files = listLtr(this.queueDir(accountId));
            for (const file of files) {
                await this.sendFile(path.join(this.queueDir(accountId), file), accountId, smtpConfig);
            }
        }
        finally {
            this.processing = false;
        }
    }
    /** Process all account queues */
    async processAllQueues() {
        for (const accountId of Object.keys(this.config.accounts)) {
            await this.processQueue(accountId);
        }
    }
    async sendFile(filePath, accountId, smtpConfig) {
        const content = fs.readFileSync(filePath, "utf-8");
        try {
            const transport = getTransport(smtpConfig);
            if (smtpConfig.auth === "oauth2" && smtpConfig.tokenProvider) {
                const token = await smtpConfig.tokenProvider();
                transport.options.auth.accessToken = token;
            }
            const result = await transport.sendMail({ raw: content });
            if (result.rejected?.length > 0) {
                console.error(`[mailsend] Rejected: ${result.rejected.join(", ")} — ${path.basename(filePath)}`);
                moveToDateDir(filePath, this.failDir(accountId));
                return false;
            }
            console.log(`[mailsend] Sent: ${path.basename(filePath)} → ${result.accepted?.join(", ")} (${result.messageId})`);
            if (this.config.logSent) {
                moveToDateDir(filePath, this.sentDir(accountId));
            }
            else {
                fs.unlinkSync(filePath);
            }
            return true;
        }
        catch (e) {
            console.error(`[mailsend] Error: ${path.basename(filePath)}: ${e.message}`);
            moveToDateDir(filePath, this.failDir(accountId));
            return false;
        }
    }
    /** Start background worker — retries failed, processes queues */
    startWorker() {
        if (this.retryTimer)
            return;
        const interval = this.config.retryIntervalMs || 300000;
        this.retryTimer = setInterval(async () => {
            for (const accountId of Object.keys(this.config.accounts)) {
                this.retryFailed(accountId);
            }
            await this.processAllQueues();
        }, interval);
    }
    retryFailed(accountId) {
        const failFiles = walkLtrFiles(this.failDir(accountId));
        const queueDir = this.queueDir(accountId);
        for (const file of failFiles) {
            fs.renameSync(file, path.join(queueDir, path.basename(file)));
        }
    }
    stopWorker() {
        if (this.retryTimer) {
            clearInterval(this.retryTimer);
            this.retryTimer = null;
        }
    }
    /** Get queue status per account */
    getStatus() {
        const result = {};
        for (const accountId of Object.keys(this.config.accounts)) {
            result[accountId] = {
                pending: listLtr(this.queueDir(accountId)).length,
                failed: walkLtrCount(this.failDir(accountId)),
            };
        }
        return result;
    }
}
// ── Singleton API ──
export function initMailSend(config) {
    singleton = new MailSender(config);
    singleton.startWorker();
    return singleton;
}
export function getMailSender() {
    return singleton;
}
export function queueMessage(msg) {
    if (!singleton)
        throw new Error("mailsend not initialized — call initMailSend() first");
    return singleton.queueMessage(msg);
}
//# sourceMappingURL=index.js.map