import assert from 'node:assert/strict'; import { mkdtempSync } from 'node:fs'; import { mkdir, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { test } from 'node:test'; import { listMaildirFolders, maildirFolderPath, maildirHomePath, readMaildirMessage, scanMaildirMailbox, setMaildirMessageSeen, sqliteMaildirStorageKey, writeMaildirMessage } from '../src/maildir-store.js'; test('Maildir store writes original bytes atomically and exposes folder flags', async () => { const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-store-')); const address = 'User@Example.com'; const rawMessageBytes = Buffer.from([ ...Buffer.from('From: sender@example.net\r\nSubject: Latin1\r\n\r\n', 'ascii'), 0x48, 0xe9, 0x6c, 0x6c, 0x6f, 0x0d, 0x0a ]); const storage = await writeMaildirMessage({ root, address, rawMessageBytes, folder: '归档/2026', flags: ['\\Flagged'], keywords: ['$Label1'], read: false, receivedAt: '2026-07-16T12:00:00.000Z', storageKey: 'mhdb-42' }); assert.equal(storage.backend, 'maildir'); assert.equal(storage.key, 'mhdb-42'); assert.match(storage.relpath, /^mail\/\./); assert.match(storage.relpath, /\/cur\//); assert.equal(storage.size, rawMessageBytes.length); assert.equal(maildirHomePath(root, address), path.join(root, 'user@example.com')); assert.match(maildirFolderPath(root, address, '归档/2026'), /mail\/\.&/); const [entry] = await scanMaildirMailbox({ root, address }); assert.equal(entry.storageKey, 'mhdb-42'); assert.equal(entry.folder, '归档/2026'); assert.deepEqual(entry.flags, ['\\Flagged']); assert.deepEqual(entry.keywords, ['$Label1']); assert.equal(entry.read, false); const stored = await readMaildirMessage(entry); assert.deepEqual(stored.bytes, rawMessageBytes); assert.deepEqual(await readFile(entry.filePath), rawMessageBytes); const updated = await setMaildirMessageSeen({ root, address, storageKey: storage.key, relpath: storage.relpath, seen: true }); assert.match(updated.relpath, /\/cur\//); assert.match(updated.relpath, /:2,FSa$/); const [seenEntry] = await scanMaildirMailbox({ root, address }); assert.deepEqual(seenEntry.flags, ['\\Flagged', '\\Seen']); assert.deepEqual(seenEntry.keywords, ['$Label1']); assert.equal(seenEntry.read, true); }); test('Maildir store rejects unsafe addresses and never overwrites a storage key', async () => { const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-safety-')); assert.throws(() => maildirHomePath(root, '../escape@example.com'), /地址不正确/); assert.equal(sqliteMaildirStorageKey(123), 'mhdb-123'); assert.throws(() => sqliteMaildirStorageKey(0), /ID 不正确/); const first = Buffer.from('Subject: first\r\n\r\nfirst'); const storage = await writeMaildirMessage({ root, address: 'safe@example.com', rawMessageBytes: first, storageKey: 'mhdb-1' }); assert.match(storage.relpath, /\/new\//); assert.doesNotMatch(storage.relpath, /:2,/); await assert.rejects( writeMaildirMessage({ root, address: 'safe@example.com', rawMessageBytes: Buffer.from('Subject: second\r\n\r\nsecond'), storageKey: 'mhdb-1' }), /存储标识冲突/ ); const [entry] = await scanMaildirMailbox({ root, address: 'safe@example.com' }); assert.deepEqual((await readMaildirMessage(entry)).bytes, first); }); test('Maildir keyword letters never become uppercase system flags', async () => { const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-keywords-')); const keywords = ['$Label1', '$Label2', '$Label3', '$Label4']; await writeMaildirMessage({ root, address: 'keywords@example.com', rawMessageBytes: Buffer.from('Subject: keyword flags\r\n\r\nBody'), keywords, storageKey: 'mhdb-7' }); const [entry] = await scanMaildirMailbox({ root, address: 'keywords@example.com' }); assert.deepEqual(entry.flags, []); assert.deepEqual(entry.keywords, keywords); }); test('Maildir folder encoding keeps literal dots distinct from hierarchy separators', async () => { const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-folder-dots-')); const address = 'folders@example.com'; const dottedPath = maildirFolderPath(root, address, 'team.ops'); const nestedPath = maildirFolderPath(root, address, 'team/ops'); assert.notEqual(dottedPath, nestedPath); assert.match(dottedPath, /\^2e/); assert.notEqual( maildirFolderPath(root, address, 'team^2eops'), dottedPath ); assert.match(maildirFolderPath(root, address, '~archive'), /\.\^7earchive$/); await writeMaildirMessage({ root, address, folder: 'team.ops', rawMessageBytes: Buffer.from('Subject: dotted\r\n\r\nDotted'), storageKey: 'mhdb-8' }); await writeMaildirMessage({ root, address, folder: 'team/ops', rawMessageBytes: Buffer.from('Subject: nested\r\n\r\nNested'), storageKey: 'mhdb-9' }); const entries = await scanMaildirMailbox({ root, address }); assert.deepEqual(entries.map((entry) => entry.folder).sort(), ['team.ops', 'team/ops']); }); test('Maildir folder inventory ignores Dovecot internal and incomplete dot directories', async () => { const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-folder-inventory-')); const address = 'inventory@example.com'; await writeMaildirMessage({ root, address, folder: 'Projects', rawMessageBytes: Buffer.from('Subject: project\r\n\r\nBody'), storageKey: 'mhdb-10' }); const mailRoot = path.join(maildirHomePath(root, address), 'mail'); await mkdir(path.join(mailRoot, '.dovecot-internal'), { recursive: true }); await mkdir(path.join(mailRoot, '.incomplete', 'cur'), { recursive: true }); assert.deepEqual( (await listMaildirFolders({ root, address })).sort(), ['INBOX', 'Projects'] ); });