OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-social / bridge-email / lib / smtp-client.js

112 lines · javascript ref:
import tls from 'tls'
import net from 'net'

class SmtpClient {
  constructor (opts = {}) {
    this.host = opts.host
    this.port = opts.port || 465
    this.user = opts.user
    this.pass = opts.pass
    this.from = opts.from || opts.user
    this.secure = opts.secure !== false // TLS by default
    this._socket = null
  }

  connect () {
    return new Promise((resolve, reject) => {
      const onConnect = () => {
        this._readLine().then(() => resolve()).catch(reject)
      }

      if (this.secure) {
        this._socket = tls.connect({
          host: this.host,
          port: this.port,
          rejectUnauthorized: true
        }, onConnect)
      } else {
        this._socket = net.connect(this.port, this.host, onConnect)
      }

      this._socket.setEncoding('utf8')
      this._socket.on('error', reject)
      this._socket.setTimeout(15000, () => reject(new Error('SMTP connection timeout')))
    })
  }

  _readLine () {
    return new Promise((resolve, reject) => {
      let buf = ''
      const onData = (chunk) => {
        buf += chunk
        if (buf.includes('\r\n')) {
          this._socket.removeListener('data', onData)
          const code = parseInt(buf.slice(0, 3), 10)
          if (code >= 400) reject(new Error(`SMTP ${code}: ${buf.trim()}`))
          else resolve(buf.trim())
        }
      }
      this._socket.on('data', onData)

      setTimeout(() => {
        this._socket.removeListener('data', onData)
        reject(new Error('SMTP read timeout'))
      }, 10000)
    })
  }

  async _send (cmd) {
    this._socket.write(cmd + '\r\n')
    return this._readLine()
  }

  async ehlo () {
    return this._send('EHLO bridge-email')
  }

  async auth () {
    await this._send('AUTH LOGIN')
    await this._send(Buffer.from(this.user).toString('base64'))
    return this._send(Buffer.from(this.pass).toString('base64'))
  }

  async sendMail (to, subject, body, opts = {}) {
    await this._send(`MAIL FROM:<${this.from}>`)
    await this._send(`RCPT TO:<${to}>`)
    await this._send('DATA')

    const inReplyTo = opts.inReplyTo || ''
    const references = opts.references || ''

    let headers = `From: ${this.from}\r\n`
    headers += `To: ${to}\r\n`
    headers += `Subject: ${subject}\r\n`
    headers += `Date: ${new Date().toUTCString()}\r\n`
    headers += `Content-Type: text/plain; charset=utf-8\r\n`
    headers += `Content-Transfer-Encoding: 8bit\r\n`
    headers += `X-Bridge: bridge-email\r\n`

    if (inReplyTo) headers += `In-Reply-To: ${inReplyTo}\r\n`
    if (references) headers += `References: ${references}\r\n`

    const message = headers + '\r\n' + body + '\r\n.'
    return this._send(message)
  }

  async quit () {
    try {
      await this._send('QUIT')
    } catch {}
    this.close()
  }

  close () {
    if (this._socket) {
      this._socket.destroy()
      this._socket = null
    }
  }
}

export { SmtpClient }