156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
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, client?: ImapFlow): Promise<Collection<string>> {
|
|
// 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 (client?.list() || 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<T>(mailbox: string, cb: (c: AsyncCollection<Message>) => Awaitable<T>): Promise<T> {
|
|
return MailboxIterable.with(mailbox, i => cb(new AsyncCollection(i, 100)))
|
|
}
|
|
|
|
export class MailboxIterable extends Iterable<Message> {
|
|
private addressMatcher: RegExp
|
|
private client?: Maybe<ImapFlow>
|
|
private borrowedClient: boolean = false
|
|
private query: FetchQueryObject = {
|
|
envelope: true,
|
|
source: true,
|
|
uid: true,
|
|
bodyParts: ['text'],
|
|
}
|
|
|
|
public static async with<T>(mailbox: string, cb: (i: MailboxIterable) => Awaitable<T>): Promise<T> {
|
|
const inst = new MailboxIterable(mailbox)
|
|
let value: T
|
|
|
|
try {
|
|
value = await cb(inst)
|
|
} finally {
|
|
await inst.release()
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
constructor(
|
|
public readonly mailbox: string,
|
|
client?: ImapFlow,
|
|
) {
|
|
super()
|
|
this.addressMatcher = buildThreadAddressMatcher()
|
|
if ( client ) {
|
|
this.client = client
|
|
this.borrowedClient = true
|
|
}
|
|
}
|
|
|
|
async at(i: number): Promise<Maybe<Message>> {
|
|
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<Collection<Message>> {
|
|
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<number> {
|
|
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,
|
|
mailbox: this.mailbox,
|
|
modseq: message.modseq,
|
|
recipients,
|
|
content,
|
|
thread,
|
|
}
|
|
}
|
|
|
|
public async release(): Promise<void> {
|
|
if ( this.client && !this.borrowedClient ) {
|
|
await this.client.logout()
|
|
}
|
|
}
|
|
|
|
protected async withMailbox<T>(cb: (client: ImapFlow) => Awaitable<T>): Promise<T> {
|
|
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<ImapFlow> {
|
|
if ( this.client ) {
|
|
return this.client
|
|
}
|
|
|
|
const client = new ImapFlow({
|
|
...config.mail.imap,
|
|
logger: false,
|
|
})
|
|
|
|
await client.connect()
|
|
return this.client = client
|
|
}
|
|
}
|