import type { ImapMessageAttributes } from 'imap' import Imap from 'imap' interface EmailAddress { name?: string address: string } interface Headers { to: Array from: EmailAddress date: Date subject: string } export default class Email { public constructor( private imap: Imap, private mailbox: Imap.Box, public readonly id: number ) {} public async getAttributes(): Promise> { return new Promise>((res) => { this.imap.fetch(this.id, { bodies: [], struct: true }).on('message', (msg) => msg.on('attributes', (attrs) => { res(attrs.struct as Array) })) }) } public async getFlags(): Promise> { return new Promise>((res) => { this.imap.fetch(this.id, { bodies: [], struct: false }).on('message', (msg) => msg.on('attributes', (attrs) => { res(attrs.flags) })) }) } public async getText() { const part = await this.getTextPart() if (!part) { return '' } return this.fetchPart(part.partID).then(this.decodeEmail) } public async isSeen() { return this.getFlags().then((arr) => arr.includes('\\Seen')) } public async setIsSeen(isSeen: boolean): Promise { console.log(this.mailbox.flags, this.mailbox.permFlags, '\\\\Seen') console.log('isSeen', isSeen, await this.isSeen()) if (await this.isSeen() === isSeen) { return isSeen } if (isSeen) { return new Promise((res, rej) => { this.imap.addFlags(this.id, 'SEEN', (err) => { if (err) { rej(err) return } res(isSeen) }) }) } return new Promise((res, rej) => { this.imap.addKeywords(this.id, 'SEEN', (err) => { if (err) { rej(err) return } res(isSeen) }) }) } public async getHeaders(): Promise { const req = this.imap.fetch(this.id, { bodies: ['HEADER'], struct: false }) return new Promise((res) => { req.on('message', (msg) => msg.on('body', (stream) => { let bfr = '' stream .on('data', (chunk) => bfr += chunk.toString('utf8')) .once('end', () => { const tmp = Imap.parseHeader(bfr) function t(email: string): EmailAddress { if (email.includes('<')) { const [name, addr] = email.split('<', 2) if (!addr || !name) { return { address: email } } return { name, address: addr.slice(0, addr.indexOf('>')) } } return { address: email } } // console.log(tmp) res({ subject: tmp.subject?.[0] as string, to: tmp.to?.map(t) ?? [], from: t(tmp.from?.[0] ?? ''), date: new Date(tmp.date?.[0] ?? '') }) }) })) }) } /** * hold the attachment ID */ public async getAttachments(): Promise> { return this.getAttachmentParts() } public async downloadAttachment(attachment: any): Promise<{filename: string, data: Buffer}> { const req = this.imap.fetch(this.id, { bodies: [attachment.partID], struct: true }) return new Promise((res) => { req.on('message', (msg) => { const filename = attachment.params.name const encoding = attachment.encoding console.log(filename, encoding, msg) let buffer = new Uint8Array(0) msg.on('body', (stream) => { stream.on('data', (chunk: Buffer) => { // merge into one common buffer const len = buffer.length + chunk.length const merged = new Uint8Array(len) merged.set(buffer) merged.set(chunk, buffer.length) buffer = merged }).once('end', () => { res({filename, data: Buffer.from( Buffer .from(buffer) .toString('ascii'), 'base64' )}) }) }).once('end', () => { console.log('ended') }).once('error', (err) => { console.log(err) }) }).once('error', (err) => { console.log(err) }) }) } // this is defined but eslint do not seems to reconize it // eslint-disable-next-line no-undef private fetchPart(partID: string, encoding: BufferEncoding = 'utf8'): Promise { const req = this.imap.fetch(this.id, { bodies: partID, struct: true }) return new Promise((res) => { req.on('message', (msg) => { msg.on('body', (strm) => { let bfr = '' strm.on('data', (chunk: Buffer) => bfr += chunk.toString(encoding)) .once('end', () => res(bfr)) }) }) }) } private async getTextPart(attrs?: ImapMessageAttributes | Array): Promise<{partID: string} | null> { if (!attrs) { attrs = await this.getAttributes() } // @ts-expect-error IMAP does not return the correct type for (const item of attrs) { if (Array.isArray(item)) { return this.getTextPart(item) } else if (item.type === 'text' && item.subtype === 'plain') { return item } } return null } private async getAttachmentParts(attrs?: ImapMessageAttributes | Array): Promise> { if (!attrs) { attrs = await this.getAttributes() } const attachments = [] for (const item of attrs) { if (Array.isArray(item)) { attachments.push(...await this.getAttachmentParts(item)) } else if (item.disposition && ['inline', 'attachment'].indexOf(item.disposition.type.toLowerCase()) > -1) { attachments.push(item) } } return attachments } private decodeEmail(data: string): string { // normalise end-of-line signals data = data.replace(/(\r\n|\n|\r)/g, '\n') // replace equals sign at end-of-line with nothing data = data.replace(/=\n/g, '') // encoded text might contain percent signs // decode each section separately const bits = data.split('%') for (let idx = 0; idx < bits.length; idx++) { let char = bits[idx] if (!char) { continue } // replace equals sign with percent sign char = char.replace(/=/g, '%') // decode the section bits[idx] = decodeURIComponent(char) } // join the sections back together return bits.join('%') } }