import { ImapFlow } from 'imapflow' import { simpleParser, ParsedMail } from 'mailparser' function getImapConfig() { return { host: process.env.IMAP_HOST || 'localhost', port: Number(process.env.IMAP_PORT) || 993, secure: (Number(process.env.IMAP_PORT) || 993) === 993, auth: { user: process.env.MAIL_USER || '', pass: process.env.MAIL_PASSWORD || '', }, logger: false as const, } } async function withClient(fn: (client: ImapFlow) => Promise): Promise { const client = new ImapFlow(getImapConfig()) await client.connect() try { return await fn(client) } finally { await client.logout() } } export interface MailFolder { path: string name: string delimiter: string specialUse: string | null subscribed: boolean messages: number unseen: number } export interface MessageSummary { uid: number from: { name: string; address: string } | null to: { name: string; address: string }[] subject: string date: string flags: string[] preview: string } export interface FullMessage { uid: number from: { name: string; address: string } | null to: { name: string; address: string }[] cc: { name: string; address: string }[] bcc: { name: string; address: string }[] replyTo: { name: string; address: string } | null subject: string date: string flags: string[] html: string text: string attachments: { filename: string contentType: string size: number contentId: string | null contentDisposition: string }[] messageId: string | null inReplyTo: string | null references: string[] } function parseAddress(addr: { name?: string; address?: string } | undefined) { if (!addr) return null return { name: addr.name || '', address: addr.address || '' } } function parseAddressList( addrs: ParsedMail['to'] ): { name: string; address: string }[] { if (!addrs) return [] const list = Array.isArray(addrs) ? addrs : [addrs] const result: { name: string; address: string }[] = [] for (const group of list) { if ('value' in group) { for (const a of group.value) { result.push({ name: a.name || '', address: a.address || '' }) } } } return result } export async function listFolders(): Promise { return withClient(async (client) => { const mailboxes = await client.list() const folders: MailFolder[] = [] for (const mb of mailboxes) { let messages = 0 let unseen = 0 try { const status = await client.status(mb.path, { messages: true, unseen: true }) messages = status.messages ?? 0 unseen = status.unseen ?? 0 } catch { // Some folders may not support STATUS } const parts = mb.path.split(mb.delimiter || '/') folders.push({ path: mb.path, name: parts[parts.length - 1] || mb.path, delimiter: mb.delimiter || '/', specialUse: (mb as Record).specialUse as string | null ?? null, subscribed: mb.subscribed ?? false, messages, unseen, }) } return folders }) } export async function listMessages( folder: string, page: number, limit: number ): Promise<{ messages: MessageSummary[]; total: number }> { return withClient(async (client) => { const lock = await client.getMailboxLock(folder) try { const total = client.mailbox?.exists ?? 0 if (total === 0) { return { messages: [], total: 0 } } // Calculate range (newest first) const end = total - (page - 1) * limit const start = Math.max(1, end - limit + 1) if (end < 1) { return { messages: [], total } } const messages: MessageSummary[] = [] for await (const msg of client.fetch(`${start}:${end}`, { uid: true, envelope: true, flags: true, bodyStructure: true, headers: ['content-type'], })) { const env = msg.envelope messages.push({ uid: msg.uid, from: env.from?.[0] ? { name: env.from[0].name || '', address: env.from[0].address || '' } : null, to: (env.to || []).map((a: { name?: string; address?: string }) => ({ name: a.name || '', address: a.address || '', })), subject: env.subject || '(No Subject)', date: env.date ? new Date(env.date).toISOString() : new Date().toISOString(), flags: Array.from(msg.flags || []), preview: '', }) } // Reverse so newest first messages.reverse() return { messages, total } } finally { lock.release() } }) } export async function getMessage( folder: string, uid: number ): Promise { return withClient(async (client) => { const lock = await client.getMailboxLock(folder) try { const raw = await client.fetchOne(String(uid), { source: true, flags: true, uid: true }, { uid: true }) if (!raw?.source) return null const parsed = await simpleParser(raw.source) return { uid: raw.uid, from: parseAddress(parsed.from?.value?.[0]), to: parseAddressList(parsed.to), cc: parseAddressList(parsed.cc), bcc: parseAddressList(parsed.bcc), replyTo: parsed.replyTo?.value?.[0] ? parseAddress(parsed.replyTo.value[0]) : null, subject: parsed.subject || '(No Subject)', date: parsed.date ? parsed.date.toISOString() : new Date().toISOString(), flags: Array.from(raw.flags || []), html: parsed.html || '', text: parsed.text || '', attachments: (parsed.attachments || []).map((att) => ({ filename: att.filename || 'attachment', contentType: att.contentType || 'application/octet-stream', size: att.size || 0, contentId: att.contentId || null, contentDisposition: att.contentDisposition || 'attachment', })), messageId: parsed.messageId || null, inReplyTo: (parsed.inReplyTo as string) || null, references: Array.isArray(parsed.references) ? parsed.references : parsed.references ? [parsed.references] : [], } } finally { lock.release() } }) } export async function updateFlags( folder: string, uid: number, action: 'add' | 'remove', flags: string[] ): Promise { return withClient(async (client) => { const lock = await client.getMailboxLock(folder) try { if (action === 'add') { await client.messageFlagsAdd(String(uid), flags, { uid: true }) } else { await client.messageFlagsRemove(String(uid), flags, { uid: true }) } } finally { lock.release() } }) } export async function moveMessage( folder: string, uid: number, destination: string ): Promise { return withClient(async (client) => { const lock = await client.getMailboxLock(folder) try { await client.messageMove(String(uid), destination, { uid: true }) } finally { lock.release() } }) } export async function deleteMessage( folder: string, uid: number ): Promise { return withClient(async (client) => { const lock = await client.getMailboxLock(folder) try { // Try to move to Trash first const mailboxes = await client.list() const trash = mailboxes.find( (mb) => (mb as Record).specialUse === '\\Trash' ) if (trash && folder !== trash.path) { await client.messageMove(String(uid), trash.path, { uid: true }) } else { // Already in trash or no trash folder -- permanently delete await client.messageFlagsAdd(String(uid), ['\\Deleted'], { uid: true }) await client.messageDelete(String(uid), { uid: true }) } } finally { lock.release() } }) } export async function searchMessages( query: string, folder: string ): Promise { return withClient(async (client) => { const lock = await client.getMailboxLock(folder) try { const uids = await client.search( { or: [ { subject: query }, { from: query }, { to: query }, { body: query }, ], }, { uid: true } ) if (uids.length === 0) return [] // Limit results const limitedUids = uids.slice(-100) const uidRange = limitedUids.join(',') const messages: MessageSummary[] = [] for await (const msg of client.fetch(uidRange, { uid: true, envelope: true, flags: true, }, { uid: true })) { const env = msg.envelope messages.push({ uid: msg.uid, from: env.from?.[0] ? { name: env.from[0].name || '', address: env.from[0].address || '' } : null, to: (env.to || []).map((a: { name?: string; address?: string }) => ({ name: a.name || '', address: a.address || '', })), subject: env.subject || '(No Subject)', date: env.date ? new Date(env.date).toISOString() : new Date().toISOString(), flags: Array.from(msg.flags || []), preview: '', }) } messages.reverse() return messages } finally { lock.release() } }) }