all-in-one smtp+imap with minimal setup
git clone http://git.nthia.dev/nthmail
const net = require('net')
const dns = require('dns')
const { Readable, Writable, pipeline } = require('stream')
const TLS = require('./starttls.js')
const split = require('./split.js')
module.exports = function (opts, onfinish) {
let send = new Send(opts, (err, sent) => {
let f = onfinish
onfinish = noop
f(err, sent)
})
let cb = send._onfinish
dns.resolveMx(send.destination, (err, mx) => {
mx.sort((a,b) => a.priority < b.priority ? -1 : +1)
;(function next(i) {
if (i >= mx.length) return cb(new Error('exhausted list of mx records'))
let socket = net.connect(opts.port ?? 25, mx[i].exchange)
socket.once('connect', onconnect)
socket.once('error', onerror)
send.once('quit', () => {
clear()
socket.destroy()
})
pipeline(socket, send, socket, (err) => { if (err) cb(err, false) })
function clear() {
socket.removeListener('error', onerror)
socket.removeListener('connect', onconnect)
}
function onconnect() { clear() }
function onerror(err) {
clear()
next(i+1)
}
})(0)
})
}
function Send(opts, onfinish) {
if (!(this instanceof Send)) return new Send(opts, onfinish)
let self = this
TLS.call(this, Object.assign({ isServer: false }, opts))
this._onfinish = onfinish || noop
this._data = opts.data
if (!opts) opts = {}
this.from = opts.from
this.to = opts.to
this.source = opts.domain ?? String(this.from).replace(/^[^@]*@/,'')
this.destination = String(this.to).replace(/^[^@]*@/,'')
this._split = split((line,next) => {
this._writeLine(line, next)
})
this._lines = []
this._code = -1
this._maxLines = 1024
this._maxLineBytes = this._maxLines*256
this._lineBytes = 0
this._cap = null
let sequence = [
(next) => this._ehlo(next),
(next) => this._upgrade(next),
(next) => this._ehlo(next),
(next) => this._sendFromTo(next),
(next) => this._sendData((err,sent,code) => {
if (err) return next(err)
this._onfinish(null, sent, code)
next()
}),
(next) => this._sendQuit(next),
(next) => { this.destroy(); next() },
]
this._cb = [
(err,code,msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return this._onfinish(new Error('non-2xx ehlo response: ' + code + ' ' + msg))
}
start(0)
}
]
function start(i) {
if (i >= sequence.length) return
sequence[i](err => {
if (err) self._onfinish(err)
else start(i+1)
})
}
}
Send.prototype = Object.create(TLS.prototype)
Send.prototype._upgrade = function (next) {
this._send('STARTTLS\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return next(new Error('non-2xx starttls response: ' + code + ' ' + msg))
}
this._startTLS()
next()
})
}
Send.prototype._ehlo = function (next) {
this._send('EHLO ' + this.source + '\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return next(new Error('non-2xx ehlo response: ' + code + ' ' + msg))
}
this._cap = msg.split(/\r?\n/)
next()
})
}
Send.prototype._sendFromTo = function (next) {
this._send('MAIL FROM: <' + this.from + '>\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return next(new Error('non-2xx mail from response: ' + code + ' ' + msg))
}
this._send('RCPT TO: <' + this.to + '>\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return next(new Error('non-2xx rcpt-to response: ' + code + ' ' + msg))
}
next()
})
})
}
Send.prototype._sendData = function (next) {
let self = this
let sent = false
this._send('DATA\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 300 || code >= 400) {
return next(new Error('non-3xx data start response: ' + code + ' ' + msg))
}
let w = new Writable({
write(buf, enc, cb) {
self._push(buf)
cb()
},
final(cb) {
self._send('\r\n.\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return next(new Error('non-2xx data end response: ' + code + ' ' + msg))
}
sent = true
next(null, true, code)
})
},
})
pipeline(this._data, w, (err) => {
if (err) next(err)
else if (!sent) next(null, false, -1)
})
})
}
Send.prototype._sendQuit = function (next) {
this._send('QUIT\r\n', (err, code, msg) => {
if (err) return next(err)
if (code < 200 || code >= 300) {
return next(new Error('non-2xx data end response: ' + code + ' ' + msg))
}
this._end()
next(null)
this.emit('quit')
})
}
Send.prototype._writeBuf = function (buf, enc, cb) {
this._split(buf, cb)
}
Send.prototype._writeLine = function (buf, next) {
const line = buf.toString()
let m = null
if (m = /^(\d+)-(.*)/.exec(line)) {
this._code = Number(m[1])
this._lines.push(m[2])
this._lineBytes += Buffer.byteLength(m[2])
if (this._lines.length > this._maxLines) {
this._lines = []
this._lineBytes = 0
this._code = -1
let cb = this._cb.shift() || noop
cb(new Error('too many lines in reply'))
}
} else if (m = /^(\d+)\s(.*)/.exec(line)) {
this._code = Number(m[1])
this._lines.push(m[2])
this._lineBytes += Buffer.byteLength(m[2])
let msg = this._lines.join('\r\n')
let code = this._code
this._lines = []
this._lineBytes = 0
this._code = -1
let cb = this._cb.shift() || noop
cb(null, code, msg)
} else {
cb(new Error('no code in response line: ' + line))
}
next()
}
Send.prototype._send = function (msg, cb) {
this._cb.push(cb)
this._push(msg)
}
function noop() {}