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

const { TLSSocket, createSecureContext } = require('tls')
const { Readable, Duplex } = require('stream')

module.exports = TLS

function TLS(opts) {
  Duplex.call(this)
  if (!opts) opts = {}
  this._tlsClear = null
  this._tlsCipher = null
  this._tlsNext = null
  this._tlsBuffer = null
  this._tlsRead = false
  this._streamNext = null
  this._streamBuffer = null
  this._streamRead = false
  this._streamFlush = false
  this._pair = opts.pair
  this._tls = Object.assign({}, opts.tls ?? {})
  if (opts.key) this._tls.key = opts.key
  if (opts.cert) this._tls.cert = opts.cert
  if (opts.ca) this._tls.ca = opts.ca
  this._isServer = true
  if (opts.isServer !== undefined) this._isServer = opts.isServer
}
TLS.prototype = Object.create(Duplex.prototype)

TLS.prototype._read = function (n) {
  if (this._streamNext) {
    var buf = this._streamBuffer
    var f = this._streamNext
    this._streamBuffer = null
    this._streamNext = null
    this.push(buf)
    if (this._streamFlush) this.push(null)
    f()
  } else {
    this._streamRead = true
  }
}

TLS.prototype._write = function (buf, enc, next) {
  if (this._tlsCipher) {
    if (this._tlsRead) {
      this._tlsRead = false
      this._tlsCipher.push(buf)
      next()
    } else {
      this._tlsBuffer = buf
      this._tlsNext = next
    }
  } else this._writeBuf(buf, enc, next)
}

TLS.prototype._destroy = function (err, next) {
  if (this._tlsClear) this._tlsClear.destroy()
}

TLS.prototype._startTLS = function () {
  var self = this
  self._tlsCipher = new Duplex({
    read(n) {
      if (self._tlsNext) {
        this.push(self._tlsBuffer)
        var f = self._tlsNext
        self._tlsNext = null
        self._tlsBuffer = null
        f()
      } else {
        self._tlsRead = true
      }
    },
    write(buf, enc, next) {
      if (self._streamRead) {
        self._streamRead = false
        self.push(buf)
        next()
      } else {
        self._streamNext = next
        self._streamBuffer = buf
      }
    },
    flush(next) {
      if (self._streamNext) {
        self._streamFlush = true
      } else {
        self.push(null)
        next()
      }
    },
  })
  var secureContext = createSecureContext(self._tls)
  self._tlsClear = new TLSSocket(self._tlsCipher, {
    pair: this._pair,
    isServer: self._isServer,
    requestCert: true,
    rejectUnauthorized: false,
    secureContext,
  })
  self._tlsClear.on('secure', () => {
    self.emit('secure', self._tlsClear)
    let cert = self._tlsClear.getPeerCertificate()
    if (cert) self.emit('certificate', cert)
  })
  self._tlsClear.on('readable', () => {
    ;(function next(err) {
      if (err) return
      var buf = self._tlsClear.read()
      if (buf === null) return
      self._writeBuf(buf, null, next)
    })()
  })
}

TLS.prototype._push = function (buf) {
  if (this._tlsClear && buf === null) {
    this._tlsClear.end()
  } else if (this._tlsClear) {
    this._tlsClear.write(buf)
  } else {
    this.push(buf)
  }
}

TLS.prototype._end = function () {
  if (this._tlsClear) {
    this._tlsClear.end()
  } else {
    this.push(null)
  }
}