OverviewFilesCommitsIssuesDocsReleasesPeers

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

143 lines · javascript ref:
import { ImapClient } from './imap-client.js'
import { SmtpClient } from './smtp-client.js'
import { KeetClient } from './keet-client.js'
import { Router } from './router.js'
import { emailToEnvelope, keetToEmail, emailToKeetText } from './adapter.js'

class EmailBridge {
  constructor (opts = {}) {
    this.imap = new ImapClient({
      host: opts.imapHost,
      port: opts.imapPort,
      user: opts.user,
      pass: opts.pass
    })
    this.smtp = new SmtpClient({
      host: opts.smtpHost,
      port: opts.smtpPort,
      user: opts.user,
      pass: opts.pass,
      from: opts.from || opts.user
    })
    this.keet = new KeetClient({
      host: opts.keetHost,
      port: opts.keetPort
    })
    this.roomId = opts.roomId
    this.forwardTo = opts.forwardTo // email address to relay Keet messages to
    this.pollInterval = opts.pollInterval || 30000 // 30s
    this.bridgeIdentity = opts.bridgeIdentity || null
    this.onLog = opts.onLog || (() => {})

    this._polling = false
    this._pollTimer = null

    this.router = new Router({
      onKeetMessage: (author, text) => {
        this.keet.send(this.roomId, text).catch(err => {
          this.onLog(`Keet send error: ${err.message}`)
        })
      },
      onExternalMessage: (author, text) => {
        if (!this.forwardTo) return
        this._sendEmail(author, text).catch(err => {
          this.onLog(`Email send error: ${err.message}`)
        })
      }
    })
  }

  async _sendEmail (author, text) {
    const smtp = new SmtpClient({
      host: this.smtp.host,
      port: this.smtp.port,
      user: this.smtp.user,
      pass: this.smtp.pass,
      from: this.smtp.from
    })

    await smtp.connect()
    await smtp.ehlo()
    await smtp.auth()
    await smtp.sendMail(
      this.forwardTo,
      `[Keet] Message from ${author}`,
      `${author} wrote:\n\n${text}\n\n---\nRelayed via bridge-email from Keet P2P chat`
    )
    await smtp.quit()
  }

  async start () {
    // Get bridge identity
    if (!this.bridgeIdentity) {
      try {
        const identity = await this.keet.identity()
        this.bridgeIdentity = identity.memberId || identity.id || ''
        this.onLog(`Bridge identity: ${this.bridgeIdentity}`)
      } catch (err) {
        this.onLog(`Warning: Could not get identity: ${err.message}`)
      }
    }

    // Connect IMAP
    await this.imap.connect()
    await this.imap.login()
    await this.imap.select('INBOX')
    this.onLog(`IMAP: connected to ${this.imap.host}`)

    // Start polling IMAP for new messages
    this._polling = true
    this._poll()

    // Watch Keet → Email
    this.keet.watch(this.roomId, (msg) => {
      const result = keetToEmail(msg, this.bridgeIdentity)
      if (!result) return

      this.router.routeToExternal(result.author, result.text)
    })

    this.onLog(`Bridge started: Email (${this.imap.host}) ↔ Keet`)
  }

  async _poll () {
    if (!this._polling) return

    try {
      const unseen = await this.imap.searchUnseen()

      for (const id of unseen) {
        try {
          const email = await this.imap.fetch(id)
          const envelope = emailToEnvelope(email)

          if (envelope) {
            const text = emailToKeetText(envelope)
            this.router.routeToKeet(envelope.author, text)
            await this.imap.markSeen(id)
          }
        } catch (err) {
          this.onLog(`Fetch error for message ${id}: ${err.message}`)
        }
      }
    } catch (err) {
      this.onLog(`Poll error: ${err.message}`)
    }

    this._pollTimer = setTimeout(() => this._poll(), this.pollInterval)
  }

  stop () {
    this._polling = false
    if (this._pollTimer) {
      clearTimeout(this._pollTimer)
      this._pollTimer = null
    }
    this.keet.stopWatch()
    this.imap.close()
    this.onLog('Bridge stopped')
  }
}

export { EmailBridge }