import { ImapFlow } from 'imapflow'; import EventEmitter from 'node:events'; import 'dotenv/config'; import { simpleParser } from 'mailparser'; // pinned to v2.0.1 due to https://github.com/jhurliman/node-rate-limiter/issues/80 import * as $limiter from 'limiter'; const { RateLimiter } = $limiter if (!process.env.SCOUT_IMAP_SERVER) throw new Error('SCOUT_IMAP_SERVER is missing from env'); if (!process.env.SCOUT_IMAP_PORT) throw new Error('SCOUT_IMAP_PORT is missing from env'); if (!process.env.SCOUT_IMAP_USERNAME) throw new Error('SCOUT_IMAP_USERNAME is missing from env'); if (!process.env.SCOUT_IMAP_PASSWORD) throw new Error('SCOUT_IMAP_PASSWORD is missing from env'); const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 3000 }); // https://stackoverflow.com/a/49428486/1004931 function streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); stream.on('error', (err) => reject(err)); stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); }) } export class Email extends EventEmitter { constructor() { super() this.client = null } async archiveMessage(uid) { await limiter.removeTokens(1); await this.client.messageDelete(uid, { uid: true }) } async connect() { this.client = new ImapFlow({ host: process.env.SCOUT_IMAP_SERVER, port: process.env.SCOUT_IMAP_PORT, secure: true, auth: { user: process.env.SCOUT_IMAP_USERNAME, pass: process.env.SCOUT_IMAP_PASSWORD } }); this.registerEventListeners() await this.client.connect() const stat = await this.getStatus() if (stat.messages > 0) { await this.emitAllMessages() } } async reconnect() { console.log(' RECONNECTING...') delete this.client await this.connect() } async getStatus() { let lock = await this.client.getMailboxLock('INBOX'); let status; try { status = await this.client.status('INBOX', { messages: true }); } finally { lock.release() } return status } async loadMessage(uid) { console.log(` 💾 loading message uid=${uid}`) let lock = await this.client.getMailboxLock('INBOX'); let dl, body try { dl = await this.client.download(uid, undefined, { uid: true }) body = await streamToString(dl.content) } finally { lock.release() } return body } async emitAllMessages() { console.log('emitAllMessages is running') let lock = await this.client.getMailboxLock('INBOX'); try { for await (let message of this.client.fetch('1:*', { envelope: true })) { // it is tempting to call this.client.download here, but that is not possible while the mailbox is locked. // client.download must be called outside of this lock // console.log('here is a message') console.log(JSON.stringify(message, null, 2)) this.emit('message', message) } } finally { lock.release(); } } registerEventListeners() { console.log(` > REGISTERING EVENT LISTENERS <`) this.client.once('end', () => this.reconnect()) this.client.on('exists', (evt) => { // console.log(`exists event! count=${evt.count} prevCount=${evt.prevCount}`) // console.log(evt) if (evt.path === 'INBOX') { this.emitAllMessages() } }) } } // // Select and lock a mailbox. Throws if mailbox does not exist // console.log('get lock') // // log out and close connection // // await client.logout();