inbound-db.test.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import assert from 'node:assert/strict';
  2. import { mkdtempSync } from 'node:fs';
  3. import { tmpdir } from 'node:os';
  4. import path from 'node:path';
  5. import { test } from 'node:test';
  6. import {
  7. createDomain,
  8. createInboundMailbox,
  9. createInboundMessage,
  10. createUser,
  11. deleteDomain,
  12. getInboundMailboxByAddress,
  13. getInboundMessage,
  14. initDatabase,
  15. listDomains,
  16. listInboundMailboxDomains,
  17. listInboundMailboxes,
  18. listInboundMessages,
  19. markInboundMessageRead,
  20. resolveInboundRecipient,
  21. updateDomain,
  22. updateInboundMailbox,
  23. upsertImportedInboundMailbox,
  24. verifySmtpCredential
  25. } from '../src/db.js';
  26. test('users can create inbound mailboxes and read received messages', () => {
  27. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-db-')), 'inbound-secret');
  28. const user = createUser({ username: 'inbound-user', email: 'inbound-user@example.com', password: 'password123' });
  29. createDomain(user.id, {
  30. domain: 'inbound.example',
  31. selector: 'mh',
  32. verificationToken: 'verify',
  33. dkimPublic: 'public',
  34. dkimPrivate: 'private',
  35. senderHost: 'mail.inbound.example',
  36. sendingIp: '192.0.2.10',
  37. spfExtra: '',
  38. dmarcPolicy: 'none',
  39. dmarcRua: ''
  40. });
  41. const mailbox = createInboundMailbox(user.id, {
  42. address: 'Support@Inbound.Example',
  43. displayName: 'Support',
  44. password: 'mailbox-pass-123',
  45. aliases: 'help desk',
  46. forwardTo: 'archive@example.net, ops@example.net',
  47. keepForwarded: false,
  48. quotaMb: 512
  49. });
  50. assert.equal(mailbox.address, 'support@inbound.example');
  51. assert.equal(mailbox.displayName, 'Support');
  52. assert.equal(mailbox.passwordSet, true);
  53. assert.deepEqual(mailbox.aliases, ['help', 'desk']);
  54. assert.deepEqual(mailbox.forwardTo, ['archive@example.net', 'ops@example.net']);
  55. assert.equal(mailbox.keepForwarded, false);
  56. assert.equal(mailbox.quotaMb, 512);
  57. assert.equal(mailbox.messageCount, 0);
  58. assert.equal(mailbox.unreadCount, 0);
  59. const resolved = getInboundMailboxByAddress('SUPPORT@INBOUND.EXAMPLE');
  60. assert.equal(resolved.id, mailbox.id);
  61. assert.equal(resolved.userId, user.id);
  62. assert.equal(verifySmtpCredential('support@inbound.example', 'mailbox-pass-123').user.id, user.id);
  63. assert.equal(verifySmtpCredential('support@inbound.example', 'wrong-password'), null);
  64. assert.equal(resolveInboundRecipient('help@inbound.example').mailbox.id, mailbox.id);
  65. const message = createInboundMessage(resolved, {
  66. sender: 'alice@example.net',
  67. recipients: ['support@inbound.example'],
  68. subject: 'Hello inbound',
  69. messageId: '<hello@example.net>',
  70. rawMessage: 'From: alice@example.net\r\nTo: support@inbound.example\r\nSubject: Hello inbound\r\n\r\nHello MailHub',
  71. textBody: 'Hello MailHub',
  72. htmlBody: ''
  73. });
  74. assert.equal(message.mailboxId, mailbox.id);
  75. assert.equal(message.read, false);
  76. const mailboxes = listInboundMailboxes(user.id);
  77. assert.equal(mailboxes.length, 1);
  78. assert.equal(mailboxes[0].messageCount, 1);
  79. assert.equal(mailboxes[0].unreadCount, 1);
  80. const messages = listInboundMessages(user.id);
  81. assert.equal(messages.length, 1);
  82. assert.equal(messages[0].subject, 'Hello inbound');
  83. assert.equal(messages[0].preview, 'Hello MailHub');
  84. assert.equal('rawMessage' in messages[0], false);
  85. const detail = getInboundMessage(user.id, message.id);
  86. assert.equal(detail.rawMessage.includes('Hello MailHub'), true);
  87. assert.equal(detail.textBody, 'Hello MailHub');
  88. const readMessage = markInboundMessageRead(user.id, message.id, true);
  89. assert.equal(readMessage.read, true);
  90. assert.equal(listInboundMailboxes(user.id)[0].unreadCount, 0);
  91. assert.equal(markInboundMessageRead(999999, message.id, true), null);
  92. });
  93. test('domains can route unknown inbound recipients to catch-all targets', () => {
  94. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-catchall-')), 'inbound-secret');
  95. const user = createUser({ username: 'catch-user', email: 'catch@example.com', password: 'password123' });
  96. const domain = createDomain(user.id, {
  97. domain: 'catch.example',
  98. selector: 'mh',
  99. verificationToken: 'verify',
  100. dkimPublic: 'public',
  101. dkimPrivate: 'private',
  102. senderHost: 'mail.catch.example',
  103. sendingIp: '192.0.2.20',
  104. spfExtra: '',
  105. dmarcPolicy: 'none',
  106. dmarcRua: ''
  107. });
  108. const mailbox = createInboundMailbox(user.id, { address: 'share@catch.example' });
  109. const updated = updateDomain(domain.id, user.id, { catchAllAddress: 'share@catch.example' });
  110. assert.equal(updated.catchAllAddress, 'share@catch.example');
  111. const localRoute = resolveInboundRecipient('missing@catch.example');
  112. assert.equal(localRoute.catchAll, true);
  113. assert.equal(localRoute.mailbox.id, mailbox.id);
  114. assert.equal(localRoute.recipient, 'missing@catch.example');
  115. updateDomain(domain.id, user.id, { catchAllAddress: '/dev/null' });
  116. const dropRoute = resolveInboundRecipient('drop@catch.example');
  117. assert.equal(dropRoute.drop, true);
  118. assert.equal(dropRoute.mailbox, null);
  119. updateDomain(domain.id, user.id, { catchAllAddress: 'external@example.net' });
  120. const forwardRoute = resolveInboundRecipient('forward@catch.example');
  121. assert.deepEqual(forwardRoute.forwardTo, ['external@example.net']);
  122. assert.equal(forwardRoute.mailbox, null);
  123. });
  124. test('domain owners can share mailbox creation without sharing domain management', () => {
  125. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-db-scope-')), 'inbound-secret');
  126. const owner = createUser({ username: 'owner-user', email: 'owner@example.com', password: 'password123' });
  127. const other = createUser({ username: 'other-user', email: 'other@example.com', password: 'password123' });
  128. const domain = createDomain(owner.id, {
  129. domain: 'owned.example',
  130. selector: 'mh',
  131. verificationToken: 'verify',
  132. dkimPublic: 'public',
  133. dkimPrivate: 'private',
  134. senderHost: 'mail.owned.example',
  135. sendingIp: '192.0.2.11',
  136. spfExtra: '',
  137. dmarcPolicy: 'none',
  138. dmarcRua: ''
  139. });
  140. assert.throws(
  141. () => createInboundMailbox(other.id, { address: 'support@owned.example' }),
  142. /收信域名不存在/
  143. );
  144. assert.equal(listInboundMailboxDomains(other.id).some((item) => item.domain === 'owned.example'), false);
  145. const shared = updateDomain(domain.id, owner.id, { mailboxSignupEnabled: true });
  146. assert.equal(shared.mailboxSignupEnabled, true);
  147. assert.equal(listDomains(other.id).some((item) => item.domain === 'owned.example'), false);
  148. const sharedDomains = listInboundMailboxDomains(other.id);
  149. assert.deepEqual(
  150. sharedDomains.map((item) => item.domain),
  151. ['owned.example']
  152. );
  153. assert.deepEqual(Object.keys(sharedDomains[0]).sort(), ['domain', 'id', 'mailboxSignupEnabled', 'userId']);
  154. const mailbox = createInboundMailbox(other.id, {
  155. address: 'support@owned.example',
  156. password: 'mailbox-pass-123'
  157. });
  158. assert.equal(mailbox.userId, other.id);
  159. assert.equal(mailbox.domainId, domain.id);
  160. assert.equal(verifySmtpCredential('support@owned.example', 'mailbox-pass-123').user.id, other.id);
  161. const message = createInboundMessage(mailbox, {
  162. sender: 'sender@example.net',
  163. recipients: ['support@owned.example'],
  164. subject: 'Shared domain',
  165. rawMessage: 'From: sender@example.net\r\nTo: support@owned.example\r\nSubject: Shared domain\r\n\r\nHello',
  166. textBody: 'Hello'
  167. });
  168. assert.equal(listInboundMailboxes(other.id).length, 1);
  169. assert.equal(listInboundMessages(other.id)[0].id, message.id);
  170. assert.equal(listInboundMessages(owner.id).length, 0);
  171. assert.throws(
  172. () => deleteDomain(domain.id, owner.id),
  173. /该域名仍有关联收信邮箱/
  174. );
  175. assert.equal(listDomains(owner.id)[0].id, domain.id);
  176. assert.equal(getInboundMailboxByAddress(mailbox.address).id, mailbox.id);
  177. assert.equal(getInboundMessage(other.id, message.id).id, message.id);
  178. });
  179. test('mailbox addresses and aliases share one namespace per domain', () => {
  180. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-namespace-')), 'inbound-secret');
  181. const owner = createUser({ username: 'namespace-owner', email: 'namespace-owner@example.com', password: 'password123' });
  182. const other = createUser({ username: 'namespace-user', email: 'namespace-user@example.com', password: 'password123' });
  183. const domain = createDomain(owner.id, {
  184. domain: 'namespace.example',
  185. selector: 'mh',
  186. verificationToken: 'verify',
  187. dkimPublic: 'public',
  188. dkimPrivate: 'private',
  189. senderHost: 'mail.namespace.example',
  190. sendingIp: '192.0.2.12',
  191. spfExtra: '',
  192. dmarcPolicy: 'none',
  193. dmarcRua: ''
  194. });
  195. updateDomain(domain.id, owner.id, { mailboxSignupEnabled: true });
  196. const primary = createInboundMailbox(owner.id, {
  197. address: 'primary@namespace.example',
  198. aliases: ['billing', 'legacy']
  199. });
  200. assert.throws(
  201. () => createInboundMailbox(other.id, { address: 'billing@namespace.example' }),
  202. /billing@namespace\.example 已被其他邮箱占用/
  203. );
  204. assert.throws(
  205. () => createInboundMailbox(other.id, {
  206. address: 'second@namespace.example',
  207. aliases: ['primary']
  208. }),
  209. /primary@namespace\.example 已被其他邮箱占用/
  210. );
  211. const second = createInboundMailbox(other.id, {
  212. address: 'second@namespace.example',
  213. aliases: ['support']
  214. });
  215. assert.deepEqual(updateInboundMailbox(other.id, second.id, { aliases: ['support'] }).aliases, ['support']);
  216. assert.throws(
  217. () => updateInboundMailbox(other.id, second.id, { aliases: ['legacy'] }),
  218. /legacy@namespace\.example 已被其他邮箱占用/
  219. );
  220. assert.throws(
  221. () => upsertImportedInboundMailbox(owner.id, { address: 'legacy@namespace.example' }),
  222. /legacy@namespace\.example 已被其他邮箱占用/
  223. );
  224. const imported = upsertImportedInboundMailbox(owner.id, {
  225. address: 'archive@namespace.example',
  226. aliases: ['old-archive']
  227. });
  228. assert.throws(
  229. () => upsertImportedInboundMailbox(owner.id, {
  230. address: imported.address,
  231. aliases: ['support']
  232. }),
  233. /support@namespace\.example 已被其他邮箱占用/
  234. );
  235. assert.equal(resolveInboundRecipient('billing@namespace.example').mailbox.id, primary.id);
  236. assert.equal(resolveInboundRecipient('support@namespace.example').mailbox.id, second.id);
  237. });