Last active
March 18, 2021 13:07
-
-
Save MikeRatcliffe/1fd09c303c996be5ea0f4d3536da8c6b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/** | |
* Very simple node script to validate an email address against its SMTP server. | |
* | |
* Usage: | |
* node validate.js [email protected] | |
* | |
* This script uses the following process to validate an email address: | |
* 1. Get the domain of the email | |
* 2. Grab the DNS MX records for that domain | |
* 3. Create a TCP connection to the smtp server | |
* 4. Send a EHLO message | |
* 5. Send a MAIL FROM message | |
* 6. Send a RCPT TO message | |
* 7. Return an object indicating whether the email passed SMTP validation: | |
* { | |
* valid: true, // true: email is valid, false: email is | |
* // invalid. | |
* err: "error message", // If an error is encountered then this will | |
* // contain the error string. If there is no | |
* // error then this value is null. | |
* } | |
* | |
* @author Mike Ratcliffe | |
*/ | |
const dns = require("dns"); | |
const net = require("net"); | |
if (process.argv.length < 3) { | |
console.log( | |
"Very simple node script to validate an email address against its SMTP server.\n\nUsage:\n node validate.js [email protected]" | |
); | |
return; | |
} | |
const dnsPromises = dns.promises; | |
const email = process.argv.pop(); | |
(async () => { | |
const start = new Date(); | |
await validate(email, 3000, (valid, err) => { | |
console.log(`valid`, valid); | |
console.log(`err`, err); | |
console.log(`Run time: ${new Date() - start}ms`); | |
}); | |
})(); | |
/** | |
* | |
* @param {String} email | |
* email to validate. | |
* @param {Number} timeout | |
* timeout in ms. | |
* @param {Function} callback | |
* Callback function e.g. myFunc(valid, err) {} | |
*/ | |
async function validate(email, timeout, callback) { | |
const domain = email.split("@")[1]; | |
const [{ exchange }] = await dnsPromises.resolveMx(domain); | |
let receivedData = false; | |
let timedOut = false; | |
const socket = net.createConnection(25, exchange); | |
socket.setEncoding("ascii"); | |
socket.setTimeout(timeout); | |
socket.on("error", (error) => { | |
console.log("error", error); | |
socket.emit("fail", error); | |
}); | |
socket.on("close", (hadError) => { | |
if (!receivedData && !hadError && !timedOut) { | |
callback( | |
false, | |
"Mail server closed connection without sending any data." | |
); | |
} | |
}); | |
socket.on("fail", (msg) => { | |
if (socket.writable && !socket.destroyed) { | |
socket.write(`quit\r\n`); | |
socket.end(); | |
socket.destroy(); | |
} | |
callback(false, msg); | |
}); | |
socket.on("success", () => { | |
if (socket.writable && !socket.destroyed) { | |
socket.write(`quit\r\n`); | |
socket.end(); | |
socket.destroy(); | |
} | |
callback(true, null); | |
}); | |
const commands = [ | |
`helo ${exchange}\r\n`, | |
`mail from: <${email}>\r\n`, | |
`rcpt to: <${email}>\r\n`, | |
]; | |
let i = 0; | |
socket.on("next", () => { | |
if (i < 3) { | |
if (socket.writable) { | |
socket.write(commands[i++]); | |
} else { | |
socket.emit("fail", "SMTP communication unexpectedly closed."); | |
} | |
} else { | |
socket.emit("success"); | |
} | |
}); | |
socket.on("timeout", () => { | |
timedOut = true; | |
socket.emit("fail", "Timeout"); | |
}); | |
socket.on("connect", () => { | |
socket.on("data", (msg) => { | |
receivedData = true; | |
if (hasCode(msg, 220) || hasCode(msg, 250)) { | |
socket.emit("next", msg); | |
} else if (hasCode(msg, 550)) { | |
socket.emit("fail", "Mailbox not found."); | |
} else { | |
// Possible error codes | |
const ErrorCodes = { | |
211: "SMTP Error: A system status or help reply.", | |
214: "SMTP Error: Help Message.", | |
220: "SMTP Error: The server is ready.", | |
221: "SMTP Error: The server is ending the conversation.", | |
250: "SMTP Error: The requested action was completed.", | |
251: "SMTP Error: The specified user is not local, but the server will forward the mail message.", | |
354: 'SMTP Error: This is a reply to the DATA command. After getting this, start sending the body of the mail message, ending with "\r\n.\r\n."', | |
421: "SMTP Error: The mail server will be shut down. Save the mail message and try again later.", | |
450: "SMTP Error: The mailbox that you are trying to reach is busy. Wait a little while and try again.", | |
451: "SMTP Error: The requested action was not done. Some error occurmiles in the mail server.", | |
452: "SMTP Error: The requested action was not done. The mail server ran out of system storage.", | |
500: "SMTP Error: The last command contained a syntax error or the command line was too long.", | |
501: "SMTP Error: The parameters or arguments in the last command contained a syntax error.", | |
502: "SMTP Error: The mail server has not implemented the last command.", | |
503: "SMTP Error: The last command was sent out of sequence. For example, you might have sent DATA before sending RECV.", | |
504: "SMTP Error: One of the parameters of the last command has not been implemented by the server.", | |
550: "SMTP Error: The mailbox that you are trying to reach can't be found or you don't have access rights.", | |
551: "SMTP Error: The specified user is not local; part of the text of the message will contain a forwarding address.", | |
552: "SMTP Error: The mailbox that you are trying to reach has run out of space. Store the message and try again tomorrow or in a few days-after the user gets a chance to delete some messages.", | |
553: "SMTP Error: The mail address that you specified was not syntactically correct.", | |
554: "SMTP Error: The mail transaction has failed for an unknown reason.", | |
}; | |
const [code] = Object.keys(ErrorCodes).filter((x) => hasCode(msg, x)); | |
socket.emit("fail", ErrorCodes[code] || "Unrecognized SMTP response."); | |
} | |
}); | |
}); | |
function hasCode(message, code) { | |
return ( | |
message.indexOf(`${code}`) === 0 || message.indexOf(`${code}\n`) > -1 | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment