How to test SMTP connection
Open a socket, read the 220 banner, send EHLO and read the capability list. node talk.js 127.0.0.1:2526 "EHLO tester.local" QUIT prints both sides of that exchange. A working server answers 220 at once and returns a multi-line 250 reply naming PIPELINING, STARTTLS and SIZE.
Why check this
Run this after any change that moves the mail path: a new relay, a new firewall rule, a new provider. Run it again on staging sign-off before a release that sends mail. The failure it prevents: a firewall permits the TCP handshake but a filtering device never passes the banner through, so every send waits for the client timeout and password reset mail arrives minutes late, with nothing in the log except a generic send error.
Three unrelated failures produce that same log line. Nothing is listening. Something is listening and it is not a mail server. A mail server is answering and refusing the sender. Each one belongs to a different team.
Prerequisites
- RFC 5321 section 4.1.1 for the command set, and section 4.2 for the reply codes.
- Two free ports, confirmed free before you start:
netstat -ano | grep -E ":(2526|2528) "
- A self-signed certificate. In Git Bash prefix this with
MSYS_NO_PATHCONV=1, or the shell rewrites/CN=into a path:
openssl req -x509 -newkey rsa:2048 -nodes -keyout smtp.key -out smtp.crt -days 30 -subj "/CN=mx.h2check.test" -addext "subjectAltName=DNS:mx.h2check.test,DNS:localhost,IP:127.0.0.1"
smtpd.js, the server under test, afternpm install smtp-server. It accepts every recipient, writes each message tocapture/, and delivers nothing:
// Catch-all SMTP server for tests. Accepts every recipient, stores every message,
// delivers nothing. Listens on the loopback address only.
const fs = require('fs');
const { SMTPServer } = require('smtp-server');
const server = new SMTPServer({
name: 'mx.h2check.test',
disableReverseLookup: true,
key: fs.readFileSync('smtp.key'),
cert: fs.readFileSync('smtp.crt'),
authOptional: true,
size: 10 * 1024 * 1024,
onData(stream, session, callback) {
const chunks = [];
stream.on('data', (c) => chunks.push(c));
stream.on('end', () => {
const raw = Buffer.concat(chunks);
const file = 'capture/' + Date.now() + '.eml';
fs.writeFileSync(file, raw);
console.log('stored ' + file + ' ' + raw.length + ' bytes return-path=' +
session.envelope.mailFrom.address + ' rcpt=' +
session.envelope.rcptTo.map((r) => r.address).join(','));
callback();
});
},
});
server.listen(2526, '127.0.0.1', () =>
console.log('smtpd listening on 127.0.0.1:2526, pid ' + process.pid));
talk.js, the client. It prints both directions and ends every command with CR LF:
// Runs one SMTP conversation over a plain socket and prints both sides.
// After a 354 reply the lines are message data, so no reply is waited for
// until the single dot.
// node talk.js 127.0.0.1:2526 "EHLO tester.local" QUIT
const net = require('net');
const [host, port] = (process.argv[2] || '127.0.0.1:2526').split(':');
const commands = process.argv.slice(3);
const sock = net.createConnection({ host, port: Number(port) });
sock.setEncoding('utf8');
sock.setTimeout(5000, () => { console.log('!! no reply for 5000 ms'); sock.destroy(); });
let buf = '';
let n = 0;
let inData = false;
function next() {
if (n >= commands.length) return sock.end();
const c = commands[n++];
console.log('C: ' + c);
sock.write(c + '\r\n');
if (inData) { if (c === '.') inData = false; else return next(); }
}
sock.on('connect', () => console.log('-- tcp connected to ' + host + ':' + port));
sock.on('data', (d) => {
buf += d;
let i;
while ((i = buf.indexOf('\r\n')) !== -1) {
const line = buf.slice(0, i); buf = buf.slice(i + 2);
console.log('S: ' + line);
if (/^354 /.test(line)) inData = true;
if (/^\d{3} /.test(line)) next();
}
});
sock.on('error', (e) => console.log('!! ' + e.code + ': ' + e.message));
sock.on('close', () => console.log('-- connection closed'));
silent.js, which accepts the connection and sends nothing. It stands in for the filtering device:
// A TCP listener that accepts the connection and sends no banner.
const net = require('net');
net.createServer(() => {}).listen(2528, '127.0.0.1', function () {
console.log('silent listener on 127.0.0.1:2528, pid ' + process.pid);
});
To use your own staging relay, replace 127.0.0.1:2526 with its host and port.
Steps
- Step 1.
Start the server and the silent listener, and keep the process ids.
node smtpd.js & node silent.js &smtpd listening on 127.0.0.1:2526, pid 18448 silent listener on 127.0.0.1:2528, pid 20512Each process prints its own operating system process id. Write both down: on Windows, stopping a background job by its shell job number reports success and leaves the port held.
- Step 2.
Ask the server what it can do.
node talk.js 127.0.0.1:2526 "EHLO tester.local" QUIT-- tcp connected to 127.0.0.1:2526 S: 220 mx.h2check.test ESMTP C: EHLO tester.local S: 250-mx.h2check.test Nice to meet you, [127.0.0.1] S: 250-PIPELINING S: 250-8BITMIME S: 250-SMTPUTF8 S: 250-AUTH LOGIN PLAIN S: 250-STARTTLS S: 250 SIZE 10485760 C: QUIT S: 221 Bye -- connection closedThe
220line proves a mail server, not only an open port. The greeting names the address the server saw the client arrive from. Every250-line is a capability, and the last line ends the reply with a space instead of a hyphen.AUTH LOGIN PLAINappears on a session with no encryption, which is worth a ticket of its own. - Step 3.
Send the older greeting and compare.
node talk.js 127.0.0.1:2526 "HELO tester.local" QUIT-- tcp connected to 127.0.0.1:2526 S: 220 mx.h2check.test ESMTP C: HELO tester.local S: 250 mx.h2check.test Nice to meet you, [127.0.0.1] C: QUIT S: 221 Bye -- connection closedOne line, no capabilities. A client that falls back to
HELOlosesSTARTTLS,SIZEandAUTHat once, and the server records a successful session either way. - Step 4.
Point the same command at the silent listener.
node talk.js 127.0.0.1:2528 "EHLO tester.local" QUIT-- tcp connected to 127.0.0.1:2528 !! no reply for 5000 ms -- connection closedThe TCP connection succeeded and no banner arrived. A port scanner and a load balancer health check call this port healthy. The mail client calls it a timeout.
- Step 5.
Point it at a port where nothing listens.
node talk.js 127.0.0.1:2530 "EHLO tester.local" QUIT!! ECONNREFUSED: connect ECONNREFUSED 127.0.0.1:2530 -- connection closedThere is no
-- tcp connectedline. A refusal arrives at once, a dropped packet never arrives, so a firewall that drops rather than rejects looks like step 4. - Step 6.
Run the same greeting inside TLS.
printf 'EHLO tester.local\r\nQUIT\r\n' | openssl s_client -starttls smtp -connect 127.0.0.1:2526 -briefCan't use SSL_get_servername depth=0 CN = mx.h2check.test verify error:num=18:self-signed certificate CONNECTION ESTABLISHED Protocol version: TLSv1.3 Ciphersuite: TLS_AES_256_GCM_SHA384 Peer certificate: CN = mx.h2check.test Hash used: SHA256 Signature type: RSA-PSS Verification error: self-signed certificate Server Temp Key: X25519, 253 bits 250 SIZE 10485760 250-mx.h2check.test Nice to meet you, [127.0.0.1] 250-PIPELINING 250-8BITMIME 250-SMTPUTF8 250-AUTH LOGIN PLAIN 250 SIZE 10485760 221 ByeThe lone
250 SIZEline is the tail of theEHLOopenssl sent beforeSTARTTLS. The block under it answers theEHLOsent inside TLS, andSTARTTLSis gone. The capability list is a property of the session, not of the server. - Step 7.
Run a whole transaction.
node talk.js 127.0.0.1:2526 "EHLO tester.local" "MAIL FROM:<ci@build.test>" "RCPT TO:<qa@build.test>" DATA "Subject: smoke" "" "one line of body" "." QUIT-- tcp connected to 127.0.0.1:2526 S: 220 mx.h2check.test ESMTP C: EHLO tester.local S: 250-mx.h2check.test Nice to meet you, [127.0.0.1] … S: 250 SIZE 10485760 C: MAIL FROM:<ci@build.test> S: 250 Accepted C: RCPT TO:<qa@build.test> S: 250 Accepted C: DATA S: 354 End data with <CR><LF>.<CR><LF> C: Subject: smoke C: C: one line of body C: . S: 250 OK: message queued C: QUIT S: 221 Bye -- connection closedMAIL FROMandRCPT TOcarry the envelope. TheSubject:line insideDATAis message text, and nothing routes on it. The server log records the pair it stored:stored capture/1789161012491.eml 36 bytes return-path=ci@build.test rcpt=qa@build.test - Step 8.
Stop both processes by their own process ids.
powershell -Command "Stop-Process -Id 18448 -Force; Stop-Process -Id 20512 -Force"$ netstat -ano | grep LISTENING | grep -E ":(2526|2528) " $No output from
netstatmeans no listener. A test MTA left running is how the next person tests yesterday's build.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| ECONNREFUSED, no tcp connected line | Nothing is listening on that port | Check the port number and that the service is running. |
| tcp connected and then no banner | The port is open and the service behind it is not SMTP, or a device is holding the banner | Look for a firewall in transparent mode or a health-check listener. |
| Neither line, the command hangs | Packets are dropped rather than refused | A network ACL or a security group, not the mail server. |
| 220 followed by 250- lines | ESMTP is working | Read the capability list and continue. |
| A single 250 after EHLO | The server answered without capabilities | An older or restricted server. Expect no STARTTLS and no SIZE. |
| 250-AUTH on a cleartext session | Credentials can be sent unencrypted | Require encryption before authentication, see Test SMTP with STARTTLS. |
| 421 on connect | The server is shedding load or has rate limited you | Back off. Do not retry in a loop. |
| 530 or 550 after MAIL FROM | The connection is fine and policy refuses the sender | Read the enhanced status code that follows the reply code. |
Thresholds
Common mistakes
What to check next
- Test SMTP with STARTTLS: whether that session was encrypted, and what happens when the capability is removed.
- How to test email sending in a test environment: the same server as a catch-all, so test mail cannot reach a person.
- How to check mx records: the names a sender resolves before opening these connections.
- How to check MTA-STS: the policy that forbids a fall back to an unencrypted session.
- Email testing checklist: where this sits in a release that touches mail.
FAQ
How to test an SMTP connection from the command line?
Run openssl s_client -starttls smtp -connect host:port: it opens an encrypted session and prints the certificate. For a cleartext session a short socket script beats telnet, because it sends CR LF.
How do I check that an SMTP server is working?
A 220 banner and a multi-line 250 reply to EHLO prove the service answers. They do not prove it will accept your mail. Run MAIL FROM and RCPT TO as in step 7, where policy is applied.
What does the multi-line 250 reply mean?
Each line is one ESMTP capability. A hyphen after the code continues the reply, a space ends it. A parser that stops at the first 250 misses the rest.
Is it safe to run these commands against a public mail server?
No. An unsolicited SMTP session against a server you do not own is indistinguishable from a probe and can get your address blocked. Use your own relay, or the server above.
Verified
Verified by Maks Vernynode 22.23.2openssl 3.1.1smtp-server 3.19.11
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate10 minpublished updated Maks Verny