fp/packages/scout/src/imap.js

133 lines
3.9 KiB
JavaScript
Raw Normal View History

2024-05-27 22:20:58 +00:00
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();