| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- import assert from 'node:assert/strict';
- import { spawn, spawnSync } from 'node:child_process';
- import { mkdtempSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import net from 'node:net';
- import path from 'node:path';
- import { test } from 'node:test';
- test('listing APIs paginate, filter, isolate users, and expose folder counts', async () => {
- const fixture = await startTestServer();
- try {
- const seeded = seedListingFixtures(fixture.dataDir, fixture.sessionSecret);
- const aliceCookie = await login(fixture.baseUrl, 'list-alice', 'password123');
- const bobCookie = await login(fixture.baseUrl, 'list-bob', 'password123');
- const defaultEvents = await getJson(fixture.baseUrl, '/api/events', aliceCookie);
- assert.equal(defaultEvents.status, 200);
- assert.equal(defaultEvents.body.total, 5);
- assert.equal(defaultEvents.body.page, 1);
- assert.equal(defaultEvents.body.pageSize, 30);
- assert.equal(defaultEvents.body.events.length, 5);
- const firstPage = await getJson(fixture.baseUrl, '/api/events?page=1&pageSize=2', aliceCookie);
- const secondPage = await getJson(fixture.baseUrl, '/api/events?page=2&pageSize=2', aliceCookie);
- assert.equal(firstPage.body.total, 5);
- assert.equal(firstPage.body.events.length, 2);
- assert.equal(secondPage.body.events.length, 2);
- assert.equal(new Set([...firstPage.body.events, ...secondPage.body.events].map((event) => event.id)).size, 4);
- assert.equal((await getJson(fixture.baseUrl, '/api/events?status=failed', aliceCookie)).body.total, 1);
- assert.equal(
- (await getJson(fixture.baseUrl, `/api/events?domainId=${seeded.aliceSecondaryDomainId}`, aliceCookie)).body.total,
- 1
- );
- assert.equal(
- (await getJson(fixture.baseUrl, '/api/events?recipient=target%2Bfilter%40example.net', aliceCookie)).body.total,
- 1
- );
- const byQueue = await getJson(fixture.baseUrl, '/api/events?q=QUEUE-ALICE-FAILED', aliceCookie);
- assert.equal(byQueue.body.total, 1);
- assert.equal(byQueue.body.events[0].status, 'failed');
- const byMessageId = await getJson(fixture.baseUrl, `/api/events?q=mh-${seeded.specialEventId}`, aliceCookie);
- assert.equal(byMessageId.body.total, 1);
- assert.equal(byMessageId.body.events[0].messageId, `mh-${seeded.specialEventId}`);
- const byNumericId = await getJson(fixture.baseUrl, `/api/events?q=${seeded.specialEventId}`, aliceCookie);
- assert.equal(byNumericId.body.total, 1);
- assert.equal(byNumericId.body.events[0].id, seeded.specialEventId);
- const bySubject = await getJson(fixture.baseUrl, '/api/events?q=Message%20identifier%20event', aliceCookie);
- assert.equal(bySubject.body.total, 1);
- assert.equal(bySubject.body.events[0].id, seeded.specialEventId);
- assert.equal((await getJson(fixture.baseUrl, '/api/events?from=2000-01-01', aliceCookie)).body.total, 5);
- assert.equal((await getJson(fixture.baseUrl, '/api/events?to=2000-01-01', aliceCookie)).body.total, 0);
- const bobEvents = await getJson(fixture.baseUrl, '/api/events?q=QUEUE-BOB-ONLY', bobCookie);
- assert.equal(bobEvents.body.total, 1);
- assert.equal((await getJson(fixture.baseUrl, '/api/events?q=QUEUE-BOB-ONLY', aliceCookie)).body.total, 0);
- const inboxPage = await getJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.aliceMailboxId}&page=1&pageSize=1`,
- aliceCookie
- );
- assert.equal(inboxPage.status, 200);
- assert.equal(inboxPage.body.total, 2);
- assert.equal(inboxPage.body.messages.length, 1);
- assert.equal(inboxPage.body.page, 1);
- assert.equal(inboxPage.body.pageSize, 1);
- const unread = await getJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.aliceMailboxId}&folder=INBOX&read=false`,
- aliceCookie
- );
- assert.equal(unread.body.total, 1);
- assert.equal(unread.body.messages[0].read, false);
- const sent = await getJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.aliceMailboxId}&folder=Sent`,
- aliceCookie
- );
- assert.equal(sent.body.total, 1);
- assert.equal(sent.body.messages[0].folder, 'Sent');
- const custom = await getJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.aliceMailboxId}&folder=Projects`,
- aliceCookie
- );
- assert.equal(custom.body.total, 1);
- assert.equal(custom.body.messages[0].folder, 'Projects');
- const byInboundMessageId = await getJson(
- fixture.baseUrl,
- '/api/inbound-messages?folder=Projects&q=inbound-project-special',
- aliceCookie
- );
- assert.equal(byInboundMessageId.body.total, 1);
- assert.equal(byInboundMessageId.body.messages[0].messageId, '<inbound-project-special@example.net>');
- const isolatedMessages = await getJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.aliceMailboxId}`,
- bobCookie
- );
- assert.equal(isolatedMessages.body.total, 0);
- const folders = await getJson(
- fixture.baseUrl,
- `/api/inbound-mailboxes/${seeded.aliceMailboxId}/folders`,
- aliceCookie
- );
- assert.equal(folders.status, 200);
- assert.deepEqual(folderSummary(folders.body.folders, 'INBOX'), {
- name: 'INBOX',
- specialUse: null,
- messageCount: 2,
- unreadCount: 1
- });
- assert.deepEqual(folderSummary(folders.body.folders, 'Sent'), {
- name: 'Sent',
- specialUse: '\\Sent',
- messageCount: 1,
- unreadCount: 0
- });
- assert.deepEqual(folderSummary(folders.body.folders, 'Projects'), {
- name: 'Projects',
- specialUse: null,
- messageCount: 1,
- unreadCount: 1
- });
- assert.ok(folders.body.folders.some((folder) => folder.name === 'Drafts' && folder.messageCount === 0));
- const isolatedFolders = await getJson(
- fixture.baseUrl,
- `/api/inbound-mailboxes/${seeded.aliceMailboxId}/folders`,
- bobCookie
- );
- assert.equal(isolatedFolders.status, 404);
- assert.deepEqual(isolatedFolders.body.folders, []);
- for (const pathName of [
- '/api/events?page=0',
- '/api/events?page=abc',
- '/api/events?pageSize=101',
- '/api/events?domainId=-1',
- '/api/events?status=sent%20OR%201%3D1',
- '/api/events?from=not-a-date',
- '/api/events?from=2026-07-02&to=2026-07-01',
- '/api/inbound-messages?page=0',
- '/api/inbound-messages?pageSize=101',
- '/api/inbound-messages?mailboxId=nope',
- '/api/inbound-messages?folder=',
- '/api/inbound-messages?read=1'
- ]) {
- const response = await getJson(fixture.baseUrl, pathName, aliceCookie);
- assert.equal(response.status, 400, pathName);
- assert.equal(typeof response.body.error, 'string');
- }
- } finally {
- fixture.child.kill('SIGTERM');
- await waitForExit(fixture.child, 1000);
- }
- });
- test('SMTP API responses keep passwords write-only while internal relay auth still works', async () => {
- const relayServer = await startFakeSmtpServer();
- const fixture = await startTestServer();
- try {
- const cookie = await login(fixture.baseUrl, 'admin', 'password123');
- const singularSave = await requestJson(fixture.baseUrl, '/api/smtp-credential', cookie, {
- method: 'PUT',
- body: { username: 'legacy-smtp', password: 'legacy-secret' }
- });
- assertSecretSummary(singularSave.body.credential);
- const singularGet = await getJson(fixture.baseUrl, '/api/smtp-credential', cookie);
- assertSecretSummary(singularGet.body.credential);
- const credentialCreate = await requestJson(fixture.baseUrl, '/api/smtp-credentials', cookie, {
- method: 'POST',
- body: { username: 'app-smtp', password: 'app-secret' }
- });
- assert.equal(credentialCreate.status, 201);
- assertSecretSummary(credentialCreate.body.credential);
- const credentialId = credentialCreate.body.credential.id;
- const credentialList = await getJson(fixture.baseUrl, '/api/smtp-credentials', cookie);
- credentialList.body.credentials.forEach(assertSecretSummary);
- assertSecretSummary((await getJson(fixture.baseUrl, `/api/smtp-credentials/${credentialId}`, cookie)).body.credential);
- const credentialPatch = await requestJson(fixture.baseUrl, `/api/smtp-credentials/${credentialId}`, cookie, {
- method: 'PATCH',
- body: { username: 'app-smtp-renamed' }
- });
- assertSecretSummary(credentialPatch.body.credential);
- const domain = await requestJson(fixture.baseUrl, '/api/domains', cookie, {
- method: 'POST',
- body: {
- domain: 'write-only-relay.example',
- selector: 'mh',
- senderHost: 'mail.write-only-relay.example',
- sendingIp: '127.0.0.1'
- }
- });
- assert.equal(domain.status, 201);
- const relayCreate = await requestJson(fixture.baseUrl, '/api/smtp-relays', cookie, {
- method: 'POST',
- body: {
- name: 'Write-only relay',
- host: '127.0.0.1',
- port: relayServer.port,
- secure: false,
- username: 'relay-user',
- password: 'relay-secret',
- helo: 'mail.write-only-relay.example',
- isDefault: true
- }
- });
- assert.equal(relayCreate.status, 201);
- assertSecretSummary(relayCreate.body.relay);
- const relayId = relayCreate.body.relay.id;
- (await getJson(fixture.baseUrl, '/api/smtp-relays', cookie)).body.relays.forEach(assertSecretSummary);
- assertSecretSummary((await getJson(fixture.baseUrl, `/api/smtp-relays/${relayId}`, cookie)).body.relay);
- const relayPatch = await requestJson(fixture.baseUrl, `/api/smtp-relays/${relayId}`, cookie, {
- method: 'PATCH',
- body: { name: 'Write-only relay renamed' }
- });
- assertSecretSummary(relayPatch.body.relay);
- const send = await requestJson(fixture.baseUrl, '/api/send', cookie, {
- method: 'POST',
- body: {
- from: 'noreply@write-only-relay.example',
- to: 'recipient@example.net',
- subject: 'write-only credential test',
- text: 'hello',
- smtpRelayId: relayId
- }
- });
- assert.equal(send.status, 202);
- const authCommand = relayServer.commands.find((command) => command.startsWith('AUTH PLAIN '));
- assert.ok(authCommand);
- assert.equal(
- Buffer.from(authCommand.slice('AUTH PLAIN '.length), 'base64').toString('utf8'),
- '\0relay-user\0relay-secret'
- );
- } finally {
- fixture.child.kill('SIGTERM');
- await waitForExit(fixture.child, 1000);
- await relayServer.close();
- }
- });
- test('authentication next paths preserve safe deep links and reject open redirects', async () => {
- const fixture = await startTestServer();
- try {
- const target = '/domains/42?tab=dns&q=pending';
- const anonymous = await fetch(`${fixture.baseUrl}${target}`, { redirect: 'manual' });
- assert.equal(anonymous.status, 302);
- const loginLocation = new URL(anonymous.headers.get('location'), fixture.baseUrl);
- assert.equal(loginLocation.pathname, '/login');
- assert.equal(loginLocation.searchParams.get('next'), target);
- const successfulLogin = await loginResponse(fixture.baseUrl, 'admin', 'password123', target);
- assert.equal(successfulLogin.status, 200);
- const loginBody = await successfulLogin.json();
- assert.equal(loginBody.redirectTo, target);
- const cookie = sessionCookieFrom(successfulLogin);
- assert.ok(cookie);
- const authenticatedLogin = await fetch(
- `${fixture.baseUrl}/login?next=${encodeURIComponent(target)}`,
- { headers: { Cookie: cookie }, redirect: 'manual' }
- );
- assert.equal(authenticatedLogin.status, 302);
- assert.equal(authenticatedLogin.headers.get('location'), target);
- const queryUrlTarget = '/activity?q=https%3A%2F%2Fexample.com%2Fmessage';
- const queryUrlLogin = await loginResponse(fixture.baseUrl, 'admin', 'password123', queryUrlTarget);
- assert.equal(queryUrlLogin.status, 200);
- assert.equal((await queryUrlLogin.json()).redirectTo, queryUrlTarget);
- for (const unsafe of [
- '//evil.example/path',
- 'https://evil.example/path',
- '/\\evil.example/path',
- '/%2F%2Fevil.example/path',
- '/%5C%5Cevil.example/path',
- '/%0Aevil',
- '/api/events'
- ]) {
- const response = await loginResponse(fixture.baseUrl, 'admin', 'password123', unsafe);
- assert.equal(response.status, 200, unsafe);
- assert.equal((await response.json()).redirectTo, '/', unsafe);
- }
- const invalidAuthenticatedLogin = await fetch(
- `${fixture.baseUrl}/login?next=${encodeURIComponent('//evil.example/path')}`,
- { headers: { Cookie: cookie }, redirect: 'manual' }
- );
- assert.equal(invalidAuthenticatedLogin.headers.get('location'), '/');
- } finally {
- fixture.child.kill('SIGTERM');
- await waitForExit(fixture.child, 1000);
- }
- });
- function seedListingFixtures(dataDir, sessionSecret) {
- const script = `
- import {
- createDomain,
- createInboundMailbox,
- createInboundMessage,
- createUser,
- initDatabase,
- logSendEvent,
- markInboundMessageRead
- } from './src/db.js';
- initDatabase(process.env.DATA_DIR, process.env.SESSION_SECRET);
- const alice = createUser({ username: 'list-alice', email: 'list-alice@example.com', password: 'password123', status: 'active' });
- const bob = createUser({ username: 'list-bob', email: 'list-bob@example.com', password: 'password123', status: 'active' });
- const domain = (userId, name) => createDomain(userId, {
- domain: name,
- selector: 'mh',
- verificationToken: 'token-' + name,
- dkimPublic: 'public-' + name,
- dkimPrivate: 'private-' + name,
- senderHost: 'mail.' + name,
- sendingIp: '127.0.0.1',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- const alicePrimary = domain(alice.id, 'listing-alice.example');
- const aliceSecondary = domain(alice.id, 'listing-alice-secondary.example');
- const bobDomain = domain(bob.id, 'listing-bob.example');
- const events = [
- { domainId: alicePrimary.id, status: 'sent', recipient: 'one@example.net', subject: 'First event', queueId: 'QUEUE-ALICE-1' },
- { domainId: alicePrimary.id, status: 'failed', recipient: 'two@example.net', subject: 'Failed event', queueId: 'QUEUE-ALICE-FAILED' },
- { domainId: alicePrimary.id, status: 'bounced', recipient: 'target+filter@example.net', subject: 'Recipient filter event', queueId: 'QUEUE-ALICE-3' },
- { domainId: aliceSecondary.id, status: 'sent', recipient: 'four@example.net', subject: 'Secondary domain event', queueId: 'QUEUE-ALICE-4' },
- { domainId: alicePrimary.id, status: 'sent', recipient: 'five@example.net', subject: 'Message identifier event', queueId: 'QUEUE-ALICE-5' }
- ];
- let specialEventId = null;
- for (const event of events) {
- const eventId = Number(logSendEvent({
- userId: alice.id,
- domainId: event.domainId,
- sender: 'noreply@listing-alice.example',
- recipients: [event.recipient],
- subject: event.subject,
- status: event.status,
- detail: event.status + ' detail',
- queueId: event.queueId
- }));
- if (event.subject === 'Message identifier event') specialEventId = eventId;
- }
- logSendEvent({
- userId: bob.id,
- domainId: bobDomain.id,
- sender: 'noreply@listing-bob.example',
- recipients: ['bob@example.net'],
- subject: 'Bob only',
- status: 'sent',
- detail: 'bob detail',
- queueId: 'QUEUE-BOB-ONLY'
- });
- const aliceMailbox = createInboundMailbox(alice.id, { address: 'inbox@listing-alice.example', password: 'mailbox-pass-123' });
- const bobMailbox = createInboundMailbox(bob.id, { address: 'inbox@listing-bob.example', password: 'mailbox-pass-123' });
- const createMessage = (mailbox, values) => createInboundMessage(mailbox, {
- sender: values.sender || 'sender@example.net',
- recipients: [mailbox.address],
- subject: values.subject,
- messageId: values.messageId,
- folder: values.folder || 'INBOX',
- rawMessage: 'Subject: ' + values.subject + '\\r\\n\\r\\n' + values.subject,
- textBody: values.subject,
- receivedAt: values.receivedAt
- });
- createMessage(aliceMailbox, { subject: 'Inbox unread', messageId: '<inbound-unread@example.net>', receivedAt: '2026-07-14T10:00:00.000Z' });
- const inboxRead = createMessage(aliceMailbox, { subject: 'Inbox read', messageId: '<inbound-read@example.net>', receivedAt: '2026-07-14T11:00:00.000Z' });
- markInboundMessageRead(alice.id, inboxRead.id, true);
- const sent = createMessage(aliceMailbox, { subject: 'Sent message', messageId: '<inbound-sent@example.net>', folder: 'Sent', receivedAt: '2026-07-14T12:00:00.000Z' });
- markInboundMessageRead(alice.id, sent.id, true);
- createMessage(aliceMailbox, { subject: 'Project message', messageId: '<inbound-project-special@example.net>', folder: 'Projects', receivedAt: '2026-07-14T13:00:00.000Z' });
- createMessage(bobMailbox, { subject: 'Bob private', messageId: '<inbound-bob@example.net>', receivedAt: '2026-07-14T14:00:00.000Z' });
- console.log(JSON.stringify({
- aliceSecondaryDomainId: aliceSecondary.id,
- aliceMailboxId: aliceMailbox.id,
- bobMailboxId: bobMailbox.id,
- specialEventId
- }));
- `;
- const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
- cwd: process.cwd(),
- env: { ...process.env, DATA_DIR: dataDir, SESSION_SECRET: sessionSecret },
- encoding: 'utf8'
- });
- assert.equal(result.status, 0, result.stderr || result.stdout);
- return JSON.parse(result.stdout);
- }
- async function startTestServer() {
- const port = await freePort();
- const dataDir = mkdtempSync(path.join(tmpdir(), 'mailhub-listing-api-test-'));
- const sessionSecret = 'listing-api-session-secret';
- const child = spawn(process.execPath, ['src/server.js'], {
- cwd: process.cwd(),
- env: {
- ...process.env,
- PORT: String(port),
- DATA_DIR: dataDir,
- SESSION_SECRET: sessionSecret,
- ADMIN_USER: 'admin',
- ADMIN_EMAIL: 'admin@example.com',
- ADMIN_PASSWORD: 'password123',
- DNS_AUTO_CHECK_ENABLED: 'false',
- DELIVERY_TRACKING_ENABLED: 'false',
- WEBHOOK_WORKER_ENABLED: 'false',
- SUBMISSION_ENABLED: 'false',
- IMAP_ENABLED: 'false',
- POP3_ENABLED: 'false'
- },
- stdio: ['ignore', 'pipe', 'pipe']
- });
- await waitForOutput(child, 'MailHub listening');
- return { child, baseUrl: `http://127.0.0.1:${port}`, dataDir, sessionSecret };
- }
- async function login(baseUrl, username, password) {
- const response = await loginResponse(baseUrl, username, password);
- assert.equal(response.status, 200);
- const cookie = sessionCookieFrom(response);
- assert.ok(cookie);
- return cookie;
- }
- function loginResponse(baseUrl, username, password, next = '') {
- return fetch(`${baseUrl}/api/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username, password, next })
- });
- }
- function sessionCookieFrom(response) {
- return response.headers.get('set-cookie')?.split(';')[0] || '';
- }
- async function getJson(baseUrl, pathname, cookie) {
- const response = await fetch(`${baseUrl}${pathname}`, { headers: { Cookie: cookie }, redirect: 'manual' });
- return { status: response.status, body: await response.json() };
- }
- async function requestJson(baseUrl, pathname, cookie, { method, body }) {
- const response = await fetch(`${baseUrl}${pathname}`, {
- method,
- headers: { 'Content-Type': 'application/json', Cookie: cookie },
- body: JSON.stringify(body)
- });
- return { status: response.status, body: await response.json() };
- }
- function folderSummary(folders, name) {
- const folder = folders.find((entry) => entry.name === name);
- assert.ok(folder, `expected folder ${name}`);
- return folder;
- }
- function assertSecretSummary(value) {
- assert.ok(value);
- assert.equal(value.passwordSet, true);
- assert.equal('password' in value, false);
- assert.equal('passwordHash' in value, false);
- assert.equal('passwordSecret' in value, false);
- assert.equal('passwordRecoverable' in value, false);
- }
- function freePort() {
- return new Promise((resolve, reject) => {
- const server = net.createServer();
- server.listen(0, '127.0.0.1', () => {
- const { port } = server.address();
- server.close((error) => (error ? reject(error) : resolve(port)));
- });
- server.on('error', reject);
- });
- }
- function waitForOutput(child, text, timeoutMs = 8000) {
- return new Promise((resolve, reject) => {
- let buffer = '';
- const timer = setTimeout(() => reject(new Error(`Timed out waiting for: ${text}\n${buffer}`)), timeoutMs);
- const onData = (chunk) => {
- buffer += String(chunk);
- if (!buffer.includes(text)) return;
- clearTimeout(timer);
- child.stdout?.off('data', onData);
- child.stderr?.off('data', onData);
- resolve();
- };
- child.stdout?.on('data', onData);
- child.stderr?.on('data', onData);
- });
- }
- function waitForExit(child, timeoutMs) {
- return new Promise((resolve) => {
- if (child.exitCode != null) return resolve(true);
- const timer = setTimeout(() => resolve(false), timeoutMs);
- child.once('exit', () => {
- clearTimeout(timer);
- resolve(true);
- });
- });
- }
- function startFakeSmtpServer() {
- const commands = [];
- const server = net.createServer((socket) => {
- socket.setEncoding('utf8');
- socket.write('220 relay.test ESMTP ready\r\n');
- let buffer = '';
- let dataMode = false;
- socket.on('data', (chunk) => {
- buffer += chunk;
- let index;
- while ((index = buffer.indexOf('\n')) !== -1) {
- const line = buffer.slice(0, index).replace(/\r$/, '');
- buffer = buffer.slice(index + 1);
- if (dataMode) {
- if (line === '.') {
- dataMode = false;
- socket.write('250 2.0.0 queued as WRITEONLY123\r\n');
- }
- continue;
- }
- commands.push(line);
- if (line.startsWith('EHLO')) socket.write('250-relay.test\r\n250 AUTH PLAIN\r\n');
- else if (line.startsWith('AUTH PLAIN')) socket.write('235 2.7.0 authenticated\r\n');
- else if (line.startsWith('MAIL FROM')) socket.write('250 2.1.0 ok\r\n');
- else if (line.startsWith('RCPT TO')) socket.write('250 2.1.5 ok\r\n');
- else if (line === 'DATA') {
- dataMode = true;
- socket.write('354 end data\r\n');
- } else if (line === 'QUIT') {
- socket.write('221 bye\r\n');
- socket.end();
- }
- }
- });
- });
- return new Promise((resolve, reject) => {
- server.once('error', reject);
- server.listen(0, '127.0.0.1', () => {
- server.off('error', reject);
- resolve({
- port: server.address().port,
- commands,
- close: () => new Promise((closeResolve) => server.close(closeResolve))
- });
- });
- });
- }
|