nthmail / cmd.js
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() {}