/**
 * File-per-message body storage backend.
 *
 * Disk layout: {basePath}/{accountId}/<xx>/<uuid>.eml
 * Filename is an opaque UUID. Two-char prefix dir for filesystem fan-out.
 *
 * CRITICAL: the on-disk filename carries NO semantic meaning — not folder
 * id, not UID, not Message-ID. A new UUID is minted on every `putMessage`.
 * Moves, UID renumbers, UIDVALIDITY bumps cannot shadow a body because no
 * filename is ever reused. DB's `body_path` column is the sole authority
 * on where a given message's body lives.
 */
import * as fs from "node:fs";
import * as path from "node:path";
import { randomUUID } from "node:crypto";
export class FileMessageStore {
    basePath;
    constructor(basePath) {
        this.basePath = basePath;
        fs.mkdirSync(basePath, { recursive: true });
    }
    /** Fresh opaque path per call. No inputs from the caller affect the name. */
    newMessagePath(accountId) {
        const uuid = randomUUID().replace(/-/g, "");
        const prefix = uuid.slice(0, 2);
        return path.join(this.basePath, accountId, prefix, `${uuid}.eml`);
    }
    /** Verify a given path resolves inside this store's basePath — refuses
     *  any value that doesn't (cheap directory-traversal guard). */
    inStore(fullPath) {
        if (!fullPath)
            return false;
        const rel = path.relative(path.resolve(this.basePath), path.resolve(fullPath));
        return !rel.startsWith("..") && !path.isAbsolute(rel);
    }
    /** Write a new body. Always a fresh UUID path. Caller MUST persist the
     *  returned path in the DB (`body_path`) and use it for all reads. The
     *  (folderId, uid) args are kept for interface compatibility; they do
     *  NOT affect the filename. */
    async putMessage(accountId, _folderId, _uid, raw) {
        const filePath = this.newMessagePath(accountId);
        fs.mkdirSync(path.dirname(filePath), { recursive: true });
        fs.writeFileSync(filePath, raw);
        return filePath;
    }
    /** Read by absolute path (DB `body_path`). The primary read API. */
    async readByPath(fullPath) {
        if (!this.inStore(fullPath))
            throw new Error(`refusing to read outside store: ${fullPath}`);
        return fs.readFileSync(fullPath);
    }
    async hasByPath(fullPath) {
        if (!this.inStore(fullPath))
            return false;
        return fs.existsSync(fullPath);
    }
    async unlinkByPath(fullPath) {
        if (!this.inStore(fullPath))
            return;
        if (fs.existsSync(fullPath))
            fs.unlinkSync(fullPath);
    }
    // MessageStore interface compatibility (unused once all callers migrate to
    // path-based reads). These used to compose {folderId}/{uid}.eml and they
    // would resurrect the comingling bug if restored. Kept as throwing stubs
    // so any accidental caller surfaces loudly rather than silently misbehave.
    getMessagePath(_accountId, _folderId, _uid) {
        throw new Error("FileMessageStore.getMessagePath is retired — read body_path from DB");
    }
    async getMessage(_accountId, _folderId, _uid) {
        throw new Error("FileMessageStore.getMessage(folder,uid) is retired — use readByPath(body_path)");
    }
    async hasMessage(_accountId, _folderId, _uid) {
        throw new Error("FileMessageStore.hasMessage(folder,uid) is retired — use hasByPath(body_path)");
    }
    async deleteMessage(_accountId, _folderId, _uid) {
        throw new Error("FileMessageStore.deleteMessage(folder,uid) is retired — use unlinkByPath(body_path)");
    }
}
//# sourceMappingURL=file-store.js.map