| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import assert from 'node:assert/strict';
- import { mkdtempSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import { test } from 'node:test';
- import {
- createDomain,
- createApiToken,
- createInboundMailbox,
- createInboundMessage,
- createUser,
- deleteDomain,
- getInboundMailboxByAddress,
- getInboundMailboxForSender,
- getInboundMessageMaildirStorage,
- getInboundMessage,
- initDatabase,
- listAdminInboundMailboxAccess,
- listDomains,
- listInboundMailboxFolders,
- listInboundMailboxDomains,
- listInboundMailboxes,
- listInboundMessages,
- markInboundMessageRead,
- replaceInboundMailboxGrants,
- resolveInboundRecipient,
- searchInboundMessages,
- transferDomain,
- updateDomain,
- updateInboundMailbox,
- upsertImportedInboundMailbox,
- verifySmtpCredential
- } from '../src/db.js';
- test('users can create inbound mailboxes and read received messages', () => {
- initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-db-')), 'inbound-secret');
- const user = createUser({ username: 'inbound-user', email: 'inbound-user@example.com', password: 'password123' });
- createDomain(user.id, {
- domain: 'inbound.example',
- selector: 'mh',
- verificationToken: 'verify',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: 'mail.inbound.example',
- sendingIp: '192.0.2.10',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- const mailbox = createInboundMailbox(user.id, {
- address: 'Support@Inbound.Example',
- displayName: 'Support',
- password: 'mailbox-pass-123',
- aliases: 'help desk',
- forwardTo: 'archive@example.net, ops@example.net',
- keepForwarded: false,
- quotaMb: 512
- });
- assert.equal(mailbox.address, 'support@inbound.example');
- assert.equal(mailbox.displayName, 'Support');
- assert.equal(mailbox.passwordSet, true);
- assert.deepEqual(mailbox.aliases, ['help', 'desk']);
- assert.deepEqual(mailbox.forwardTo, ['archive@example.net', 'ops@example.net']);
- assert.equal(mailbox.keepForwarded, false);
- assert.equal(mailbox.quotaMb, 512);
- assert.equal(mailbox.messageCount, 0);
- assert.equal(mailbox.unreadCount, 0);
- const resolved = getInboundMailboxByAddress('SUPPORT@INBOUND.EXAMPLE');
- assert.equal(resolved.id, mailbox.id);
- assert.equal(resolved.userId, user.id);
- assert.equal(verifySmtpCredential('support@inbound.example', 'mailbox-pass-123').user.id, user.id);
- assert.equal(verifySmtpCredential('support@inbound.example', 'wrong-password'), null);
- assert.equal(resolveInboundRecipient('help@inbound.example').mailbox.id, mailbox.id);
- const message = createInboundMessage(resolved, {
- sender: 'alice@example.net',
- recipients: ['support@inbound.example'],
- subject: 'Hello inbound',
- messageId: '<hello@example.net>',
- rawMessage: 'From: alice@example.net\r\nTo: support@inbound.example\r\nSubject: Hello inbound\r\n\r\nHello MailHub',
- textBody: 'Hello MailHub',
- htmlBody: ''
- });
- assert.equal(message.mailboxId, mailbox.id);
- assert.equal(message.read, false);
- const mailboxes = listInboundMailboxes(user.id);
- assert.equal(mailboxes.length, 1);
- assert.equal(mailboxes[0].messageCount, 1);
- assert.equal(mailboxes[0].unreadCount, 1);
- const messages = listInboundMessages(user.id);
- assert.equal(messages.length, 1);
- assert.equal(messages[0].subject, 'Hello inbound');
- assert.equal(messages[0].preview, 'Hello MailHub');
- assert.equal('rawMessage' in messages[0], false);
- const detail = getInboundMessage(user.id, message.id);
- assert.equal(detail.rawMessage.includes('Hello MailHub'), true);
- assert.equal(detail.textBody, 'Hello MailHub');
- const readMessage = markInboundMessageRead(user.id, message.id, true);
- assert.equal(readMessage.read, true);
- assert.equal(listInboundMailboxes(user.id)[0].unreadCount, 0);
- assert.equal(markInboundMessageRead(999999, message.id, true), null);
- });
- test('mailbox grants isolate owner, view, receive, send, token selection, and transfer access', () => {
- initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-grants-')), 'inbound-secret');
- const owner = createUser({ username: 'grant-owner', email: 'grant-owner@example.com', password: 'password123' });
- const viewer = createUser({ username: 'grant-viewer', email: 'grant-viewer@example.com', password: 'password123' });
- const receiver = createUser({ username: 'grant-receiver', email: 'grant-receiver@example.com', password: 'password123' });
- const sender = createUser({ username: 'grant-sender', email: 'grant-sender@example.com', password: 'password123' });
- const domain = createDomain(owner.id, {
- domain: 'grants.example',
- selector: 'mh',
- verificationToken: 'verify-grants',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: 'mail.grants.example',
- sendingIp: '192.0.2.31',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- const mailbox = createInboundMailbox(owner.id, {
- address: 'support@grants.example',
- aliases: ['help']
- });
- const message = createInboundMessage(mailbox, {
- sender: 'alice@example.net',
- recipients: [mailbox.address],
- subject: 'Mailbox grant message',
- rawMessage: 'Subject: Mailbox grant message\r\n\r\nShared body',
- textBody: 'Shared body'
- });
- const accessEntry = replaceInboundMailboxGrants(mailbox.id, [
- { userId: viewer.id, view: true },
- { userId: receiver.id, permissions: ['receive'] },
- { userId: sender.id, permissions: { send: true } }
- ]);
- assert.equal(accessEntry.mailbox.id, mailbox.id);
- assert.equal(accessEntry.owner.id, owner.id);
- assert.deepEqual(
- new Map(accessEntry.grants.map((grant) => [grant.user.id, grant.permissions])),
- new Map([
- [viewer.id, { view: true, receive: false, send: false }],
- [receiver.id, { view: true, receive: true, send: false }],
- [sender.id, { view: true, receive: false, send: true }]
- ])
- );
- assert.equal(listAdminInboundMailboxAccess().find((entry) => entry.mailbox.id === mailbox.id).owner.id, owner.id);
- assert.deepEqual(listInboundMailboxes(owner.id, { permission: 'view' })[0].access, {
- type: 'owner',
- permissions: { view: true, receive: true, send: true }
- });
- assert.deepEqual(listInboundMailboxes(viewer.id), []);
- const [viewOnlyMailbox] = listInboundMailboxes(viewer.id, { permission: 'view' });
- assert.deepEqual(viewOnlyMailbox.access, {
- type: 'assigned',
- permissions: { view: true, receive: false, send: false }
- });
- assert.equal(viewOnlyMailbox.ownerUserId, owner.id);
- assert.equal(viewOnlyMailbox.messageCount, null);
- assert.equal(viewOnlyMailbox.unreadCount, null);
- assert.equal(viewOnlyMailbox.lastMessageAt, null);
- assert.equal(getInboundMessage(viewer.id, message.id, { permission: 'receive' }), null);
- assert.equal(listInboundMailboxFolders(viewer.id, mailbox.id, { permission: 'receive' }), null);
- assert.equal(getInboundMessageMaildirStorage(viewer.id, message.id, { permission: 'receive' }), null);
- assert.equal(markInboundMessageRead(viewer.id, message.id, true, { permission: 'receive' }), null);
- assert.equal(searchInboundMessages(receiver.id, { pageSize: 100 }).total, 0);
- const [receivedMailbox] = listInboundMailboxes(receiver.id, { permission: 'view' });
- assert.equal(receivedMailbox.access.permissions.receive, true);
- assert.equal(receivedMailbox.messageCount, 1);
- assert.equal(receivedMailbox.unreadCount, 1);
- assert.equal(searchInboundMessages(receiver.id, { pageSize: 100 }, { permission: 'receive' }).total, 1);
- assert.equal(getInboundMessage(receiver.id, message.id, { permission: 'receive' }).subject, 'Mailbox grant message');
- assert.ok(listInboundMailboxFolders(receiver.id, mailbox.id, { permission: 'receive' }));
- assert.equal(getInboundMessageMaildirStorage(receiver.id, message.id, { permission: 'receive' }).id, message.id);
- assert.equal(markInboundMessageRead(receiver.id, message.id, true, { permission: 'receive' }).read, true);
- assert.equal(getInboundMailboxForSender(receiver.id, mailbox.address), null);
- assert.equal(getInboundMailboxForSender(sender.id, mailbox.address).id, mailbox.id);
- assert.equal(getInboundMailboxForSender(sender.id, 'help@grants.example').id, mailbox.id);
- assert.equal(getInboundMailboxForSender(sender.id, mailbox.address, { ownerOnly: true }), null);
- assert.equal(getInboundMailboxForSender(sender.id, mailbox.address, { mailboxIds: [] }), null);
- assert.equal(getInboundMailboxForSender(sender.id, mailbox.address, { mailboxIds: [mailbox.id] }).id, mailbox.id);
- const selectedToken = createApiToken(viewer.id, 'granted mailbox', {
- scopes: ['mailboxes:read'],
- mailboxAccess: 'selected',
- mailboxIds: [mailbox.id]
- });
- assert.equal(selectedToken.mailboxAccess, 'selected');
- assert.deepEqual(selectedToken.mailboxIds, [mailbox.id]);
- assert.equal(listInboundMailboxes(viewer.id, {
- permission: 'view',
- mailboxIds: selectedToken.mailboxIds
- }).length, 1);
- assert.equal(searchInboundMessages(viewer.id, { pageSize: 100 }, {
- permission: 'receive',
- mailboxIds: selectedToken.mailboxIds
- }).total, 0);
- const receiveToken = createApiToken(receiver.id, 'received mailbox', {
- scopes: ['messages:read'],
- mailboxAccess: 'selected',
- mailboxIds: [mailbox.id]
- });
- assert.equal(searchInboundMessages(receiver.id, { pageSize: 100 }, {
- permission: 'receive',
- mailboxIds: receiveToken.mailboxIds
- }).total, 1);
- assert.throws(
- () => replaceInboundMailboxGrants(mailbox.id, [{ userId: viewer.id, view: false, receive: false, send: false }]),
- /至少需要一项权限/
- );
- assert.throws(
- () => replaceInboundMailboxGrants(mailbox.id, [{ userId: owner.id, view: true }]),
- /所有者无需额外授权/
- );
- assert.throws(
- () => replaceInboundMailboxGrants(mailbox.id, [{ userId: 999999, view: true }]),
- /用户不存在/
- );
- assert.throws(
- () => replaceInboundMailboxGrants(mailbox.id, [
- { userId: viewer.id, view: true },
- { userId: viewer.id, receive: true }
- ]),
- /不能重复/
- );
- assert.equal(listAdminInboundMailboxAccess().find((entry) => entry.mailbox.id === mailbox.id).grants.length, 3);
- replaceInboundMailboxGrants(mailbox.id, []);
- assert.equal(listInboundMailboxes(viewer.id, {
- permission: 'view',
- mailboxIds: selectedToken.mailboxIds
- }).length, 0);
- assert.equal(getInboundMailboxForSender(sender.id, mailbox.address), null);
- replaceInboundMailboxGrants(mailbox.id, [{ userId: sender.id, send: true }]);
- transferDomain({ actorUserId: owner.id, domainId: domain.id, targetUserId: sender.id });
- const transferred = listAdminInboundMailboxAccess().find((entry) => entry.mailbox.id === mailbox.id);
- assert.equal(transferred.owner.id, sender.id);
- assert.deepEqual(transferred.grants, []);
- });
- test('domains can route unknown inbound recipients to catch-all targets', () => {
- initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-catchall-')), 'inbound-secret');
- const user = createUser({ username: 'catch-user', email: 'catch@example.com', password: 'password123' });
- const domain = createDomain(user.id, {
- domain: 'catch.example',
- selector: 'mh',
- verificationToken: 'verify',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: 'mail.catch.example',
- sendingIp: '192.0.2.20',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- const mailbox = createInboundMailbox(user.id, { address: 'share@catch.example' });
- const updated = updateDomain(domain.id, user.id, { catchAllAddress: 'share@catch.example' });
- assert.equal(updated.catchAllAddress, 'share@catch.example');
- const localRoute = resolveInboundRecipient('missing@catch.example');
- assert.equal(localRoute.catchAll, true);
- assert.equal(localRoute.mailbox.id, mailbox.id);
- assert.equal(localRoute.recipient, 'missing@catch.example');
- updateDomain(domain.id, user.id, { catchAllAddress: '/dev/null' });
- const dropRoute = resolveInboundRecipient('drop@catch.example');
- assert.equal(dropRoute.drop, true);
- assert.equal(dropRoute.mailbox, null);
- updateDomain(domain.id, user.id, { catchAllAddress: 'external@example.net' });
- const forwardRoute = resolveInboundRecipient('forward@catch.example');
- assert.deepEqual(forwardRoute.forwardTo, ['external@example.net']);
- assert.equal(forwardRoute.mailbox, null);
- });
- test('domain owners can share mailbox creation without sharing domain management', () => {
- initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-db-scope-')), 'inbound-secret');
- const owner = createUser({ username: 'owner-user', email: 'owner@example.com', password: 'password123' });
- const other = createUser({ username: 'other-user', email: 'other@example.com', password: 'password123' });
- const domain = createDomain(owner.id, {
- domain: 'owned.example',
- selector: 'mh',
- verificationToken: 'verify',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: 'mail.owned.example',
- sendingIp: '192.0.2.11',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- assert.throws(
- () => createInboundMailbox(other.id, { address: 'support@owned.example' }),
- /收信域名不存在/
- );
- assert.equal(listInboundMailboxDomains(other.id).some((item) => item.domain === 'owned.example'), false);
- const shared = updateDomain(domain.id, owner.id, { mailboxSignupEnabled: true });
- assert.equal(shared.mailboxSignupEnabled, true);
- assert.equal(listDomains(other.id).some((item) => item.domain === 'owned.example'), false);
- const sharedDomains = listInboundMailboxDomains(other.id);
- assert.deepEqual(
- sharedDomains.map((item) => item.domain),
- ['owned.example']
- );
- assert.deepEqual(Object.keys(sharedDomains[0]).sort(), ['domain', 'id', 'mailboxSignupEnabled', 'userId']);
- const mailbox = createInboundMailbox(other.id, {
- address: 'support@owned.example',
- password: 'mailbox-pass-123'
- });
- assert.equal(mailbox.userId, other.id);
- assert.equal(mailbox.domainId, domain.id);
- assert.equal(verifySmtpCredential('support@owned.example', 'mailbox-pass-123').user.id, other.id);
- const message = createInboundMessage(mailbox, {
- sender: 'sender@example.net',
- recipients: ['support@owned.example'],
- subject: 'Shared domain',
- rawMessage: 'From: sender@example.net\r\nTo: support@owned.example\r\nSubject: Shared domain\r\n\r\nHello',
- textBody: 'Hello'
- });
- assert.equal(listInboundMailboxes(other.id).length, 1);
- assert.equal(listInboundMessages(other.id)[0].id, message.id);
- assert.equal(listInboundMessages(owner.id).length, 0);
- assert.throws(
- () => deleteDomain(domain.id, owner.id),
- /该域名仍有关联收信邮箱/
- );
- assert.equal(listDomains(owner.id)[0].id, domain.id);
- assert.equal(getInboundMailboxByAddress(mailbox.address).id, mailbox.id);
- assert.equal(getInboundMessage(other.id, message.id).id, message.id);
- });
- test('mailbox addresses and aliases share one namespace per domain', () => {
- initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-namespace-')), 'inbound-secret');
- const owner = createUser({ username: 'namespace-owner', email: 'namespace-owner@example.com', password: 'password123' });
- const other = createUser({ username: 'namespace-user', email: 'namespace-user@example.com', password: 'password123' });
- const domain = createDomain(owner.id, {
- domain: 'namespace.example',
- selector: 'mh',
- verificationToken: 'verify',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: 'mail.namespace.example',
- sendingIp: '192.0.2.12',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- updateDomain(domain.id, owner.id, { mailboxSignupEnabled: true });
- const primary = createInboundMailbox(owner.id, {
- address: 'primary@namespace.example',
- aliases: ['billing', 'legacy']
- });
- assert.throws(
- () => createInboundMailbox(other.id, { address: 'billing@namespace.example' }),
- /billing@namespace\.example 已被其他邮箱占用/
- );
- assert.throws(
- () => createInboundMailbox(other.id, {
- address: 'second@namespace.example',
- aliases: ['primary']
- }),
- /primary@namespace\.example 已被其他邮箱占用/
- );
- const second = createInboundMailbox(other.id, {
- address: 'second@namespace.example',
- aliases: ['support']
- });
- assert.deepEqual(updateInboundMailbox(other.id, second.id, { aliases: ['support'] }).aliases, ['support']);
- assert.throws(
- () => updateInboundMailbox(other.id, second.id, { aliases: ['legacy'] }),
- /legacy@namespace\.example 已被其他邮箱占用/
- );
- assert.throws(
- () => upsertImportedInboundMailbox(owner.id, { address: 'legacy@namespace.example' }),
- /legacy@namespace\.example 已被其他邮箱占用/
- );
- const imported = upsertImportedInboundMailbox(owner.id, {
- address: 'archive@namespace.example',
- aliases: ['old-archive']
- });
- assert.throws(
- () => upsertImportedInboundMailbox(owner.id, {
- address: imported.address,
- aliases: ['support']
- }),
- /support@namespace\.example 已被其他邮箱占用/
- );
- assert.equal(resolveInboundRecipient('billing@namespace.example').mailbox.id, primary.id);
- assert.equal(resolveInboundRecipient('support@namespace.example').mailbox.id, second.id);
- });
|