all-in-one smtp+imap with minimal setup
git clone http://git.nthia.dev/nthmail
const fs = require('fs')
const path = require('path')
const { randomBytes } = require('crypto')
const { pipeline, Writable, Readable, Transform } = require('stream')
const { nextTick } = process
const head = require('./head.js')
const body = require('./body.js')
const parseFetchParts = require('./parse-fetch-parts.js')
module.exports = Inbox
function Inbox(opts) {
if (!(this instanceof Inbox)) return new Inbox(opts)
if (typeof opts.maildir !== 'function') {
throw new Error('opts.maildir must be a function')
}
this._maildir = opts.maildir
this._nextuid = new Map
}
Inbox.prototype.write = function (id) {
let md = this._maildir(id)
if (!md) return null
let ws = null, wbuf = null, wenc = null, wnext = null, wfinal = null
let w = new Writable({
write(buf, enc, next) {
if (ws) {
ws.write(buf)
next()
} else {
wbuf = buf
wenc = enc
wnext = next
}
},
final(next) {
if (ws) {
ws.end()
} else {
wfinal = next
}
next()
},
})
this._getNextUID(id, (err,uid) => {
if (err) return w.emit('error', err)
let file = new Date().toISOString().replace(/:/,'h').replace(/:/,'m').replace(/\.\w+$/,'s')
+ '-' + String(uid) + '-' + randomBytes(3).toString('hex')
w.file = path.join(md,file)
w.emit('file', w.file)
fs.mkdir(path.dirname(w.file), { recursive: true }, err => {
if (err) return w.emit('error', err)
ws = fs.createWriteStream(w.file)
if (wnext !== null) {
let buf = wbuf, enc = wenc, next = wnext
wbuf = null
wenc = null
wnext = null
ws.write(buf)
next()
}
if (wfinal !== null) {
ws.end()
wfinal = null
}
})
})
return w
}
Inbox.prototype._getNextUID = function (id, cb) {
let uid = this._nextuid.get(id)
if (uid) return nextTick(cb, null, uid)
let uidFile = this.path(id,'_nextuid')
if (!uidFile) return null
fs.stat(uidFile, (err, s) => {
if (!s) return cb(null, 0)
fs.readFile(uidFile, 'utf8', (err, src) => {
if (err) return cb(err)
let uid = Number(src.trim())
this._nextuid.set(id, uid)
cb(null, uid)
})
})
}
Inbox.prototype.rawWrite = function (id, file, buf, offset, len, pos, cb) {
if (!cb) cb = noop
let wfile = this.path(id,file)
if (!wfile) return null
fs.open(wfile, (e0,fd) => {
if (e0) fs.close(fd, e1 => cb(e0))
else fs.write(fd, buf, offset, len, pos, e1 => {
fs.close(fd, e2 => {
if (e1) cb(e1)
else if (e2) cb(e2)
else cb()
})
})
})
}
Inbox.prototype.path = function (id, file) {
let md = this._maildir(id)
if (!md) return null
return path.join(md,file)
}
Inbox.prototype.read = function (id, file) {
return fs.createReadStream(this.path(id,file))
}
Inbox.prototype.head = function (id, file, cb) {
return pipeline(this.read(id, file), head(f), err => { if (err) cb(err) })
function f(err, buf) {
let g = cb
cb = noop
g(err, buf)
}
}
Inbox.prototype.body = function (id, file, cb) {
if (!cb) cb = noop
return pipeline(this.read(id, file), body(), cb)
}
Inbox.prototype.list = function (id, cb) {
var dir = this._maildir(id)
if (!dir) return cb(null, [])
fs.readdir(dir, (err,files) => {
if (err) return cb(err)
cb(null, files
.map(x => /^(\d{4})-(\d{2})-(\d{2})T(\d{2})h(\d{2})m(\d{2})s-(\d+)-([0-9a-f]{6})$/.exec(x))
.filter(x => x)
.map(x => ({
file: x[0],
time: `${x[1]}-${x[2]}-${x[3]} ${x[4]}:${x[5]}:${x[6]}`,
uid: x[7],
})))
})
}
Inbox.prototype.fetch = function (id, range, query) {
let self = this
let parts = Array.isArray(query) ? query : parseFetchParts(query)
let files = null, index = Number(range[0])
let stream = new Readable({
objectMode: true,
read(n) {
if (!files) {
self.list(id, (err, lfiles) => {
if (err) return this.emit('error', err)
files = lfiles
next(index)
index++
})
} else {
next(index)
index++
}
},
})
return stream
function next(i) {
let f = files[i-1]
if (i > Number(range[1]) || !f) return stream.push(null)
// todo: calculate size and stream in better way
let buffers = []
pipeline(
self.body(id, f.file),
new Writable({
write(buf, enc, next) {
buffers.push(buf)
next()
},
}),
err => {
if (err) return stream.emit('error', err)
let buf = Buffer.concat(buffers)
stream.push(Object.assign({}, f, { data: buf }))
}
)
}
}
Inbox.prototype.remove = function (id, file, cb) {
let d = this.path(id, file)
if (!d) return null
fs.unlink(d, cb)
}
function noop() {}