From 3f93b59a2e896497f79ea4e28176b6702f7ce047 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 3 Oct 2026 02:18:39 +0200 Subject: [PATCH] fix(smtp): skip the reverse DNS lookup of connecting clients (#104) smtp-server reverse resolves every connecting client before it sends the 220 greeting, unless disableReverseLookup is set. MailDev never reads the resolved hostname, and without a network the lookup does not answer, so each connection waited for smtp-server to give up on it. --- .changeset/fix-smtp-reverse-lookup.md | 8 +++ .../smtp/src/__tests__/reverse-lookup.test.ts | 49 +++++++++++++++++++ packages/smtp/src/server.ts | 3 ++ 3 files changed, 60 insertions(+) create mode 100644 .changeset/fix-smtp-reverse-lookup.md create mode 100644 packages/smtp/src/__tests__/reverse-lookup.test.ts diff --git a/.changeset/fix-smtp-reverse-lookup.md b/.changeset/fix-smtp-reverse-lookup.md new file mode 100644 index 0000000..8044524 --- /dev/null +++ b/.changeset/fix-smtp-reverse-lookup.md @@ -0,0 +1,8 @@ +--- +'@maildev/smtp': patch +'maildev': patch +--- + +Skip the reverse DNS lookup of connecting SMTP clients (#104). The resolved +hostname was never used, and without a network the lookup held back the +greeting on every connection until it timed out. diff --git a/packages/smtp/src/__tests__/reverse-lookup.test.ts b/packages/smtp/src/__tests__/reverse-lookup.test.ts new file mode 100644 index 0000000..68ad649 --- /dev/null +++ b/packages/smtp/src/__tests__/reverse-lookup.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import dns from 'node:dns' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import nodemailer from 'nodemailer' +import { MemoryStorage } from '@maildev/core' +import { SMTPServer } from '../server.js' + +describe('client reverse DNS lookup', () => { + let mailDir: string + let storage: MemoryStorage + let server: SMTPServer + + beforeEach(async () => { + mailDir = await mkdtemp(join(tmpdir(), 'maildev-reverse-')) + storage = new MemoryStorage() + await storage.initialize() + server = new SMTPServer({ storage, mailDir, port: 0, host: '127.0.0.1' }) + await server.start() + }) + + afterEach(async () => { + vi.restoreAllMocks() + await server.stop() + await rm(mailDir, { recursive: true, force: true }) + }) + + // Without a network the lookup never answers, and smtp-server holds back the + // 220 greeting until it gives up on it (#104). + it('accepts mail without reverse resolving the connecting client', async () => { + const reverse = vi.spyOn(dns, 'reverse').mockImplementation(() => {}) + + const transport = nodemailer.createTransport({ + host: '127.0.0.1', + port: server.getPort(), + secure: false, + }) + await transport.sendMail({ + from: 'a@example.com', + to: 'b@example.com', + subject: 'offline', + text: 'hello', + }) + + expect(await storage.getAll()).toHaveLength(1) + expect(reverse).not.toHaveBeenCalled() + }) +}) diff --git a/packages/smtp/src/server.ts b/packages/smtp/src/server.ts index c933860..0db1a3c 100644 --- a/packages/smtp/src/server.ts +++ b/packages/smtp/src/server.ts @@ -524,6 +524,9 @@ export class SMTPServer extends EventEmitter { logger: false, hideSTARTTLS: true, disabledCommands: ['AUTH'] as string[], + // The client hostname is never used, and without a network the lookup + // stalls every connection before the greeting (#104). + disableReverseLookup: true, } // Advertise and enforce a maximum message size. smtp-server rejects