all-in-one smtp+imap with minimal setup
git clone http://git.nthia.dev/nthmail
#!/usr/bin/env node
const parseArgs = require('./lib/args')
let [args,argv] = parseArgs(process.argv.slice(2), {
boolean: ['certbot']
})
const alloc = require('./lib/alloc.js')
let ports = {
smtp: { clear: [], ssl: [] },
imap: { clear: [], ssl: [] },
}
let fd = {
smtp: { clear: [], ssl: [] },
imap: { clear: [], ssl: [] },
}
{
let defaults = {
smtp: ['25','587','2525'],
imap: ['143','+993'],
}
;['smtp','imap'].forEach(type => {
let list = (argv.get(type+'-ports') ?? [])
.concat(argv.get(type+'-port') ?? [])
.concat(argv.get(type.slice(0,1)+'p') ?? [])
.flatMap(x => x.split(','))
if (list.length === 0) list = defaults[type]
list.forEach(p => {
let port = p.replace(/^\+/,'')
if (/^\+/.test(p)) {
ports[type].ssl.push(port)
fd[type].ssl.push({ fd: alloc(Number(port)) })
} else {
ports[type].clear.push(port)
fd[type].clear.push({ fd: alloc(Number(port)) })
}
})
})
}
let ug = (argv.get('user') ?? []).concat(argv.get('u') ?? [])
if (ug.length > 0) {
let [user,group] = ug[0].split(':')
if (group === undefined) group = user
process.setgid(group)
process.setuid(user)
}
if (ports.smtp.clear + ports.smtp.ssl + ports.imap.clear + ports.imap.ssl === 0) {
throw new Error('no ports bound for smtp or imap')
}
const fs = require('fs')
const path = require('path')
const NthMail = require('./index.js')
let config = new Map
let cs = [].concat(argv.get('config') ?? []).concat(argv.get('c') ?? [])
cs.filter(Boolean).forEach(c => {
let m = /^([^=\[]+)(?:\[(\d+)\])?=(.*)$/.exec(c)
if (m && m[2]) {
let xs = config.get(m[1]) ?? []
if (m[2] === '') xs.push(m[3])
else xs[Number(m[2])] = m[3]
config.set(m[1], xs)
} else if (m) {
config.set(m[1], m[2])
} else {
config = new Map([...config, ...Object.entries(JSON.parse(fs.readFileSync(c, 'utf8')))])
}
})
let tls = {}
;(function () {
let files = {}
if (argv.get('certdir') && argv.get('certdir').length > 0) {
Object.assign(files, certFiles(argv.get('certdir')[0]))
} else if (config.get('certdir')) {
Object.assign(files, certFiles(config.get('certdir')))
}
;['key','cert','ca'].forEach(key => {
let v = argv.get(key)
if (v && v.length > 0) files.key = v[0]
})
loadFiles(files)
})()
function certFiles(cd) {
let files = {}
if (fs.existsSync(path.join(cd,'privkey.pem'))) files.key = path.join(cd,'privkey.pem')
else if (fs.existsSync(path.join(cd,'key.pem'))) files.key = path.join(cd,'key.pem')
if (fs.existsSync(path.join(cd,'cert.pem'))) files.cert = path.join(cd,'cert.pem')
if (fs.existsSync(path.join(cd,'fullchain.pem'))) files.cert = path.join(cd,'fullchain.pem')
else if (fs.existsSync(path.join(cd,'ca.pem'))) files.cert = path.join(cd,'ca.pem')
return files
}
function loadFiles(files, cb) {
if (!cb) cb = noop
let pending = 1
;['key','cert','ca'].forEach(key => {
if (!files[key]) return
fs.readFile(files[key], (err,src) => {
if (err) {
let f = cb
cb = noop
f(err)
return console.error(err)
}
tls[key] = src
if (--pending === 0) cb()
})
})
if (--pending === 0) cb()
}
let nthmail = new NthMail({
accounts: config.get('accounts') ?? [],
logins: config.get('logins') ?? [],
domains: (argv.get('domain') ?? []).concat(argv.get('d') ?? []),
createServer: require('net').createServer,
})
nthmail.listen(fd, (err) => {
if (err) return console.error(err)
if (ports.smtp.clear.length + ports.smtp.ssl.length > 0) {
let ls = ports.smtp.clear.concat(ports.smtp.ssl.map(p => '+'+p))
console.error('smtp listening on: ' + ls.join(' '))
}
if (ports.imap.clear.length + ports.imap.ssl.length > 0) {
let ls = ports.imap.clear.concat(ports.imap.ssl.map(p => '+'+p))
console.error('imap listening on: ' + ls.join(' '))
}
})
function noop() {}