nthmail / lib / login.js
all-in-one smtp+imap with minimal setup
git clone http://git.nthia.dev/nthmail

const { scrypt } = require('crypto')
const { nextTick } = process

module.exports = Login

function Login(opts) {
  if (!(this instanceof Login)) return new Login(opts)
  this._accounts = new Map
  this._names = new Map
  ;(opts.accounts ?? []).forEach(a => {
    this._accounts.set(a.id, a)
    ;(a.names ?? []).forEach(name => {
      this._names.set(name, a)
    })
  })
  this._logins = (opts.logins ?? []).map(l => {
    l = Object.assign({}, l)
    if (l.fingerprint512 !== undefined) l.fingerprint512 = normfp(l.fingerprint512)
    if (typeof l.salt === 'string') l.salt = Buffer.from(l.salt,'hex')
    return l
  })
  this._domains = new Set
  ;(opts.domains ?? []).forEach(d => this._domains.add(d))
}

Login.prototype.checkSmtp = function (src, dst, accountId) {
  let srcName = src.replace(/@.*/,'')
  let srcDomain = src.replace(/^[^@]*@/,'')
  let dstName = dst.replace(/@.*/,'')
  let dstDomain = dst.replace(/^[^@]*@/,'')
  if (this._domains.has(srcDomain)) {
    if (accountId === undefined || accountId === null) return false
    let a = this._accounts[accountId]
    if (!a) return false
    if (!Array.isArray(a.names)) return false
    return a.names.includes(srcName)
  }
  return this._domains.has(dstDomain)
}

Login.prototype.checkLogin = function (opts, cb) {
  let self = this
  let fp512 = opts.fingerprint512 !== undefined ? normfp(opts.fingerprint512) : null
  ;(function next(i) {
    if (i >= self._logins.length) return cb(null, null)
    let l = self._logins[i]
    if (opts.type === 'plain' && l.type === 'scrypt') {
      if (opts.password === undefined) return next(i+1)
      if (l.username !== opts.username) return next(i+1)
      if (l.key === undefined) return next(i+1)
      if (l.fingerprint512 !== undefined && fp512 !== l.fingerprint512) return next(i+1)
      scrypt(opts.password, l.salt, 64, (err,key) => {
        if (err) {
          let f = cb
          cb = noop
          return f(err)
        }
        if (key.toString('hex') === l.key) {
          cb(null, l.account)
        } else {
          next(i+1)
        }
      })
    } else if (opts.type === 'fingerprint' && l.type === 'fingerprint') {
      if (fp512 !== undefined && fp512 === l.fingerprint512) {
        cb(null, l.account)
      } else {
        next(i+1)
      }
    } else {
      next(i+1)
    }
  })(0)
}

Login.prototype.getAccountFromEmail = function (email) {
  let name = email.replace(/@.*/,'')
  let domain = email.replace(/^[^@]*@/,'')
  if (!this._domains.has(domain)) return null
  return this.getAccountFromName(name)
}

Login.prototype.getAccountFromName = function (name) {
  return this._names.get(name) ?? null
}

Login.prototype.getAccountFromId = function (id) {
  return this._accounts.get(id)
}

Login.prototype.hasDomain = function (domain) {
  return this._domains.has(domain)
}

function normfp(x) { return String(x).toLowerCase().replace(/:/g,'') }
function noop() {}