| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- 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']
- );
- });
|