submission-inbound.test.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import assert from 'node:assert/strict';
  2. import { mkdtempSync } from 'node:fs';
  3. import net from 'node:net';
  4. import { tmpdir } from 'node:os';
  5. import path from 'node:path';
  6. import { test } from 'node:test';
  7. import {
  8. createDomain,
  9. createInboundMailbox,
  10. createUser,
  11. createWebhook,
  12. initDatabase,
  13. listInboundMessages,
  14. listWebhookDeliveries,
  15. updateDomain
  16. } from '../src/db.js';
  17. import { sendViaSmtp } from '../src/mailer.js';
  18. import { startSubmissionServer } from '../src/submission.js';
  19. test('SMTP accepts unauthenticated inbound mail for local mailboxes', async () => {
  20. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-inbound-')), 'inbound-secret');
  21. const user = createUser({ username: 'inbound-smtp', email: 'inbound-smtp@example.com', password: 'password123' });
  22. createDomain(user.id, {
  23. domain: 'inbound.example',
  24. selector: 'mh',
  25. verificationToken: 'verify',
  26. dkimPublic: 'public',
  27. dkimPrivate: 'private',
  28. senderHost: 'mail.inbound.example',
  29. sendingIp: '192.0.2.10',
  30. spfExtra: '',
  31. dmarcPolicy: 'none',
  32. dmarcRua: ''
  33. });
  34. createInboundMailbox(user.id, { address: 'support@inbound.example', displayName: 'Support' });
  35. const [server] = startSubmissionServer({
  36. enabled: true,
  37. listeners: [{ port: 0, protocol: 'smtp' }],
  38. hostname: 'mx.inbound.example',
  39. allowInsecureAuth: true,
  40. inboundEnabled: true,
  41. relayHost: '',
  42. relayPort: 25,
  43. relaySecure: false,
  44. relayUsername: '',
  45. relayPassword: '',
  46. relayHelo: 'mx.inbound.example'
  47. });
  48. await waitForListening(server);
  49. try {
  50. const rawMessage = [
  51. 'From: Alice <alice@example.net>',
  52. 'To: Support <support@inbound.example>',
  53. 'Subject: Hello inbound SMTP',
  54. 'Message-ID: <hello-inbound@example.net>',
  55. 'Content-Type: text/plain; charset=UTF-8',
  56. '',
  57. 'Hello through SMTP.',
  58. ''
  59. ].join('\r\n');
  60. const response = await sendViaSmtp({
  61. host: '127.0.0.1',
  62. port: server.address().port,
  63. secure: false,
  64. username: '',
  65. password: '',
  66. helo: 'sender.example.net',
  67. mailFrom: 'alice@example.net',
  68. recipients: ['support@inbound.example'],
  69. rawMessage
  70. });
  71. assert.match(response.message, /Message accepted/i);
  72. const [message] = listInboundMessages(user.id);
  73. assert.equal(message.sender, 'alice@example.net');
  74. assert.deepEqual(message.recipients, ['support@inbound.example']);
  75. assert.equal(message.subject, 'Hello inbound SMTP');
  76. assert.equal(message.preview, 'Hello through SMTP.');
  77. } finally {
  78. await closeServer(server);
  79. }
  80. });
  81. test('SMTP queues a mailbox receipt webhook only after the inbound message is stored', async () => {
  82. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-inbound-webhook-')), 'inbound-secret');
  83. const user = createUser({ username: 'inbound-webhook', email: 'inbound-webhook@example.com', password: 'password123' });
  84. createDomain(user.id, {
  85. domain: 'receipt-hook.example',
  86. selector: 'mh',
  87. verificationToken: 'verify',
  88. dkimPublic: 'public',
  89. dkimPrivate: 'private',
  90. senderHost: 'mail.receipt-hook.example',
  91. sendingIp: '192.0.2.12',
  92. spfExtra: '',
  93. dmarcPolicy: 'none',
  94. dmarcRua: ''
  95. });
  96. const mailbox = createInboundMailbox(user.id, { address: 'support@receipt-hook.example' });
  97. createWebhook(user.id, {
  98. name: 'Support received',
  99. url: 'https://hooks.example.com/receipt',
  100. events: ['received'],
  101. mailboxId: mailbox.id
  102. });
  103. const [server] = startSubmissionServer({
  104. enabled: true,
  105. listeners: [{ port: 0, protocol: 'smtp' }],
  106. hostname: 'mx.receipt-hook.example',
  107. allowInsecureAuth: true,
  108. inboundEnabled: true
  109. });
  110. await waitForListening(server);
  111. try {
  112. await sendViaSmtp({
  113. host: '127.0.0.1',
  114. port: server.address().port,
  115. secure: false,
  116. username: '',
  117. password: '',
  118. helo: 'sender.example.net',
  119. mailFrom: 'sender@example.net',
  120. recipients: ['support@receipt-hook.example'],
  121. rawMessage: [
  122. 'Message-ID: <smtp-receipt-hook@example.net>',
  123. 'From: sender@example.net',
  124. 'To: support@receipt-hook.example',
  125. 'Subject: Stored before callback',
  126. '',
  127. 'Inbound body'
  128. ].join('\r\n')
  129. });
  130. const [message] = listInboundMessages(user.id);
  131. assert.ok(message);
  132. const [delivery] = listWebhookDeliveries(user.id, { eventType: 'received' });
  133. assert.ok(delivery);
  134. assert.equal(delivery.inboundMessageId, message.id);
  135. const payload = JSON.parse(delivery.payloadJson);
  136. assert.equal(payload.type, 'email.received');
  137. assert.equal(payload.data.message_id, '<smtp-receipt-hook@example.net>');
  138. assert.equal(payload.data.text, 'Inbound body');
  139. } finally {
  140. await closeServer(server);
  141. }
  142. });
  143. test('SMTP rejects unauthenticated inbound mail for unknown recipients', async () => {
  144. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-inbound-reject-')), 'inbound-secret');
  145. const [server] = startSubmissionServer({
  146. enabled: true,
  147. listeners: [{ port: 0, protocol: 'smtp' }],
  148. hostname: 'mx.inbound.example',
  149. allowInsecureAuth: true,
  150. inboundEnabled: true
  151. });
  152. await waitForListening(server);
  153. try {
  154. const transcript = await smtpTranscript(server.address().port, [
  155. 'EHLO sender.example.net',
  156. 'MAIL FROM:<alice@example.net>',
  157. 'RCPT TO:<nobody@external.example>'
  158. ]);
  159. assert.match(transcript.at(-1), /^550 /);
  160. assert.equal(listInboundMessages(1).length, 0);
  161. } finally {
  162. await closeServer(server);
  163. }
  164. });
  165. test('SMTP authenticates with a mailbox account address and password', async () => {
  166. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-mailbox-auth-')), 'inbound-secret');
  167. const user = createUser({ username: 'mailbox-auth', email: 'mailbox-auth@example.com', password: 'password123' });
  168. createDomain(user.id, {
  169. domain: 'authmail.example',
  170. selector: 'mh',
  171. verificationToken: 'verify',
  172. dkimPublic: 'public',
  173. dkimPrivate: 'private',
  174. senderHost: 'mail.authmail.example',
  175. sendingIp: '192.0.2.16',
  176. spfExtra: '',
  177. dmarcPolicy: 'none',
  178. dmarcRua: ''
  179. });
  180. createDomain(user.id, {
  181. domain: 'other-authmail.example',
  182. selector: 'mh',
  183. verificationToken: 'verify-other',
  184. dkimPublic: 'public',
  185. dkimPrivate: 'private',
  186. senderHost: 'mail.other-authmail.example',
  187. sendingIp: '192.0.2.17',
  188. spfExtra: '',
  189. dmarcPolicy: 'none',
  190. dmarcRua: ''
  191. });
  192. createInboundMailbox(user.id, {
  193. address: 'admin@authmail.example',
  194. password: 'mailbox-pass-123',
  195. aliases: ['sales']
  196. });
  197. const [server] = startSubmissionServer({
  198. enabled: true,
  199. listeners: [{ port: 0, protocol: 'smtp' }],
  200. hostname: 'mx.authmail.example',
  201. allowInsecureAuth: true,
  202. inboundEnabled: true
  203. });
  204. await waitForListening(server);
  205. try {
  206. const auth = Buffer.from('\u0000admin@authmail.example\u0000mailbox-pass-123').toString('base64');
  207. const ownSender = await smtpTranscript(server.address().port, [
  208. 'EHLO sender.example.net',
  209. `AUTH PLAIN ${auth}`,
  210. 'MAIL FROM:<admin@authmail.example>'
  211. ]);
  212. assert.match(ownSender.at(-1), /^250 /);
  213. const aliasSender = await smtpTranscript(server.address().port, [
  214. 'EHLO sender.example.net',
  215. `AUTH PLAIN ${auth}`,
  216. 'MAIL FROM:<sales@authmail.example>'
  217. ]);
  218. assert.match(aliasSender.at(-1), /^250 /);
  219. const crossMailboxSender = await smtpTranscript(server.address().port, [
  220. 'EHLO sender.example.net',
  221. `AUTH PLAIN ${auth}`,
  222. 'MAIL FROM:<other@other-authmail.example>'
  223. ]);
  224. assert.match(crossMailboxSender.at(-1), /^553 /);
  225. const crossMailboxHeader = await smtpTranscript(server.address().port, [
  226. 'EHLO sender.example.net',
  227. `AUTH PLAIN ${auth}`,
  228. 'MAIL FROM:<admin@authmail.example>',
  229. 'RCPT TO:<recipient@example.net>',
  230. 'DATA',
  231. [
  232. 'From: Other <other@other-authmail.example>',
  233. 'To: recipient@example.net',
  234. 'Subject: blocked mailbox impersonation',
  235. '',
  236. 'This message must not be relayed.',
  237. '.'
  238. ].join('\r\n')
  239. ]);
  240. assert.match(crossMailboxHeader.at(-1), /^553 /);
  241. } finally {
  242. await closeServer(server);
  243. }
  244. });
  245. test('SMTP routes unknown inbound recipients to the domain catch-all mailbox', async () => {
  246. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-catchall-')), 'inbound-secret');
  247. const user = createUser({ username: 'catchall-smtp', email: 'catchall-smtp@example.com', password: 'password123' });
  248. const domain = createDomain(user.id, {
  249. domain: 'catchall.example',
  250. selector: 'mh',
  251. verificationToken: 'verify',
  252. dkimPublic: 'public',
  253. dkimPrivate: 'private',
  254. senderHost: 'mail.catchall.example',
  255. sendingIp: '192.0.2.17',
  256. spfExtra: '',
  257. dmarcPolicy: 'none',
  258. dmarcRua: ''
  259. });
  260. createInboundMailbox(user.id, { address: 'share@catchall.example' });
  261. updateDomain(domain.id, user.id, { catchAllAddress: 'share@catchall.example' });
  262. const [server] = startSubmissionServer({
  263. enabled: true,
  264. listeners: [{ port: 0, protocol: 'smtp' }],
  265. hostname: 'mx.catchall.example',
  266. allowInsecureAuth: true,
  267. inboundEnabled: true
  268. });
  269. await waitForListening(server);
  270. try {
  271. await sendViaSmtp({
  272. host: '127.0.0.1',
  273. port: server.address().port,
  274. secure: false,
  275. username: '',
  276. password: '',
  277. helo: 'sender.example.net',
  278. mailFrom: 'alice@example.net',
  279. recipients: ['missing@catchall.example'],
  280. rawMessage: 'From: alice@example.net\r\nSubject: Catch all\r\n\r\nHello catch-all.'
  281. });
  282. const [message] = listInboundMessages(user.id);
  283. assert.equal(message.mailboxAddress, 'share@catchall.example');
  284. assert.deepEqual(message.recipients, ['missing@catchall.example']);
  285. assert.equal(message.subject, 'Catch all');
  286. } finally {
  287. await closeServer(server);
  288. }
  289. });
  290. test('SMTP forwards inbound messages without storing them when keepForwarded is false', async () => {
  291. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-forward-')), 'inbound-secret');
  292. const relay = await startFakeSmtpServer();
  293. const user = createUser({ username: 'forward-smtp', email: 'forward-smtp@example.com', password: 'password123' });
  294. createDomain(user.id, {
  295. domain: 'forward.example',
  296. selector: 'mh',
  297. verificationToken: 'verify',
  298. dkimPublic: 'public',
  299. dkimPrivate: 'private',
  300. senderHost: 'mail.forward.example',
  301. sendingIp: '192.0.2.18',
  302. spfExtra: '',
  303. dmarcPolicy: 'none',
  304. dmarcRua: ''
  305. });
  306. createInboundMailbox(user.id, {
  307. address: 'ops@forward.example',
  308. forwardTo: 'archive@example.net',
  309. keepForwarded: false
  310. });
  311. const [server] = startSubmissionServer({
  312. enabled: true,
  313. listeners: [{ port: 0, protocol: 'smtp' }],
  314. hostname: 'mx.forward.example',
  315. allowInsecureAuth: true,
  316. inboundEnabled: true,
  317. relayHost: '127.0.0.1',
  318. relayPort: relay.port,
  319. relaySecure: false,
  320. relayUsername: '',
  321. relayPassword: '',
  322. relayHelo: 'mx.forward.example'
  323. });
  324. await waitForListening(server);
  325. try {
  326. await sendViaSmtp({
  327. host: '127.0.0.1',
  328. port: server.address().port,
  329. secure: false,
  330. username: '',
  331. password: '',
  332. helo: 'sender.example.net',
  333. mailFrom: 'alice@example.net',
  334. recipients: ['ops@forward.example'],
  335. rawMessage: 'From: alice@example.net\r\nSubject: Forward only\r\n\r\nForward this.'
  336. });
  337. assert.equal(listInboundMessages(user.id).length, 0);
  338. assert.ok(relay.commands.includes('RCPT TO:<archive@example.net>'));
  339. assert.match(relay.messages[0], /Subject: Forward only/);
  340. } finally {
  341. await closeServer(server);
  342. await relay.close();
  343. }
  344. });
  345. test('SMTP stores each inbound recipient without exposing other envelope recipients', async () => {
  346. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-inbound-multi-')), 'inbound-secret');
  347. const supportUser = createUser({ username: 'support-user', email: 'support@example.com', password: 'password123' });
  348. const privateUser = createUser({ username: 'private-user', email: 'private@example.com', password: 'password123' });
  349. createDomain(supportUser.id, {
  350. domain: 'support.example',
  351. selector: 'mh',
  352. verificationToken: 'verify',
  353. dkimPublic: 'public',
  354. dkimPrivate: 'private',
  355. senderHost: 'mail.support.example',
  356. sendingIp: '192.0.2.12',
  357. spfExtra: '',
  358. dmarcPolicy: 'none',
  359. dmarcRua: ''
  360. });
  361. createDomain(privateUser.id, {
  362. domain: 'private.example',
  363. selector: 'mh',
  364. verificationToken: 'verify',
  365. dkimPublic: 'public',
  366. dkimPrivate: 'private',
  367. senderHost: 'mail.private.example',
  368. sendingIp: '192.0.2.13',
  369. spfExtra: '',
  370. dmarcPolicy: 'none',
  371. dmarcRua: ''
  372. });
  373. createInboundMailbox(supportUser.id, { address: 'support@support.example' });
  374. createInboundMailbox(privateUser.id, { address: 'private@private.example' });
  375. const [server] = startSubmissionServer({
  376. enabled: true,
  377. listeners: [{ port: 0, protocol: 'smtp' }],
  378. hostname: 'mx.inbound.example',
  379. allowInsecureAuth: true,
  380. inboundEnabled: true
  381. });
  382. await waitForListening(server);
  383. try {
  384. const rawMessage = [
  385. 'From: Alice <alice@example.net>',
  386. 'To: Support <support@support.example>',
  387. 'Subject: Multi recipient',
  388. '',
  389. 'Hello both.',
  390. ''
  391. ].join('\r\n');
  392. await sendViaSmtp({
  393. host: '127.0.0.1',
  394. port: server.address().port,
  395. secure: false,
  396. username: '',
  397. password: '',
  398. helo: 'sender.example.net',
  399. mailFrom: 'alice@example.net',
  400. recipients: ['support@support.example', 'private@private.example'],
  401. rawMessage
  402. });
  403. assert.deepEqual(listInboundMessages(supportUser.id)[0].recipients, ['support@support.example']);
  404. assert.deepEqual(listInboundMessages(privateUser.id)[0].recipients, ['private@private.example']);
  405. } finally {
  406. await closeServer(server);
  407. }
  408. });
  409. test('SMTP accepts unauthenticated inbound bounces with an empty envelope sender', async () => {
  410. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-inbound-bounce-')), 'inbound-secret');
  411. const user = createUser({ username: 'bounce-user', email: 'bounce@example.com', password: 'password123' });
  412. createDomain(user.id, {
  413. domain: 'bounce.example',
  414. selector: 'mh',
  415. verificationToken: 'verify',
  416. dkimPublic: 'public',
  417. dkimPrivate: 'private',
  418. senderHost: 'mail.bounce.example',
  419. sendingIp: '192.0.2.14',
  420. spfExtra: '',
  421. dmarcPolicy: 'none',
  422. dmarcRua: ''
  423. });
  424. createInboundMailbox(user.id, { address: 'postmaster@bounce.example' });
  425. const [server] = startSubmissionServer({
  426. enabled: true,
  427. listeners: [{ port: 0, protocol: 'smtp' }],
  428. hostname: 'mx.bounce.example',
  429. allowInsecureAuth: true,
  430. inboundEnabled: true
  431. });
  432. await waitForListening(server);
  433. try {
  434. await sendViaSmtp({
  435. host: '127.0.0.1',
  436. port: server.address().port,
  437. secure: false,
  438. username: '',
  439. password: '',
  440. helo: 'sender.example.net',
  441. mailFrom: '',
  442. recipients: ['postmaster@bounce.example'],
  443. rawMessage: 'From: MAILER-DAEMON <>\r\nSubject: Delivery status\r\n\r\nBounced.'
  444. });
  445. const [message] = listInboundMessages(user.id);
  446. assert.equal(message.sender, '');
  447. assert.deepEqual(message.recipients, ['postmaster@bounce.example']);
  448. } finally {
  449. await closeServer(server);
  450. }
  451. });
  452. test('SMTP rejects oversized unauthenticated inbound messages without storing them', async () => {
  453. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-inbound-size-')), 'inbound-secret');
  454. const user = createUser({ username: 'size-user', email: 'size@example.com', password: 'password123' });
  455. createDomain(user.id, {
  456. domain: 'size.example',
  457. selector: 'mh',
  458. verificationToken: 'verify',
  459. dkimPublic: 'public',
  460. dkimPrivate: 'private',
  461. senderHost: 'mail.size.example',
  462. sendingIp: '192.0.2.15',
  463. spfExtra: '',
  464. dmarcPolicy: 'none',
  465. dmarcRua: ''
  466. });
  467. createInboundMailbox(user.id, { address: 'support@size.example' });
  468. const [server] = startSubmissionServer({
  469. enabled: true,
  470. listeners: [{ port: 0, protocol: 'smtp' }],
  471. hostname: 'mx.size.example',
  472. allowInsecureAuth: true,
  473. inboundEnabled: true,
  474. maxMessageBytes: 64
  475. });
  476. await waitForListening(server);
  477. try {
  478. const transcript = await smtpTranscript(server.address().port, [
  479. 'EHLO sender.example.net',
  480. 'MAIL FROM:<alice@example.net>',
  481. 'RCPT TO:<support@size.example>',
  482. 'DATA',
  483. [
  484. 'Subject: Oversized inbound',
  485. '',
  486. 'This body is intentionally longer than the configured inbound message size limit.',
  487. '.'
  488. ].join('\r\n')
  489. ]);
  490. assert.match(transcript.at(-1), /^552 /);
  491. assert.equal(listInboundMessages(user.id).length, 0);
  492. } finally {
  493. await closeServer(server);
  494. }
  495. });
  496. function waitForListening(server) {
  497. if (server.listening) return Promise.resolve();
  498. return new Promise((resolve) => server.once('listening', resolve));
  499. }
  500. function closeServer(server) {
  501. return new Promise((resolve, reject) => {
  502. server.close((error) => error ? reject(error) : resolve());
  503. });
  504. }
  505. async function smtpTranscript(port, commands) {
  506. return await new Promise((resolve, reject) => {
  507. const socket = net.createConnection({ host: '127.0.0.1', port });
  508. socket.setEncoding('utf8');
  509. socket.setTimeout(3000);
  510. const responses = [];
  511. let buffer = '';
  512. let index = -1;
  513. socket.on('data', (chunk) => {
  514. buffer += chunk;
  515. let lineEnd;
  516. while ((lineEnd = buffer.indexOf('\n')) !== -1) {
  517. const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
  518. buffer = buffer.slice(lineEnd + 1);
  519. if (!/^\d{3}[ -]/.test(line)) continue;
  520. responses.push(line);
  521. if (/^\d{3} /.test(line)) {
  522. index += 1;
  523. if (index >= commands.length) {
  524. socket.end('QUIT\r\n');
  525. resolve(responses);
  526. return;
  527. }
  528. socket.write(`${commands[index]}\r\n`);
  529. }
  530. }
  531. });
  532. socket.once('error', reject);
  533. socket.once('timeout', () => reject(new Error('SMTP transcript timed out')));
  534. });
  535. }
  536. function startFakeSmtpServer() {
  537. const commands = [];
  538. const messages = [];
  539. const server = net.createServer((socket) => {
  540. socket.setEncoding('utf8');
  541. socket.write('220 relay.test ESMTP ready\r\n');
  542. let buffer = '';
  543. let dataMode = false;
  544. let messageLines = [];
  545. socket.on('data', (chunk) => {
  546. buffer += chunk;
  547. let index;
  548. while ((index = buffer.indexOf('\n')) !== -1) {
  549. const line = buffer.slice(0, index).replace(/\r$/, '');
  550. buffer = buffer.slice(index + 1);
  551. if (dataMode) {
  552. if (line === '.') {
  553. dataMode = false;
  554. messages.push(messageLines.join('\n'));
  555. messageLines = [];
  556. socket.write('250 2.0.0 queued as FORWARD123\r\n');
  557. } else {
  558. messageLines.push(line);
  559. }
  560. continue;
  561. }
  562. commands.push(line);
  563. if (line.startsWith('EHLO')) socket.write('250 relay.test\r\n');
  564. else if (line.startsWith('MAIL FROM') || line.startsWith('RCPT TO')) socket.write('250 ok\r\n');
  565. else if (line === 'DATA') {
  566. dataMode = true;
  567. socket.write('354 end with dot\r\n');
  568. } else if (line === 'QUIT') {
  569. socket.write('221 bye\r\n');
  570. socket.end();
  571. }
  572. }
  573. });
  574. });
  575. return new Promise((resolve, reject) => {
  576. server.once('error', reject);
  577. server.listen(0, '127.0.0.1', () => resolve({
  578. port: server.address().port,
  579. commands,
  580. messages,
  581. close: () => closeServer(server)
  582. }));
  583. });
  584. }