Skip to content
← Public packages

@kody/mailroom

Offline-first PWA for your Kody mailbox: sent and received threads, copy-prompt for agents, no send.

src/mail.ts

116 lines · 3.9 KB · TypeScript
import { kody } from 'kody:runtime'
import type { MailDirection, MailMessage, MailSummary } from './types.ts'

const DEFAULT_LIMIT = 100

function asStringArray(value: unknown): Array<string> {
	if (!Array.isArray(value)) return []
	return value.map((item) => String(item)).filter(Boolean)
}

export function asSummary(row: Record<string, unknown>): MailSummary {
	return {
		id: String(row.id || ''),
		direction: row.direction === 'outbound' ? 'outbound' : 'inbound',
		inbox_id: row.inbox_id == null ? null : String(row.inbox_id),
		thread_id: row.thread_id == null ? null : String(row.thread_id),
		from_address: row.from_address == null ? null : String(row.from_address),
		envelope_from: row.envelope_from == null ? null : String(row.envelope_from),
		to_addresses: asStringArray(row.to_addresses),
		subject: row.subject == null ? null : String(row.subject),
		message_id_header:
			row.message_id_header == null ? null : String(row.message_id_header),
		processing_status: String(row.processing_status || ''),
		classification: String(row.classification || ''),
		delivery_status:
			row.delivery_status == null ? null : String(row.delivery_status),
		received_at: row.received_at == null ? null : String(row.received_at),
		sent_at: row.sent_at == null ? null : String(row.sent_at),
		created_at: String(row.created_at || ''),
		updated_at: String(row.updated_at || ''),
	}
}

export function asMessage(row: Record<string, unknown>): MailMessage {
	const attachments = Array.isArray(row.attachments) ? row.attachments : []
	return {
		...asSummary(row),
		cc_addresses: asStringArray(row.cc_addresses),
		bcc_addresses: asStringArray(row.bcc_addresses),
		reply_to_addresses: asStringArray(row.reply_to_addresses),
		in_reply_to_header:
			row.in_reply_to_header == null ? null : String(row.in_reply_to_header),
		references: asStringArray(row.references),
		text_body: row.text_body == null ? null : String(row.text_body),
		html_body: row.html_body == null ? null : String(row.html_body),
		attachments: attachments.map((item) => {
			const att = (item || {}) as Record<string, unknown>
			return {
				id: String(att.id || ''),
				filename: att.filename == null ? null : String(att.filename),
				content_type: att.content_type == null ? null : String(att.content_type),
				size: typeof att.size === 'number' ? att.size : null,
			}
		}),
	}
}

export async function listMail(input: {
	direction?: MailDirection
	query?: string
	limit?: number
	inbox_id?: string
} = {}): Promise<Array<MailSummary>> {
	const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), 200)
	if (input.query?.trim()) {
		const result = await kody.emailMessageSearch({
			query: input.query.trim(),
			direction: input.direction,
			inbox_id: input.inbox_id,
			limit,
		})
		return (result.messages || []).map((row) =>
			asSummary(row as unknown as Record<string, unknown>),
		)
	}
	const result = await kody.emailMessageList({
		direction: input.direction,
		inbox_id: input.inbox_id,
		limit,
	})
	return (result.messages || []).map((row) =>
		asSummary(row as unknown as Record<string, unknown>),
	)
}

export async function listMailbox(limit = DEFAULT_LIMIT): Promise<Array<MailSummary>> {
	const [inbound, outbound] = await Promise.all([
		listMail({ direction: 'inbound', limit }),
		listMail({ direction: 'outbound', limit }),
	])
	const seen = new Set<string>()
	const all: Array<MailSummary> = []
	for (const message of [...inbound, ...outbound]) {
		if (!message.id || seen.has(message.id)) continue
		seen.add(message.id)
		all.push(message)
	}
	return all
}

export async function getMailMessage(messageId: string): Promise<MailMessage | null> {
	const id = messageId.trim()
	if (!id) throw new Error('messageId is required.')
	try {
		const row = await kody.emailMessageGet({ message_id: id })
		if (!row?.id) return null
		return asMessage(row as unknown as Record<string, unknown>)
	} catch {
		return null
	}
}

export async function listInboxes() {
	const result = await kody.emailInboxList({})
	return result.inboxes || []
}