vesta-import-db.test.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import assert from 'node:assert/strict';
  2. import { mkdtempSync } from 'node:fs';
  3. import os from 'node:os';
  4. import path from 'node:path';
  5. import { test } from 'node:test';
  6. import {
  7. createDomain,
  8. createImportedInboundMessage,
  9. getInboundMailbox,
  10. getInboundMailboxProtocolMessage,
  11. getInboundMessage,
  12. hasImportedInboundMessage,
  13. initDatabase,
  14. listInboundMailboxProtocolMessages,
  15. searchInboundMessages,
  16. seedAdminUser,
  17. upsertImportedInboundMailbox,
  18. verifyInboundMailboxCredential
  19. } from '../src/db.js';
  20. import { createDkimKeyPair } from '../src/dkim.js';
  21. import { isLegacyPasswordHash } from '../src/password-hash.js';
  22. function tempDataDir() {
  23. return mkdtempSync(path.join(os.tmpdir(), 'mailhub-vesta-import-'));
  24. }
  25. function createTestDomain(userId, domainName) {
  26. const keys = createDkimKeyPair();
  27. return createDomain(userId, {
  28. domain: domainName,
  29. selector: 'mhimport',
  30. verificationToken: 'import-verification-token',
  31. dkimPublic: keys.publicKey,
  32. dkimPrivate: keys.privateKey,
  33. senderHost: `mail.${domainName}`,
  34. sendingIp: '192.0.2.10',
  35. spfExtra: '',
  36. dmarcPolicy: 'none',
  37. dmarcRua: ''
  38. });
  39. }
  40. test('imports Vesta mailbox hashes without plaintext and upgrades after successful login', () => {
  41. initDatabase(tempDataDir(), 'import-test-secret');
  42. const user = seedAdminUser({ username: 'admin', email: 'admin@example.test', password: 'admin-password' });
  43. createTestDomain(user.id, 'legacy.example');
  44. const legacyHash = '{MD5}$1$hfT7jp2q$G3yf0NUx7mUkX.LIFWQxN.';
  45. const mailbox = upsertImportedInboundMailbox(user.id, {
  46. address: 'inbox@legacy.example',
  47. passwordHash: legacyHash,
  48. quotaMb: null,
  49. status: 'active'
  50. });
  51. const before = getInboundMailbox(mailbox.id, user.id, { includeHash: true, includeSecret: true });
  52. assert.equal(before.passwordHash, legacyHash);
  53. assert.equal(before.passwordSecret, '');
  54. assert.equal(before.passwordRecoverable, false);
  55. assert.equal(verifyInboundMailboxCredential(mailbox.address, 'wrong password'), null);
  56. assert.ok(verifyInboundMailboxCredential(mailbox.address, 'password'));
  57. const upgraded = getInboundMailbox(mailbox.id, user.id, { includeHash: true, includeSecret: true });
  58. assert.match(upgraded.passwordHash, /^scrypt\$/);
  59. assert.equal(isLegacyPasswordHash(upgraded.passwordHash), false);
  60. assert.equal(upgraded.passwordSecret, '');
  61. upsertImportedInboundMailbox(user.id, { address: mailbox.address, passwordHash: legacyHash });
  62. const afterResume = getInboundMailbox(mailbox.id, user.id, { includeHash: true });
  63. assert.equal(afterResume.passwordHash, upgraded.passwordHash);
  64. });
  65. test('imports raw Maildir bytes, flags, timestamps and skips the same source key', () => {
  66. initDatabase(tempDataDir(), 'import-test-secret');
  67. const user = seedAdminUser({ username: 'admin', email: 'admin@example.test', password: 'admin-password' });
  68. createTestDomain(user.id, 'mail.example');
  69. const mailbox = upsertImportedInboundMailbox(user.id, {
  70. address: 'archive@mail.example',
  71. passwordHash: '{MD5}$1$hfT7jp2q$G3yf0NUx7mUkX.LIFWQxN.'
  72. });
  73. const rawMessageBytes = Buffer.from('From: sender@example.test\r\nSubject: imported\r\n\r\nbody', 'utf8');
  74. const payload = {
  75. importSource: 'vesta:ali.ss5.xyz',
  76. sourceKey: 'maildir-v1-source-key',
  77. folder: 'Projects/2026',
  78. sender: 'sender@example.test',
  79. recipients: [mailbox.address],
  80. subject: 'imported',
  81. messageId: '',
  82. rawMessageBytes,
  83. textBody: 'body',
  84. flags: ['\\Seen', '\\Flagged'],
  85. keywords: ['$Forwarded'],
  86. receivedAt: '2026-07-01T12:34:56.000Z'
  87. };
  88. const first = createImportedInboundMessage(mailbox, payload);
  89. const second = createImportedInboundMessage(mailbox, payload);
  90. assert.equal(first.created, true);
  91. assert.equal(second.created, false);
  92. assert.equal(second.message.id, first.message.id);
  93. assert.equal(hasImportedInboundMessage(payload.importSource, payload.sourceKey), true);
  94. const message = getInboundMessage(user.id, first.message.id);
  95. assert.equal(message.rawMessage, rawMessageBytes.toString('utf8'));
  96. assert.equal(message.read, true);
  97. assert.deepEqual(message.flags, ['\\Seen', '\\Flagged']);
  98. assert.deepEqual(message.keywords, ['$Forwarded']);
  99. assert.equal(message.folder, 'Projects/2026');
  100. assert.equal(message.receivedAt, payload.receivedAt);
  101. const [summary] = listInboundMailboxProtocolMessages(mailbox, { folder: payload.folder });
  102. assert.equal(summary.rawMessageSize, rawMessageBytes.length);
  103. assert.equal(summary.pop3MessageSize, rawMessageBytes.length + 2);
  104. assert.equal(Object.hasOwn(summary, 'rawMessage'), false);
  105. assert.equal(Object.hasOwn(summary, 'rawMessageBytes'), false);
  106. const hydrated = getInboundMailboxProtocolMessage(mailbox, summary.id, { folder: payload.folder });
  107. assert.ok(hydrated.rawMessageBytes.equals(rawMessageBytes));
  108. const searchResult = searchInboundMessages(user.id, { folder: payload.folder });
  109. assert.equal(searchResult.messages[0].rawMessageSize, rawMessageBytes.length);
  110. assert.equal(Object.hasOwn(searchResult.messages[0], 'rawMessageBytes'), false);
  111. });