maildir-store.test.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import assert from 'node:assert/strict';
  2. import { mkdtempSync } from 'node:fs';
  3. import { mkdir, readFile } from 'node:fs/promises';
  4. import { tmpdir } from 'node:os';
  5. import path from 'node:path';
  6. import { test } from 'node:test';
  7. import {
  8. listMaildirFolders,
  9. maildirFolderPath,
  10. maildirHomePath,
  11. readMaildirMessage,
  12. scanMaildirMailbox,
  13. setMaildirMessageSeen,
  14. sqliteMaildirStorageKey,
  15. writeMaildirMessage
  16. } from '../src/maildir-store.js';
  17. test('Maildir store writes original bytes atomically and exposes folder flags', async () => {
  18. const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-store-'));
  19. const address = 'User@Example.com';
  20. const rawMessageBytes = Buffer.from([
  21. ...Buffer.from('From: sender@example.net\r\nSubject: Latin1\r\n\r\n', 'ascii'),
  22. 0x48, 0xe9, 0x6c, 0x6c, 0x6f, 0x0d, 0x0a
  23. ]);
  24. const storage = await writeMaildirMessage({
  25. root,
  26. address,
  27. rawMessageBytes,
  28. folder: '归档/2026',
  29. flags: ['\\Flagged'],
  30. keywords: ['$Label1'],
  31. read: false,
  32. receivedAt: '2026-07-16T12:00:00.000Z',
  33. storageKey: 'mhdb-42'
  34. });
  35. assert.equal(storage.backend, 'maildir');
  36. assert.equal(storage.key, 'mhdb-42');
  37. assert.match(storage.relpath, /^mail\/\./);
  38. assert.match(storage.relpath, /\/cur\//);
  39. assert.equal(storage.size, rawMessageBytes.length);
  40. assert.equal(maildirHomePath(root, address), path.join(root, 'user@example.com'));
  41. assert.match(maildirFolderPath(root, address, '归档/2026'), /mail\/\.&/);
  42. const [entry] = await scanMaildirMailbox({ root, address });
  43. assert.equal(entry.storageKey, 'mhdb-42');
  44. assert.equal(entry.folder, '归档/2026');
  45. assert.deepEqual(entry.flags, ['\\Flagged']);
  46. assert.deepEqual(entry.keywords, ['$Label1']);
  47. assert.equal(entry.read, false);
  48. const stored = await readMaildirMessage(entry);
  49. assert.deepEqual(stored.bytes, rawMessageBytes);
  50. assert.deepEqual(await readFile(entry.filePath), rawMessageBytes);
  51. const updated = await setMaildirMessageSeen({
  52. root,
  53. address,
  54. storageKey: storage.key,
  55. relpath: storage.relpath,
  56. seen: true
  57. });
  58. assert.match(updated.relpath, /\/cur\//);
  59. assert.match(updated.relpath, /:2,FSa$/);
  60. const [seenEntry] = await scanMaildirMailbox({ root, address });
  61. assert.deepEqual(seenEntry.flags, ['\\Flagged', '\\Seen']);
  62. assert.deepEqual(seenEntry.keywords, ['$Label1']);
  63. assert.equal(seenEntry.read, true);
  64. });
  65. test('Maildir store rejects unsafe addresses and never overwrites a storage key', async () => {
  66. const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-safety-'));
  67. assert.throws(() => maildirHomePath(root, '../escape@example.com'), /地址不正确/);
  68. assert.equal(sqliteMaildirStorageKey(123), 'mhdb-123');
  69. assert.throws(() => sqliteMaildirStorageKey(0), /ID 不正确/);
  70. const first = Buffer.from('Subject: first\r\n\r\nfirst');
  71. const storage = await writeMaildirMessage({
  72. root,
  73. address: 'safe@example.com',
  74. rawMessageBytes: first,
  75. storageKey: 'mhdb-1'
  76. });
  77. assert.match(storage.relpath, /\/new\//);
  78. assert.doesNotMatch(storage.relpath, /:2,/);
  79. await assert.rejects(
  80. writeMaildirMessage({
  81. root,
  82. address: 'safe@example.com',
  83. rawMessageBytes: Buffer.from('Subject: second\r\n\r\nsecond'),
  84. storageKey: 'mhdb-1'
  85. }),
  86. /存储标识冲突/
  87. );
  88. const [entry] = await scanMaildirMailbox({ root, address: 'safe@example.com' });
  89. assert.deepEqual((await readMaildirMessage(entry)).bytes, first);
  90. });
  91. test('Maildir keyword letters never become uppercase system flags', async () => {
  92. const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-keywords-'));
  93. const keywords = ['$Label1', '$Label2', '$Label3', '$Label4'];
  94. await writeMaildirMessage({
  95. root,
  96. address: 'keywords@example.com',
  97. rawMessageBytes: Buffer.from('Subject: keyword flags\r\n\r\nBody'),
  98. keywords,
  99. storageKey: 'mhdb-7'
  100. });
  101. const [entry] = await scanMaildirMailbox({ root, address: 'keywords@example.com' });
  102. assert.deepEqual(entry.flags, []);
  103. assert.deepEqual(entry.keywords, keywords);
  104. });
  105. test('Maildir folder encoding keeps literal dots distinct from hierarchy separators', async () => {
  106. const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-folder-dots-'));
  107. const address = 'folders@example.com';
  108. const dottedPath = maildirFolderPath(root, address, 'team.ops');
  109. const nestedPath = maildirFolderPath(root, address, 'team/ops');
  110. assert.notEqual(dottedPath, nestedPath);
  111. assert.match(dottedPath, /\^2e/);
  112. assert.notEqual(
  113. maildirFolderPath(root, address, 'team^2eops'),
  114. dottedPath
  115. );
  116. assert.match(maildirFolderPath(root, address, '~archive'), /\.\^7earchive$/);
  117. await writeMaildirMessage({
  118. root,
  119. address,
  120. folder: 'team.ops',
  121. rawMessageBytes: Buffer.from('Subject: dotted\r\n\r\nDotted'),
  122. storageKey: 'mhdb-8'
  123. });
  124. await writeMaildirMessage({
  125. root,
  126. address,
  127. folder: 'team/ops',
  128. rawMessageBytes: Buffer.from('Subject: nested\r\n\r\nNested'),
  129. storageKey: 'mhdb-9'
  130. });
  131. const entries = await scanMaildirMailbox({ root, address });
  132. assert.deepEqual(entries.map((entry) => entry.folder).sort(), ['team.ops', 'team/ops']);
  133. });
  134. test('Maildir folder inventory ignores Dovecot internal and incomplete dot directories', async () => {
  135. const root = mkdtempSync(path.join(tmpdir(), 'mailhub-maildir-folder-inventory-'));
  136. const address = 'inventory@example.com';
  137. await writeMaildirMessage({
  138. root,
  139. address,
  140. folder: 'Projects',
  141. rawMessageBytes: Buffer.from('Subject: project\r\n\r\nBody'),
  142. storageKey: 'mhdb-10'
  143. });
  144. const mailRoot = path.join(maildirHomePath(root, address), 'mail');
  145. await mkdir(path.join(mailRoot, '.dovecot-internal'), { recursive: true });
  146. await mkdir(path.join(mailRoot, '.incomplete', 'cur'), { recursive: true });
  147. assert.deepEqual(
  148. (await listMaildirFolders({ root, address })).sort(),
  149. ['INBOX', 'Projects']
  150. );
  151. });