all-in-one smtp+imap with minimal setup
git clone http://git.nthia.dev/nthmail
const TLS = require('./starttls.js')
const split = require('./split.js')
const { pipeline, Writable } = require('stream')
const parseFetchParts = require('./parse-fetch-parts.js')
module.exports = IMAP
function IMAP(opts) {
if (!(this instanceof IMAP)) return new IMAP(opts)
TLS.call(this, opts)
this._authenticated = false
this._inbox = opts.inbox
this._split = split((line,next) => {
this._writeLine(line, next)
})
this._id = null
this._fingerprint512 = null
this._getAccountFromUsername = opts.getAccountFromUsername
this._push('* OK [CAPABILITY IMAP4rev2 STARTTLS LOGINDISABLED]\r\n')
this.on('certificate', cert => {
this._fingerprint512 = cert.fingerprint512
})
}
IMAP.prototype = Object.create(TLS.prototype)
IMAP.prototype._writeBuf = function (buf, enc, cb) {
this._split(buf, cb)
}
IMAP.prototype._writeLine = function (buf, next) {
let self = this
const line = buf.toString()
console.log('LINE:',line)
let m = /^(\S+)(?:\s+(.*))?\s*$/.exec(line)
if (!m) {
this._push('invalid syntax\r\n')
return next()
}
let tag = m[1]
let parts = String(m[2] ?? '')
.match(/[^"\s]\S*|"[^"]+"/g)
.map(x => x.replace(/^"(.*)"$/g,'$1'))
let cmd = parts[0].toLowerCase()
let args = parts.slice(1)
if (cmd === 'capability') {
let caps = [ 'STARTTLS' ]
if (this._tlsClear) {
caps.push('AUTH=PLAIN', 'AUTH=LOGIN')
} else {
caps.push('AUTH=PLAIN', 'AUTH=LOGIN')
//caps.push('LOGINDISABLED')
}
this._push(
`* CAPABILITY IMAP4rev2 ${caps.join(' ')}\r\n`
+ `${tag} OK CAPABILITY completed\r\n`
)
next()
} else if (cmd === 'starttls') {
if (this._tlsClear) {
this._push(`${tag} BAD already using tls\r\n`)
return next()
}
this._push(`${tag} OK STARTTLS completed\r\n`)
this._startTLS()
next()
} else if (cmd === 'noop') {
this._push(`${tag} OK NOOP completed\r\n`)
next()
} else if (cmd === 'authenticate') {
// ...
} else if (cmd === 'login') {
let login = {
type: 'plain',
username: args[0],
password: args[1],
}
if (this._fingerprint512) login.fingerprint512 = this._fingerprint512
this._ackEmit('login', tag, login, (err, mode, msg, id) => {
if (mode === 'accept' && id !== undefined && id !== null) {
this._id = id
}
next()
})
} else if (cmd === 'logout') {
this._id = null
this._push(
'* BYE IMAP4rev2 server logging out\r\n'
+ `${tag} OK LOGOUT completed\r\n`
)
this._end()
next()
} else if (cmd === 'list' && this._id === null) {
this._authRequired(tag, 'list', next)
} else if (cmd === 'list') {
this._push(
'* LIST () "/" inbox\r\n'
+ `${tag} OK LIST completed\r\n`
)
next()
} else if (cmd === 'examine' && this._id === null) {
this._authRequired(tag, 'examine', next)
} else if (cmd === 'examine') {
this._inbox.list(this._id, (err,list) => {
if (err) return this._push(`${tag} BAD ${err.message ?? err}\r\n`)
this._push(
`* ${list.length} EXISTS\r\n`
+ `${tag} OK EXAMINE completed\r\n`
)
next()
})
} else if (cmd === 'fetch' && this._id === null) {
this._authRequired(tag, 'fetch', next)
} else if (cmd === 'fetch') {
let q = args.slice(1).join(' ').replace(/^\(|\)$/,'')
let parts = parseFetchParts(q)
pipeline(
this._inbox.fetch(this._id, args[0], q),
new Writable({
objectMode: true,
write(row, enc, cb) {
// todo: backpressure
let response = [`* ${row.uid} FETCH (`]
for (let i = 0; i < parts.length; i++) {
let p = parts[i]
console.log(JSON.stringify(p))
if (p.name === 'UID') {
response.push(`UID (${row.uid})`)
} else if (p.name === 'FLAGS') {
response.push(`FLAGS ()`)
} else if (p.name === 'INTERNALDATE') {
response.push(`INTERNALDATE "${row.time}"`)
} else if (p.name === 'RFC822.SIZE') {
response.push(`RFC822.SIZE ${row.data.length}`)
} else if (p.name === 'BODY.PEEK') {
response.push(`BODY.PEEK[${
p.sections.map(s => s.name + ' (' + s.fields.join(' ') + ')')
}] (\r\n${row.data}\r\n)`)
}
}
self._push(response.join(' ') + '\r\n)\r\n')
cb()
},
final(cb) {
self._push(`${tag} OK FETCH completed\r\n`)
cb()
},
}),
next
)
} else {
this._push('* BAD command unrecognized\r\n')
next()
}
}
IMAP.prototype._authRequired = function (tag, cmd, next) {
this._push(`${tag} NO ${cmd.toUpperCase()} authorization required\r\n`)
next()
}
IMAP.prototype._ackEmit = function (evName, tag, obj, cb) {
let self = this
let n = self.listeners(evName).length + 1
let acceptMsg = null, acceptObj = null, rejected = false
self.emit(evName, obj, {
accept: (msg, aobj) => {
acceptMsg = msg
acceptObj = aobj
//console.log('ACCEPT',msg)
if (--n === 0) finish()
},
reject: (msg, aobj) => {
if (rejected) return
rejected = true
//console.log('REJECT',msg)
self._push(`${tag} NO${msg === undefined ? '' : ' ' + msg}\r\n`)
if (typeof cb === 'function') cb(null, 'reject', msg, aobj)
},
error: (msg, aobj) => {
if (rejected) return
rejected = true
//console.log('ERROR',msg)
self._push(`${tag} BAD${msg === undefined ? '' : ' ' + msg}\r\n`)
if (typeof cb === 'function') cb(null, 'error', msg, aobj)
},
})
if (--n === 0) return finish()
function finish() {
if (rejected) return
//console.log(`PUSH ${tag} OK${acceptMsg === undefined ? '' : ' ' + acceptMsg}\r\n`)
self._push(`${tag} OK${acceptMsg === undefined ? '' : ' ' + acceptMsg}\r\n`)
if (typeof cb === 'function') cb(null, 'accept', acceptMsg, acceptObj)
}
}
IMAP.prototype.preauth = function (user) {
this._push(`* PREAUTH IMAP4rev2 server logged in as ${user}\r\n`)
}
function noop() {}