SMTP Ping

SMTP Ping or Email Adress Box Pinging is the process of establishing a connection and exchanging messages with a mail server in order to check if an email address exists or not.

The messages are exchanged using the SMTP protocol and the idea is to "trick the server" by starting a transaction to send an email but never completing it(the email is never sent).

Pinging an email address could be useful if you need to avoid email bouncing for example.

Note that pinging an email address is not reliable and can generate many false positive results due to the fact that most mail servers have security policies blocking connection attempts from non authorized clients.

The source code presented here can be found on github.

SMTP Protocol

The Simple Mail Transfer Protocol(SMTP) is a text based communication protocol that allows a client(mail sender) to communicate with a server(mail server) issuing command strings and supplying data to send an email. Tipically the process is done over a TCP connection on port 25 or 587.

The basic commands used to perform a SMTP Ping are the following:

Command Description
HELO The HELO command is used to start the communication with the server, we could say that this is the client greeting the server. This command also expects an argument that specifies the domain name or IP address of the SMTP client.
MAIL FROM The MAIL FROM command initiates a mail transfer. The command expects an argument that specifies the email address of the sender.
RCPT TO The command RCPT TO indicates the mail recipient. The command expects an argument that specifies the email address of the recipient.
QUIT The QUIT command requests the session to be terminated.

For each command that the client issues, the server will reply with a text message containing a numeric status code followed by a description.

DNS

In order to find a mail exchanger or mail server domain we need to perform a DNS lookup against the domain's authoritative name server. Specifically, mail server domains are specified in MX records.

MX Records

A mail exchager record(MX record) specifies the domain or the domains of the mail server responsible for handling email messages.

The record is composed by a list of mail server domains containing a number that indicates the priority of each mail server. The lowest number indicates the highest priority.

10 smtp1.mail-server.com.
20 smtp2.mail-server.com.

Pinging

To validate the SMTP Ping flow you can start a session with a smtp server and issue the necessary commands in order to validade the existence of an email address.

First, we need to find the mail server by performing a MX lookup using nslookup over the terminal:

nslookup -q=mx google.com
Server:        127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
google.com    mail exchanger = 10 smtp.google.com.

After finding the mail server, you can start a session using telnet:

telnet smtp.google.com 25

Trying 2a00:1450:4013:c14::1a...
Connected to smtp.google.com.
Escape character is '^]'.
220 mx.google.com ESMTP gs24-20020a170906f19800b0072f90babec9si4469504ejb.231 - gsmtp

HELO gmail.com
250 mx.google.com at your service

MAIL FROM:<test@gmail.com>
250 2.1.0 OK gs24-20020a170906f19800b0072f90babec9si4469504ejb.231 - gsmtp

RCPT TO:<test@gmail.com>
550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1  https://support.google.com/mail/?p=NoSuchUser gs24-20020a170906f19800b0072f90babec9si4469504ejb.231 - gsmtp

QUIT
221 2.0.0 closing connection gs24-20020a170906f19800b0072f90babec9si4469504ejb.231 - gsmtp
Connection closed by foreign host.

SMTP Ping with JavaScript

Let's start by installing the required dependencies:

npm install --save promise-socket

Defining settings and constants:

const { resolveMx } = require('dns').promises;
const { PromiseSocket, TimeoutError } = require('promise-socket');

const DEFAULT_SETTINGS = { port: 25, timeout: 3000 };
const MAIL_PROVIDERS = ['gmail.com', 'yahoo.com', 'aol.com', 'outlook.com'];
const SmtpPingStatus = { OK: 'OK', INVALID: 'INVALID', UNKNOWN: 'UNKNOWN' };
const SmtpStatusCode = { READY: 220, OK: 250, MAILBOX_UNAVAILABLE: 550 };

We need some utility functions to:

// -- utils --
const after = (str, char) => str.substring(str.lastIndexOf(char) + 1);
const strip = str => str.replace(/\n|\r/g, '').trim();
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
const randomAlpha = size => Math.random().toString(36).slice(size - 1);
const pickRandom = items => items[randomInt(0, items.length - 1)];
const randomEmail = () => `${randomAlpha(7)}@${pickRandom(MAIL_PROVIDERS)}`;

We need some functions to find the mail exchanger server:

// -- dns --
const getHost = ({ exchange }) => exchange;
const byLowestPriority = (previous, current) => previous.priority < current.priority ? previous : current;
const findRecordWithLowestPriority = records => records.reduce(byLowestPriority);
const findMailExchangerHost = async fqdn => getHost(findRecordWithLowestPriority(await resolveMx(fqdn)));

Let's go ahead and define a builder function to create an object based on the smtp response and another builder function to create an object based on the Error that may occur during the communication with the server.

const buildSmtpResponse = (command, response) => Object.freeze({
  command: command ? strip(command) : command,
  response: response ? strip(response) : response,
  code: response ? parseInt(response.substring(0, 3)) : response
});

const mapError = error => Object.freeze({
  complete: !(error instanceof TimeoutError),
  status: error instanceof TimeoutError ? SmtpPingStatus.UNKNOWN : SmtpPingStatus.INVALID,
  error: error
});

Let's now create the SMTP pipeline which consists of a sequence of commands that need to be sent and the responses that need to be parsed and verified.

const executePipeline = async(pipeline, args) => {
  if (pipeline.length === 0) return;
  const first = pipeline[0];
  const next  = async() => await executePipeline(pipeline.slice(1), args);
  return await first({ ...args, ...{ next } });
};

const smtpPipeline = [

  async({ socket, commandHistory, next }) => {
    const data = await socket.read();
    const response = buildSmtpResponse(null, data.toString());
    commandHistory.push(response);
    return SmtpStatusCode.READY === response.code
      ? await next()
      : { complete: false, status: SmtpPingStatus.UNKNOWN };
  },

  async({ socket, commandHistory, sender, next }) => {
    const command = `HELO ${after(sender, '@')}\r\n`;
    await socket.write(command);
    const data = await socket.read();
    const response = buildSmtpResponse(command, data.toString());
    commandHistory.push(response);
    return SmtpStatusCode.OK === response.code
      ? await next()
      : { complete: false, status: SmtpPingStatus.UNKNOWN };
  },

  async({ socket, commandHistory, sender, next }) => {
    const command = `MAIL FROM:<${sender}>\r\n`;
    await socket.write(command);
    const data = await socket.read();
    const response = buildSmtpResponse(command, data.toString());
    commandHistory.push(response);
    return SmtpStatusCode.OK === response.code
      ? await next()
      : { complete: false, status: SmtpPingStatus.UNKNOWN };
  },

  async({ socket, commandHistory, recipient }) => {
    const command = `RCPT TO:<${recipient}>\r\n`;
    await socket.write(command);
    const data = await socket.read();
    const response = buildSmtpResponse(command, data.toString());
    commandHistory.push(response);
    return {
      complete: true,
      status: { 
        [SmtpStatusCode.OK]:                  SmtpPingStatus.OK, 
        [SmtpStatusCode.MAILBOX_UNAVAILABLE]: SmtpPingStatus.INVALID  
      } [response.code] || SmtpPingStatus.UNKNOWN   
    };
  }

];

Finally, we will create the ping function to orchestrate the SMTP ping flow:

async function ping(recipient, config) {
  const settings       = { ...DEFAULT_SETTINGS, ...config || {} };
  const fqdn           = after(recipient, '@');
  const port           = settings.port;
  const timeout        = settings.timeout;
  const sender         = settings.sender || randomEmail();
  const commandHistory = [];

  let socket, host, complete, status, error;

  try {

    host = await findMailExchangerHost(fqdn);

    socket = new PromiseSocket();
    socket.setTimeout(timeout);
    await socket.connect({ host, port });

    ({ complete, status } = await executePipeline(smtpPipeline, { socket, sender, recipient, commandHistory }));

  } catch (e) {
    ({ complete, status, error } = mapError(e));

  } finally {
    if (socket) socket.destroy();
  }

  return Object.freeze({
    complete,
    status,
    sender,
    recipient,
    fqdn,
    host,
    port,
    timeout,
    error,
    commandHistory
  });
}

module.exports = { ping, SmtpPingStatus };

You can then perform a SMTP Ping to validate an email address:

ping('any@gmail.com')
  .then(result => console.log(result))
  .catch(error => console.error(error));

This project is already available on npm and you can also check it on github.