mail-access.test.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. createInboundMessage,
  11. createUser,
  12. initDatabase,
  13. listInboundMessages
  14. } from '../src/db.js';
  15. import { startMailboxAccessServers } from '../src/mail-access.js';
  16. test('IMAP clients can log in and fetch mailbox messages', async () => {
  17. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-imap-test-')), 'mail-access-secret');
  18. const { user, mailbox } = createMailboxFixture('imap.example', 'imap-user');
  19. createInboundMessage(mailbox, {
  20. sender: 'alice@example.net',
  21. recipients: ['admin@imap.example'],
  22. subject: 'IMAP hello',
  23. messageId: '<imap-hello@example.net>',
  24. rawMessage: [
  25. 'From: Alice <alice@example.net>',
  26. 'To: admin@imap.example',
  27. 'Subject: IMAP hello',
  28. 'Message-ID: <imap-hello@example.net>',
  29. '',
  30. 'Hello through IMAP.'
  31. ].join('\r\n'),
  32. textBody: 'Hello through IMAP.'
  33. });
  34. const [server] = startMailboxAccessServers({
  35. hostname: 'mail.imap.example',
  36. imapEnabled: true,
  37. imapListeners: [{ port: 0, protocol: 'imap' }],
  38. pop3Enabled: false,
  39. pop3Listeners: [],
  40. allowInsecureAuth: true
  41. });
  42. await waitForListening(server);
  43. try {
  44. const port = server.address().port;
  45. const client = await connectClient(port);
  46. await client.readUntil(/\* OK .* IMAP ready\r\n/);
  47. assert.match(await client.command('A1 LOGIN "admin@imap.example" "mailbox-pass-123"', /A1 OK/), /LOGIN completed/);
  48. const selected = await client.command('A2 SELECT INBOX', /A2 OK/);
  49. assert.match(selected, /\* 1 EXISTS/);
  50. const fetched = await client.command('A3 UID FETCH 1:* (UID FLAGS RFC822.SIZE BODY.PEEK[])', /A3 OK/);
  51. assert.match(fetched, /\* 1 FETCH/);
  52. assert.match(fetched, /UID 1/);
  53. assert.match(fetched, /Subject: IMAP hello/);
  54. assert.match(fetched, /Hello through IMAP\./);
  55. await client.command('A4 LOGOUT', /A4 OK/);
  56. client.close();
  57. assert.equal(listInboundMessages(user.id).length, 1);
  58. } finally {
  59. await closeServer(server);
  60. }
  61. });
  62. test('POP3 clients can retrieve and delete messages on quit', async () => {
  63. initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-pop3-test-')), 'mail-access-secret');
  64. const { user, mailbox } = createMailboxFixture('pop3.example', 'pop3-user');
  65. createInboundMessage(mailbox, {
  66. sender: 'bob@example.net',
  67. recipients: ['admin@pop3.example'],
  68. subject: 'POP3 hello',
  69. messageId: '<pop3-hello@example.net>',
  70. rawMessage: [
  71. 'From: Bob <bob@example.net>',
  72. 'To: admin@pop3.example',
  73. 'Subject: POP3 hello',
  74. 'Message-ID: <pop3-hello@example.net>',
  75. '',
  76. 'Hello through POP3.'
  77. ].join('\r\n'),
  78. textBody: 'Hello through POP3.'
  79. });
  80. const [server] = startMailboxAccessServers({
  81. hostname: 'mail.pop3.example',
  82. imapEnabled: false,
  83. imapListeners: [],
  84. pop3Enabled: true,
  85. pop3Listeners: [{ port: 0, protocol: 'pop3' }],
  86. allowInsecureAuth: true
  87. });
  88. await waitForListening(server);
  89. try {
  90. const client = await connectClient(server.address().port);
  91. await client.readUntil(/\+OK .* POP3 ready\r\n/);
  92. assert.match(await client.command('USER admin@pop3.example', /\+OK/), /User accepted/);
  93. assert.match(await client.command('PASS mailbox-pass-123', /\+OK/), /ready/);
  94. assert.match(await client.command('STAT', /\+OK \d+ \d+/), /\+OK 1 /);
  95. assert.match(await client.command('UIDL 1', /\+OK 1 mh-1/), /\+OK 1 mh-1/);
  96. const retrieved = await client.command('RETR 1', /\r\n\.\r\n/);
  97. assert.match(retrieved, /Subject: POP3 hello/);
  98. assert.match(retrieved, /Hello through POP3\./);
  99. assert.match(await client.command('DELE 1', /\+OK/), /deleted/);
  100. await client.command('QUIT', /\+OK Bye/);
  101. client.close();
  102. assert.equal(listInboundMessages(user.id).length, 0);
  103. } finally {
  104. await closeServer(server);
  105. }
  106. });
  107. function createMailboxFixture(domainName, username) {
  108. const user = createUser({ username, email: `${username}@example.com`, password: 'password123' });
  109. createDomain(user.id, {
  110. domain: domainName,
  111. selector: 'mh',
  112. verificationToken: 'verify',
  113. dkimPublic: 'public',
  114. dkimPrivate: 'private',
  115. senderHost: `mail.${domainName}`,
  116. sendingIp: '192.0.2.30',
  117. spfExtra: '',
  118. dmarcPolicy: 'none',
  119. dmarcRua: ''
  120. });
  121. const mailbox = createInboundMailbox(user.id, {
  122. address: `admin@${domainName}`,
  123. password: 'mailbox-pass-123'
  124. });
  125. return { user, mailbox };
  126. }
  127. function connectClient(port) {
  128. return new Promise((resolve, reject) => {
  129. const socket = net.createConnection({ host: '127.0.0.1', port });
  130. socket.setEncoding('utf8');
  131. socket.setTimeout(5000);
  132. let buffer = '';
  133. const waiters = [];
  134. socket.on('data', (chunk) => {
  135. buffer += chunk;
  136. for (const waiter of [...waiters]) {
  137. if (waiter.pattern.test(buffer)) {
  138. waiters.splice(waiters.indexOf(waiter), 1);
  139. const output = buffer;
  140. buffer = '';
  141. waiter.resolve(output);
  142. }
  143. }
  144. });
  145. socket.once('connect', () => resolve({
  146. command(command, pattern) {
  147. socket.write(`${command}\r\n`);
  148. return this.readUntil(pattern);
  149. },
  150. readUntil(pattern) {
  151. if (pattern.test(buffer)) {
  152. const output = buffer;
  153. buffer = '';
  154. return Promise.resolve(output);
  155. }
  156. return new Promise((waitResolve, waitReject) => {
  157. waiters.push({ pattern, resolve: waitResolve, reject: waitReject });
  158. });
  159. },
  160. close() {
  161. socket.destroy();
  162. }
  163. }));
  164. socket.once('error', reject);
  165. socket.once('timeout', () => reject(new Error('Mail access client timed out')));
  166. });
  167. }
  168. function waitForListening(server) {
  169. if (server.listening) return Promise.resolve();
  170. return new Promise((resolve) => server.once('listening', resolve));
  171. }
  172. function closeServer(server) {
  173. return new Promise((resolve, reject) => {
  174. server.close((error) => error ? reject(error) : resolve());
  175. });
  176. }