import {Iterable} from "../bones/collection/Iterable.ts"; import type {Message} from "../types.ts"; import {type FetchMessageObject, type FetchQueryObject, ImapFlow} from "imapflow"; import {config} from "../config.ts"; import type {Awaitable, Maybe} from "../bones"; import {ReplyParser} from "./replies.ts"; import {extract} from "letterparser"; import {collect, Collection} from "../bones/collection/Collection.ts"; import {AsyncCollection} from "../bones/collection/AsyncCollection.ts"; import {withClient} from "./client.ts"; import {buildThreadAddressMatcher} from "../threads/id.ts"; export async function getMailboxesToSearch(thread?: string): Promise> { // There are 2 possibilities for where mail might end up. // Either the mail-server is configured to forward the + extensions // to their own folders automatically (e.g. the "c.1234" folder), or // aliased mail just winds up in INBOX. return collect(await withClient(c => c.list())) .filter(box => box.name === 'INBOX' || box.specialUse === '\\\\Inbox' || (!thread && box.name.startsWith(config.mail.threads.idPrefix)) || (thread && box.name === thread) ) .pluck('name') } export async function withMailbox(mailbox: string, cb: (c: AsyncCollection) => Awaitable): Promise { return MailboxIterable.with(mailbox, i => cb(new AsyncCollection(i, 100))) } export class MailboxIterable extends Iterable { private addressMatcher: RegExp private client?: Maybe private query: FetchQueryObject = { envelope: true, source: true, uid: true, bodyParts: ['text'], } public static async with(mailbox: string, cb: (i: MailboxIterable) => Awaitable): Promise { const inst = new MailboxIterable(mailbox) let value: T try { value = await cb(inst) } finally { await inst.release() } return value } constructor( public readonly mailbox: string, ) { super() this.addressMatcher = buildThreadAddressMatcher() } async at(i: number): Promise> { return this.withMailbox(async client => { const m = await client.fetchOne(`${i+1}`, this.query) return this.format(m) }) } async range(start: number, end: number): Promise> { return this.withMailbox(async client => { const m = await client.fetchAll(`${start+1}:${end+1}`, this.query) return Collection.normalize(m) .map(i => this.format(i)) }) } async count(): Promise { return this.withMailbox(async client => { const m = await client.status(this.mailbox, { messages: true, }) return m.messages || 0 }) } clone(): MailboxIterable { return new MailboxIterable(this.mailbox) } protected format(message: FetchMessageObject): Message { const source = message.source.toString('utf-8') const content = ReplyParser.parseReply(extract(source).text || '') const recipients = message.envelope.to .map(x => x.address || '') .filter(Boolean) const thread = recipients.map(addr => this.addressMatcher.exec(addr)) .map(result => result?.[1]) .filter(Boolean)[0] return { id: `${this.mailbox}.${message.uid}`, date: message.envelope.date, from: message.envelope.from[0], subject: message.envelope.subject, recipients, content, thread, } } public async release(): Promise { if ( this.client ) { await this.client.logout() } } protected async withMailbox(cb: (client: ImapFlow) => Awaitable): Promise { const client = await this.getClient() const lock = await client.getMailboxLock(this.mailbox) let value: T try { await client.mailboxOpen(this.mailbox) value = await cb(client) } finally { lock.release() } return value } protected async getClient(): Promise { if ( this.client ) { return this.client } const client = new ImapFlow({ ...config.mail.imap, logger: false, }) await client.connect() return this.client = client } }