Implement regexp-based thread ID parsing from recipient email
This commit is contained in:
parent
9f750bc2eb
commit
0f596ab5f5
12
index.ts
12
index.ts
@ -1,5 +1,9 @@
|
|||||||
import {withMailbox} from "./src/mail/read.ts";
|
import {withClient} from "./src/mail/client.ts";
|
||||||
|
import {collect, Collection} from "./src/bones/collection/Collection.ts";
|
||||||
|
import {getMailboxesToSearch, withMailbox} from "./src/mail/read.ts";
|
||||||
|
|
||||||
await withMailbox('e12a', async c => {
|
(await getMailboxesToSearch())
|
||||||
console.log((await c.collect()).all())
|
.map(box => withMailbox(box, async c => {
|
||||||
})
|
console.log(await c.all())
|
||||||
|
}))
|
||||||
|
.awaitAll()
|
||||||
|
@ -13,7 +13,7 @@ const maybeConfig: any = {
|
|||||||
threads: {
|
threads: {
|
||||||
type: 'alias',
|
type: 'alias',
|
||||||
template: process.env.CHORUS_THREAD_TEMPLATE,
|
template: process.env.CHORUS_THREAD_TEMPLATE,
|
||||||
idPrefix: 't.',
|
idPrefix: 'c.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
20
src/mail/client.ts
Normal file
20
src/mail/client.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {ImapFlow} from "imapflow";
|
||||||
|
import type {Awaitable} from "../bones";
|
||||||
|
import {config} from "../config.ts";
|
||||||
|
|
||||||
|
export async function withClient<T>(cb: (client: ImapFlow) => Awaitable<T>): Promise<T> {
|
||||||
|
const client = new ImapFlow({
|
||||||
|
...config.mail.imap,
|
||||||
|
logger: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
let result: T
|
||||||
|
try {
|
||||||
|
result = await cb(client)
|
||||||
|
} finally {
|
||||||
|
await client.logout()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
@ -5,14 +5,31 @@ import {config} from "../config.ts";
|
|||||||
import type {Awaitable, Maybe} from "../bones";
|
import type {Awaitable, Maybe} from "../bones";
|
||||||
import {ReplyParser} from "./replies.ts";
|
import {ReplyParser} from "./replies.ts";
|
||||||
import {extract} from "letterparser";
|
import {extract} from "letterparser";
|
||||||
import {Collection} from "../bones/collection/Collection.ts";
|
import {collect, Collection} from "../bones/collection/Collection.ts";
|
||||||
import {AsyncCollection} from "../bones/collection/AsyncCollection.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<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 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> {
|
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)))
|
return MailboxIterable.with(mailbox, i => cb(new AsyncCollection(i, 100)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MailboxIterable extends Iterable<Message> {
|
export class MailboxIterable extends Iterable<Message> {
|
||||||
|
private addressMatcher: RegExp
|
||||||
private client?: Maybe<ImapFlow>
|
private client?: Maybe<ImapFlow>
|
||||||
private query: FetchQueryObject = {
|
private query: FetchQueryObject = {
|
||||||
envelope: true,
|
envelope: true,
|
||||||
@ -38,6 +55,7 @@ export class MailboxIterable extends Iterable<Message> {
|
|||||||
public readonly mailbox: string,
|
public readonly mailbox: string,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
this.addressMatcher = buildThreadAddressMatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
async at(i: number): Promise<Maybe<Message>> {
|
async at(i: number): Promise<Maybe<Message>> {
|
||||||
@ -73,13 +91,22 @@ export class MailboxIterable extends Iterable<Message> {
|
|||||||
const source = message.source.toString('utf-8')
|
const source = message.source.toString('utf-8')
|
||||||
const content = ReplyParser.parseReply(extract(source).text || '')
|
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 {
|
return {
|
||||||
id: `${this.mailbox}.${message.uid}`,
|
id: `${this.mailbox}.${message.uid}`,
|
||||||
date: message.envelope.date,
|
date: message.envelope.date,
|
||||||
recipients: message.envelope.to.map(x => x.address || '').filter(Boolean),
|
|
||||||
from: message.envelope.from[0],
|
from: message.envelope.from[0],
|
||||||
subject: message.envelope.subject,
|
subject: message.envelope.subject,
|
||||||
|
recipients,
|
||||||
content,
|
content,
|
||||||
|
thread,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
src/threads/id.ts
Normal file
22
src/threads/id.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {config} from "../config.ts";
|
||||||
|
|
||||||
|
function escapeRexString(rex: string): string {
|
||||||
|
const specials = ['.', '+', '(', ')', '[', ']', '^', '$']
|
||||||
|
for ( const char of specials ) {
|
||||||
|
rex = rex.replaceAll(char, `\\${char}`)
|
||||||
|
}
|
||||||
|
return rex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildThreadAddressMatcher(): RegExp {
|
||||||
|
// We want a regular expression that only matches the thread address
|
||||||
|
// template specified by the config. We also want it to have a capture
|
||||||
|
// group for the thread ID.
|
||||||
|
// In the config, the thread ID is represented with the placeholder: %ID%
|
||||||
|
|
||||||
|
const idPrefix = escapeRexString(config.mail.threads.idPrefix)
|
||||||
|
const idCapture = `(${idPrefix}[^@]+)` // first rule of email: don't be as strict as the RFC
|
||||||
|
|
||||||
|
const template = escapeRexString(config.mail.threads.template).replace('%ID%', idCapture)
|
||||||
|
return new RegExp(`^${template}$`)
|
||||||
|
}
|
@ -35,4 +35,5 @@ export type Message = {
|
|||||||
},
|
},
|
||||||
subject: string,
|
subject: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
thread?: string,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user