import fs from 'node:fs'; import path from 'node:path'; export interface IFaxBoxConfig { id: string; enabled: boolean; maxMessages?: number; } export interface IFaxMessage { id: string; boxId: string; callerNumber?: string; timestamp: number; fileName: string; completionCode?: number | null; completionLabel?: string | null; pageCount?: number; bitRate?: number; } export class FaxBoxManager { private boxes = new Map(); private readonly basePath: string; private readonly log: (msg: string) => void; constructor(log: (msg: string) => void) { this.basePath = path.join(process.cwd(), '.nogit', 'fax', 'inboxes'); this.log = log; } init(faxBoxConfigs: IFaxBoxConfig[]): void { this.boxes.clear(); for (const cfg of faxBoxConfigs) { cfg.enabled ??= true; cfg.maxMessages ??= 50; this.boxes.set(cfg.id, cfg); } fs.mkdirSync(this.basePath, { recursive: true }); this.log(`[faxbox] initialized ${this.boxes.size} fax box(es)`); } getBox(boxId: string): IFaxBoxConfig | null { return this.boxes.get(boxId) ?? null; } getBoxDir(boxId: string): string { return path.join(this.basePath, boxId); } addMessage( boxId: string, info: { callerNumber?: string; fileName: string; completionCode?: number | null; completionLabel?: string | null; pageCount?: number; bitRate?: number; }, ): void { const msg: IFaxMessage = { id: crypto.randomUUID(), boxId, callerNumber: info.callerNumber, timestamp: Date.now(), fileName: path.basename(info.fileName), completionCode: info.completionCode ?? null, completionLabel: info.completionLabel ?? null, pageCount: info.pageCount, bitRate: info.bitRate, }; this.saveMessage(msg); } saveMessage(msg: IFaxMessage): void { const boxDir = this.getBoxDir(msg.boxId); fs.mkdirSync(boxDir, { recursive: true }); const messages = this.loadMessages(msg.boxId); messages.unshift(msg); const box = this.boxes.get(msg.boxId); const maxMessages = box?.maxMessages ?? 50; while (messages.length > maxMessages) { const old = messages.pop()!; const oldPath = path.join(boxDir, old.fileName); try { if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); } catch {} } this.writeMessages(msg.boxId, messages); this.log(`[faxbox] saved fax ${msg.id} in box "${msg.boxId}" (${msg.fileName})`); } getMessages(boxId: string): IFaxMessage[] { return this.loadMessages(boxId); } getMessage(boxId: string, messageId: string): IFaxMessage | null { return this.loadMessages(boxId).find((m) => m.id === messageId) ?? null; } getMessageFilePath(boxId: string, messageId: string): string | null { const msg = this.getMessage(boxId, messageId); if (!msg) return null; const filePath = path.join(this.getBoxDir(boxId), msg.fileName); return fs.existsSync(filePath) ? filePath : null; } deleteMessage(boxId: string, messageId: string): boolean { const messages = this.loadMessages(boxId); const idx = messages.findIndex((m) => m.id === messageId); if (idx === -1) return false; const msg = messages[idx]; const filePath = path.join(this.getBoxDir(boxId), msg.fileName); try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch {} messages.splice(idx, 1); this.writeMessages(boxId, messages); return true; } private messagesPath(boxId: string): string { return path.join(this.getBoxDir(boxId), 'messages.json'); } private loadMessages(boxId: string): IFaxMessage[] { const filePath = this.messagesPath(boxId); try { if (!fs.existsSync(filePath)) return []; return JSON.parse(fs.readFileSync(filePath, 'utf8')) as IFaxMessage[]; } catch { return []; } } private writeMessages(boxId: string, messages: IFaxMessage[]): void { const boxDir = this.getBoxDir(boxId); fs.mkdirSync(boxDir, { recursive: true }); fs.writeFileSync(this.messagesPath(boxId), JSON.stringify(messages, null, 2), 'utf8'); } }