@zaclinley/dsh-mailharbor 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/lib/client.js +146 -6
- package/lib/client.js.map +2 -2
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/host/address.ts", "../src/host/smtp.ts", "../src/host/imap.ts", "../src/host/api.ts", "../src/host/config.ts", "../src/host/paths.ts", "../src/host/picker.ts", "../src/host/backup.ts", "../src/host/mail-db.ts", "../src/host/http.ts", "../src/host/ai.ts", "../src/host/projects.ts", "../src/host/version.ts", "../src/host/html.ts", "../src/host/runtime.ts", "../src/host/secrets.ts", "../src/host/accounts.ts", "../src/host/ai-store.ts", "../src/host/tasks.ts", "../src/host/drafts.ts", "../src/host/read-state.ts", "../src/host/tools.ts", "../src/host/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Address } from '../shared/types.js'\n\n/** Split `a@b, \"Doe, John\" <c@d>` on commas that are not inside quotes or angle brackets. */\nexport function splitAddressHeader(value: string): string[] {\n const text = value.trim()\n if (text === '') return []\n const parts: string[] = []\n let current = ''\n let quoted = false\n let angle = 0\n for (const ch of text) {\n if (ch === '\"') quoted = !quoted\n if (ch === '<' && !quoted) angle += 1\n if (ch === '>' && !quoted && angle > 0) angle -= 1\n if ((ch === ',' || ch === ';') && !quoted && angle === 0) {\n parts.push(current)\n current = ''\n continue\n }\n current += ch\n }\n parts.push(current)\n return parts.map((part) => part.trim()).filter((part) => part !== '')\n}\n\n/** Parse one address token into `{ name, address }`. */\nexport function parseAddress(token: string): Address | null {\n const text = token.trim()\n if (text === '') return null\n const angled = /^(.*?)\\s*<([^>]+)>\\s*$/.exec(text)\n if (angled !== null) {\n const name = (angled[1] ?? '').trim().replace(/^\"|\"$/g, '').trim()\n const address = (angled[2] ?? '').trim()\n return address === '' ? null : { name, address }\n }\n if (/^[^\\s@]+@[^\\s@]+$/.test(text)) return { name: '', address: text }\n return null\n}\n\n/** Parse a whole To/Cc header. Invalid tokens are dropped. */\nexport function parseAddressList(value: string): Address[] {\n return splitAddressHeader(value)\n .map(parseAddress)\n .filter((address): address is Address => address !== null)\n}\n\n/** Format addresses back into a header value. */\nexport function formatAddressList(list: Address[]): string {\n return list\n .map((address) => (address.name !== '' ? `${address.name} <${address.address}>` : address.address))\n .filter((text) => text !== '')\n .join(', ')\n}\n\n/** First address of a list, or an empty address. */\nexport function firstAddress(list: Address[] | undefined): Address {\n const first = list?.[0]\n return first === undefined ? { name: '', address: '' } : { name: first.name, address: first.address }\n}\n\n/** The bare address from `Name <a@b>` or `a@b`. */\nexport function addressOf(value: string): string {\n const angled = /<([^>]+)>/.exec(value)\n return (angled === null ? value : (angled[1] ?? '')).trim()\n}\n\n/** Validate a single address without a full RFC parser. */\nexport function isEmail(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+$/.test(value.trim())\n}\n", "import nodemailer from 'nodemailer'\nimport type { SendResult, SmtpSettings } from '../shared/types.js'\nimport { addressOf, isEmail } from './address.js'\n\nexport interface SmtpCredentials {\n user: string\n pass?: string\n accessToken?: string\n}\n\nexport interface SmtpSecurityOptions {\n /**\n * Accept a self-signed certificate. Deliberately not reachable from the HTTP\n * API: production connections always verify the server certificate.\n */\n allowSelfSigned?: boolean\n}\n\nexport interface OutgoingMail {\n from?: string\n to: string\n cc?: string\n bcc?: string\n subject: string\n text: string\n html?: string\n inReplyTo?: string\n references?: string\n attachments?: { filename: string; contentType: string; content: Buffer }[]\n}\n\nfunction transportOptions(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n security: SmtpSecurityOptions = {},\n) {\n const secure = settings.secure !== false\n return {\n host: settings.host,\n port: Number(settings.port),\n secure,\n // Plain submission ports must upgrade; never send credentials in the clear.\n requireTLS: !secure,\n auth:\n credentials.accessToken !== undefined\n ? { type: 'OAuth2' as const, user: credentials.user, accessToken: credentials.accessToken }\n : { user: credentials.user, pass: credentials.pass ?? '' },\n tls: { rejectUnauthorized: security.allowSelfSigned !== true, servername: settings.host },\n connectionTimeout: 20_000,\n greetingTimeout: 20_000,\n socketTimeout: 120_000,\n pool: false,\n }\n}\n\nfunction createTransport(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n security: SmtpSecurityOptions = {},\n) {\n return nodemailer.createTransport(transportOptions(settings, credentials, security))\n}\n\n/** Validate one recipient header into envelope addresses. */\nexport function recipientsOf(value: string): string[] {\n return value\n .split(/[,;]/)\n .map((part) => addressOf(part))\n .filter((address) => isEmail(address))\n}\n\n/** Check credentials and connectivity without sending a message. */\nexport async function verifySmtp(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n security: SmtpSecurityOptions = {},\n): Promise<{ ok: true; host: string; port: number }> {\n const transport = createTransport(settings, credentials, security)\n try {\n await transport.verify()\n return { ok: true, host: settings.host, port: Number(settings.port) }\n } finally {\n transport.close()\n }\n}\n\n/**\n * Send one message: plain text plus an optional HTML alternative and files.\n * nodemailer builds the MIME tree, so encodings, long headers and 8-bit bodies\n * do not depend on hand-written RFC 5322 code.\n */\nexport async function sendMail(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n account: string,\n mail: OutgoingMail,\n security: SmtpSecurityOptions = {},\n): Promise<SendResult> {\n const from = addressOf(mail.from ?? account)\n if (!isEmail(from)) throw new Error('\u53D1\u4EF6\u4EBA\u5730\u5740\u65E0\u6548')\n const to = recipientsOf(mail.to)\n const cc = recipientsOf(mail.cc ?? '')\n if (to.length === 0) throw new Error('\u6536\u4EF6\u4EBA\u4E0D\u80FD\u4E3A\u7A7A')\n\n const transport = createTransport(settings, credentials, security)\n try {\n const info = await transport.sendMail({\n from: settings.fromName === '' ? from : { name: settings.fromName, address: from },\n to,\n ...(cc.length > 0 ? { cc } : {}),\n subject: mail.subject,\n text: mail.text,\n ...(mail.html !== undefined && mail.html.trim() !== '' ? { html: mail.html } : {}),\n ...(mail.inReplyTo !== undefined && mail.inReplyTo !== '' ? { inReplyTo: mail.inReplyTo } : {}),\n ...(mail.references !== undefined && mail.references !== '' ? { references: mail.references } : {}),\n ...(mail.attachments !== undefined && mail.attachments.length > 0\n ? {\n attachments: mail.attachments.map((file) => ({\n filename: file.filename,\n content: file.content,\n contentType: file.contentType,\n })),\n }\n : {}),\n })\n return {\n messageId: String(info.messageId ?? ''),\n accepted: (info.accepted ?? []).map((entry) => String(entry)),\n rejected: (info.rejected ?? []).map((entry) => String(entry)),\n }\n } finally {\n transport.close()\n }\n}\n", "import { ImapFlow } from 'imapflow'\nimport { simpleParser } from 'mailparser'\nimport type { Address, AttachmentMeta, ImapSettings } from '../shared/types.js'\nimport { addressOf, formatAddressList, parseAddressList } from './address.js'\n\nexport interface ImapCredentials {\n user: string\n pass?: string\n accessToken?: string\n}\n\nexport interface ImapMessage {\n uid: number\n messageId: string\n subject: string\n from: Address\n to: Address[]\n cc: Address[]\n date: string\n size: number\n flags: string[]\n read: boolean\n}\n\nexport interface ImapMailbox {\n path: string\n name: string\n specialUse: string | null\n messages: number\n}\n\nexport interface ImapFullMessage extends ImapMessage {\n text: string\n html: string | null\n attachments: AttachmentMeta[]\n inReplyTo: string\n references: string[]\n}\n\nexport interface ImapProviderOptions {\n connectionTimeoutMs?: number\n socketTimeoutMs?: number\n /** Called whenever the connection is replaced, for logging. */\n onReconnect?: (reason: string) => void\n}\n\nfunction normalizeId(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/^<|>$/g, '')\n .trim()\n}\n\nfunction isoDate(value: unknown): string {\n const date =\n value instanceof Date\n ? value\n : value === undefined || value === null\n ? undefined\n : new Date(String(value))\n return date instanceof Date && !Number.isNaN(date.getTime()) ? date.toISOString() : ''\n}\n\nfunction flagsOf(value: unknown): string[] {\n if (value === undefined || value === null) return []\n return [...(value as Iterable<unknown>)].map((flag) => String(flag))\n}\n\nfunction isSeen(flags: string[]): boolean {\n return flags.some((flag) => flag.toLowerCase() === '\\\\seen')\n}\n\nfunction attachmentList(parsed: Awaited<ReturnType<typeof simpleParser>>): AttachmentMeta[] {\n return (parsed.attachments ?? []).map((attachment, index) => ({\n id: String(index),\n filename: String(attachment.filename ?? 'attachment'),\n contentType: String(attachment.contentType ?? 'application/octet-stream'),\n size: Number(attachment.size ?? attachment.content?.length ?? 0),\n }))\n}\n\n/**\n * One IMAP account. The connection is reused across operations and every\n * operation is serialized, because an IMAP session is stateful: two interleaved\n * commands on one socket corrupt each other. A failed connection is discarded\n * and rebuilt on the next call, so a dropped socket never wedges the account.\n */\nexport class ImapProvider {\n private readonly settings: ImapSettings\n private readonly credentials: ImapCredentials\n private readonly options: ImapProviderOptions\n private client: ImapFlow | null = null\n private connecting: Promise<ImapFlow> | null = null\n private queue: Promise<unknown> = Promise.resolve()\n private disposed = false\n private mailboxCache: { at: number; items: ImapMailbox[] } | null = null\n private readonly countCache = new Map<string, number>()\n private readonly pageCache = new Map<string, { at: number; total: number; items: ImapMessage[] }>()\n private keepalive: ReturnType<typeof setInterval> | null = null\n private static readonly MAILBOX_TTL_MS = 60_000\n\n constructor(settings: ImapSettings, credentials: ImapCredentials, options: ImapProviderOptions = {}) {\n this.settings = settings\n this.credentials = credentials\n this.options = options\n }\n\n private async connect(): Promise<ImapFlow> {\n if (this.client !== null && this.client.usable) return this.client\n if (this.connecting !== null) return this.connecting\n this.connecting = (async () => {\n const client = new ImapFlow({\n host: this.settings.host,\n port: Number(this.settings.port),\n secure: this.settings.secure !== false,\n auth:\n this.credentials.accessToken !== undefined\n ? { user: this.credentials.user, accessToken: this.credentials.accessToken }\n : { user: this.credentials.user, pass: this.credentials.pass ?? '' },\n logger: false,\n tls: { rejectUnauthorized: true },\n connectionTimeout: this.options.connectionTimeoutMs ?? 15_000,\n greetingTimeout: this.options.connectionTimeoutMs ?? 15_000,\n socketTimeout: this.options.socketTimeoutMs ?? 60_000,\n })\n client.on('error', () => {\n // Errors surface through the operation that triggered them; keep the\n // process-wide handler so an idle socket error is never unhandled.\n })\n client.on('close', () => {\n if (this.client === client) {\n this.client = null\n this.stopKeepalive()\n }\n })\n try {\n await client.connect()\n } catch (error) {\n throw new Error(\n `IMAP \u8FDE\u63A5\u5931\u8D25 (${this.settings.host}:${String(this.settings.port)}): ${messageOf(error)}`,\n )\n }\n this.client = client\n this.startKeepalive(client)\n return client\n })()\n try {\n return await this.connecting\n } finally {\n this.connecting = null\n }\n }\n\n private startKeepalive(client: ImapFlow): void {\n this.stopKeepalive()\n // A NOOP every few minutes keeps the server from dropping an idle session,\n // so opening server mail does not pay a TLS + LOGIN round trip.\n this.keepalive = setInterval(() => {\n if (this.disposed || this.client !== client || !client.usable) return\n void this.run(async (active) => {\n await active.noop()\n }).catch(() => {\n // A failed keepalive is handled by the next operation.\n })\n }, 240_000)\n this.keepalive.unref?.()\n }\n\n private stopKeepalive(): void {\n if (this.keepalive !== null) {\n clearInterval(this.keepalive)\n this.keepalive = null\n }\n }\n\n /** Connect eagerly so the first list does not pay the handshake. */\n async warm(): Promise<void> {\n await this.connect()\n }\n\n private async reset(reason: string): Promise<void> {\n const client = this.client\n this.client = null\n if (client !== null) {\n try {\n await client.logout()\n } catch {\n // Already gone.\n }\n }\n if (!this.disposed) this.options.onReconnect?.(reason)\n }\n\n /** Serialize one operation on this account's connection. */\n private run<T>(operation: (client: ImapFlow) => Promise<T>): Promise<T> {\n const task = this.queue.then(\n async () => {\n const client = await this.connect()\n try {\n return await operation(client)\n } catch (error) {\n // A protocol or socket failure invalidates the session; reconnect next time.\n if (isFatal(error)) await this.reset(messageOf(error))\n throw error\n }\n },\n async () => {\n const client = await this.connect()\n return operation(client)\n },\n )\n this.queue = task.then(\n () => undefined,\n () => undefined,\n )\n return task\n }\n\n async check(): Promise<{ ok: true; mailboxes: { name: string; count: number }[] }> {\n return this.run(async (client) => {\n const boxes = await this.listMailboxes(client)\n return { ok: true as const, mailboxes: boxes.map((box) => ({ name: box.path, count: box.messages })) }\n })\n }\n\n async mailboxes(force = false): Promise<ImapMailbox[]> {\n const cached = this.mailboxCache\n if (!force && cached !== null && Date.now() - cached.at < ImapProvider.MAILBOX_TTL_MS) {\n return cached.items\n }\n const items = await this.run((client) => this.listMailboxes(client))\n this.mailboxCache = { at: Date.now(), items }\n return items\n }\n\n private async listMailboxes(client: ImapFlow): Promise<ImapMailbox[]> {\n const listed = await client.list()\n const boxes = listed.filter((box) => box.flags?.has?.('\\\\Noselect') !== true)\n return boxes.map((box) => ({\n path: box.path,\n name: box.name ?? box.path,\n specialUse: box.specialUse ?? null,\n messages: this.countCache.get(box.path) ?? 0,\n }))\n }\n\n async list(mailbox: string, limit: number, offset = 0): Promise<{ total: number; items: ImapMessage[] }> {\n const cacheKey = `${mailbox}:${String(limit)}:${String(offset)}`\n const cached = this.pageCache.get(cacheKey)\n if (cached !== undefined && Date.now() - cached.at < 15_000) {\n return { total: cached.total, items: cached.items }\n }\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const total = Number((client.mailbox as { exists?: number } | undefined)?.exists ?? 0)\n const cachedBox = this.mailboxCache?.items.find((box) => box.path === mailbox)\n if (cachedBox !== undefined) cachedBox.messages = total\n this.countCache.set(mailbox, total)\n const end = total - offset\n if (total === 0 || end < 1) return { total, items: [] }\n const start = Math.max(1, end - limit + 1)\n const items: ImapMessage[] = []\n for await (const message of client.fetch(`${String(start)}:${String(end)}`, {\n uid: true,\n envelope: true,\n flags: true,\n internalDate: true,\n size: true,\n })) {\n items.push(toMessage(message as unknown as RawImapMessage))\n }\n items.sort((left, right) => right.date.localeCompare(left.date))\n this.pageCache.set(cacheKey, { at: Date.now(), total, items })\n if (this.pageCache.size > 30) {\n const oldest = this.pageCache.keys().next().value\n if (oldest !== undefined) this.pageCache.delete(oldest)\n }\n return { total, items }\n } finally {\n lock.release()\n }\n })\n }\n\n async search(\n mailbox: string,\n options: {\n keywords?: string[]\n from?: string\n subject?: string\n since?: string\n before?: string\n limit?: number\n },\n ): Promise<{ items: ImapMessage[]; total: number }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const criteria: Record<string, unknown> = {}\n if ((options.from ?? '') !== '') criteria.from = options.from\n if ((options.subject ?? '') !== '') criteria.subject = options.subject\n const keywords = (options.keywords ?? []).filter((keyword) => keyword !== '')\n if (keywords.length > 0) criteria.text = keywords.join(' ')\n if ((options.since ?? '') !== '') criteria.since = new Date(`${String(options.since)}T00:00:00Z`)\n if ((options.before ?? '') !== '') criteria.before = new Date(`${String(options.before)}T23:59:59Z`)\n if (Object.keys(criteria).length === 0) return { items: [], total: 0 }\n const found = await client.search(criteria, { uid: true })\n const uids = (Array.isArray(found) ? found : [])\n .map((uid) => Number(uid))\n .filter((uid) => Number.isFinite(uid))\n .sort((left, right) => left - right)\n const take = uids.slice(-Math.min(100, options.limit ?? 60))\n if (take.length === 0) return { items: [], total: uids.length }\n const wanted = new Set(take)\n const items: ImapMessage[] = []\n const first = take[0] ?? 0\n const last = take[take.length - 1] ?? 0\n for await (const message of client.fetch(\n `${String(first)}:${String(last)}`,\n { uid: true, envelope: true, flags: true, internalDate: true, size: true },\n { uid: true },\n )) {\n const item = toMessage(message as unknown as RawImapMessage)\n if (wanted.has(item.uid)) items.push(item)\n }\n items.sort((left, right) => right.date.localeCompare(left.date))\n return { items, total: uids.length }\n } finally {\n lock.release()\n }\n })\n }\n\n async read(mailbox: string, uid: number): Promise<ImapFullMessage> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const message = await client.fetchOne(\n uid,\n { uid: true, source: true, flags: true, envelope: true, internalDate: true, size: true },\n { uid: true },\n )\n if (message === false || message === undefined || message.source === undefined) {\n throw new Error('\u90AE\u4EF6\u4E0D\u5B58\u5728')\n }\n const parsed = await simpleParser(message.source)\n const base = toMessage(message as unknown as RawImapMessage)\n const html =\n typeof parsed.html === 'string'\n ? parsed.html\n : Buffer.isBuffer(parsed.html)\n ? parsed.html.toString('utf8')\n : null\n return {\n ...base,\n text: typeof parsed.text === 'string' ? parsed.text : '',\n html: html === '' ? null : html,\n attachments: attachmentList(parsed),\n inReplyTo: normalizeId(parsed.inReplyTo),\n references: String(parsed.references ?? '')\n .split(/\\s+/)\n .map(normalizeId)\n .filter((item) => item !== ''),\n }\n } finally {\n lock.release()\n }\n })\n }\n\n async attachment(\n mailbox: string,\n uid: number,\n part: number,\n ): Promise<{ filename: string; contentType: string; dataBase64: string }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const message = await client.fetchOne(uid, { uid: true, source: true }, { uid: true })\n if (message === false || message === undefined || message.source === undefined) {\n throw new Error('\u90AE\u4EF6\u4E0D\u5B58\u5728')\n }\n const parsed = await simpleParser(message.source)\n const attachment = parsed.attachments?.[part]\n if (attachment === undefined) throw new Error('\u9644\u4EF6\u4E0D\u5B58\u5728')\n return {\n filename: String(attachment.filename ?? 'attachment'),\n contentType: String(attachment.contentType ?? 'application/octet-stream'),\n dataBase64: attachment.content.toString('base64'),\n }\n } finally {\n lock.release()\n }\n })\n }\n\n async unseen(): Promise<{ count: number; items: { uid: number; messageId: string }[] }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock('INBOX')\n try {\n const found = await client.search({ seen: false }, { uid: true })\n const uids = Array.isArray(found) ? found : []\n const items: { uid: number; messageId: string }[] = []\n if (uids.length > 0) {\n const range = uids.slice(-200).join(',')\n for await (const message of client.fetch(range, { uid: true, envelope: true }, { uid: true })) {\n items.push({\n uid: Number(message.uid),\n messageId: normalizeId(message.envelope?.messageId),\n })\n }\n }\n return { count: uids.length, items }\n } finally {\n lock.release()\n }\n })\n }\n\n async setSeen(mailbox: string, uid: number, seen: boolean): Promise<{ read: boolean; flags: string[] }> {\n this.pageCache.clear()\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n if (seen) await client.messageFlagsAdd(uid, ['\\\\Seen'], { uid: true })\n else await client.messageFlagsRemove(uid, ['\\\\Seen'], { uid: true })\n const message = await client.fetchOne(uid, { uid: true, flags: true }, { uid: true })\n const flags = flagsOf(message === false || message === undefined ? [] : message.flags)\n return { read: isSeen(flags), flags }\n } finally {\n lock.release()\n }\n })\n }\n\n /** Set \\Seen by Message-ID, which is how local-archive rows reach the server. */\n async setSeenByMessageId(\n mailbox: string,\n messageId: string,\n seen: boolean,\n ): Promise<{ found: boolean; uid?: number; read?: boolean }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const id = normalizeId(messageId)\n let found = await client.search({ header: { 'Message-ID': id } }, { uid: true })\n if (!Array.isArray(found) || found.length === 0) {\n found = await client.search({ header: { 'Message-ID': `<${id}>` } }, { uid: true })\n }\n const uids = Array.isArray(found) ? found : []\n const uid = uids[uids.length - 1]\n if (uid === undefined) return { found: false }\n if (seen) await client.messageFlagsAdd(uid, ['\\\\Seen'], { uid: true })\n else await client.messageFlagsRemove(uid, ['\\\\Seen'], { uid: true })\n const message = await client.fetchOne(uid, { uid: true, flags: true }, { uid: true })\n const flags = flagsOf(message === false || message === undefined ? [] : message.flags)\n return { found: true, uid: Number(uid), read: isSeen(flags) }\n } finally {\n lock.release()\n }\n })\n }\n\n async dispose(): Promise<void> {\n this.disposed = true\n this.pageCache.clear()\n this.stopKeepalive()\n await this.reset('dispose')\n }\n}\n\nexport interface RawImapMessage {\n uid: number\n envelope?: {\n messageId?: string\n subject?: string\n from?: { name?: string; address?: string }[]\n to?: { name?: string; address?: string }[]\n cc?: { name?: string; address?: string }[]\n date?: string | Date\n }\n internalDate?: string | Date\n size?: number\n flags?: Iterable<unknown>\n}\n\nfunction toMessage(message: RawImapMessage): ImapMessage {\n const flags = flagsOf(message.flags)\n const addressList = (value: { name?: string; address?: string }[] | undefined): Address[] =>\n (value ?? [])\n .map((entry) => ({ name: entry.name ?? '', address: entry.address ?? '' }))\n .filter((address) => address.address !== '')\n return {\n uid: Number(message.uid),\n messageId: normalizeId(message.envelope?.messageId),\n subject: String(message.envelope?.subject ?? '(\u65E0\u4E3B\u9898)'),\n from: addressList(message.envelope?.from)[0] ?? { name: '', address: '' },\n to: addressList(message.envelope?.to),\n cc: addressList(message.envelope?.cc),\n date: isoDate(message.internalDate ?? message.envelope?.date),\n size: Number(message.size ?? 0),\n flags,\n read: isSeen(flags),\n }\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\n/** Errors that mean the session itself is unusable. */\nfunction isFatal(error: unknown): boolean {\n if (error instanceof Error) {\n const code = (error as { code?: string }).code ?? ''\n if (\n /^(NoConnection|ConnectionClosed|ConnectionNotAvailable|EConnectionClosed|ETIMEDOUT|ECONNRESET|EPIPE)$/.test(\n code,\n )\n ) {\n return true\n }\n return /connection (closed|lost|not available)|socket/i.test(error.message)\n }\n return false\n}\n\n/** Exported for tests and for the service layer that formats addresses. */\nexport { addressOf, formatAddressList, parseAddressList }\n", "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport type {\n Account,\n Address,\n AppState,\n AttachmentMeta,\n BackupStatus,\n Draft,\n DraftAttachment,\n MessageDetail,\n MessageSummary,\n NewAccountInput,\n Page,\n Project,\n SendResult,\n Task,\n SettingsView,\n} from '../shared/types.js'\nimport { addressOf, formatAddressList, parseAddressList } from './address.js'\nimport {\n buildAccount,\n normalizeAccount,\n saveAccounts,\n saveSettings,\n type MailHarborConfig,\n} from './config.js'\nimport { resolveUserPath } from './paths.js'\nimport { pickDirectory } from './picker.js'\nimport { BackupEngine } from './backup.js'\nimport { HttpError, Router, assertTrusted, readJsonBody, sendJson, type RequestContext } from './http.js'\nimport { normalizeTranslated, summaryBlocks, textToHtml } from './ai.js'\nimport { appendConversation, createProject, type ProjectRecord } from './projects.js'\nimport type { TaskRecord } from './tasks.js'\nimport { VERSION } from './version.js'\nimport type { MailHarborRuntime } from './runtime.js'\nimport type { ImapProvider } from './imap.js'\nimport { verifySmtp } from './smtp.js'\nimport { escapeHtml, htmlToText } from './html.js'\n\nexport const API_PREFIX = '/mail-plugin/api'\n\n/** Build the request handler the Host mounts at `/mail-plugin`. */\nexport function createApiHandler(runtime: MailHarborRuntime) {\n const router = buildRouter(runtime)\n return async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', 'http://localhost')\n if (!url.pathname.startsWith(API_PREFIX)) {\n sendJson(res, 404, { error: 'Not found' })\n return\n }\n const pathname = url.pathname.slice(API_PREFIX.length) || '/'\n const method = (req.method ?? 'GET').toUpperCase()\n try {\n if (method !== 'GET' && method !== 'HEAD') assertTrusted(req)\n const match = router.match(method, pathname)\n if (match === null) throw new HttpError(404, `\u672A\u77E5\u63A5\u53E3: ${method} ${pathname}`)\n runtime.refresh()\n const context: RequestContext = {\n req,\n res,\n url,\n params: match.params,\n body: () =>\n readJsonBody(req, method === 'POST' && pathname === '/send' ? 25 * 1024 * 1024 : 2 * 1024 * 1024),\n }\n const result = await match.handler(context)\n if (result !== undefined) sendJson(res, 200, result)\n } catch (error) {\n if (res.headersSent) {\n res.end()\n return\n }\n sendJson(res, error instanceof HttpError ? error.status : 500, {\n error: error instanceof Error ? error.message : String(error),\n ...(error instanceof HttpError && error.code !== undefined ? { code: error.code } : {}),\n })\n }\n }\n}\n\nfunction buildRouter(runtime: MailHarborRuntime): Router {\n const router = new Router()\n\n router.get('/health', () => ({ ok: true, version: VERSION }))\n\n router.get('/state', () => appState(runtime))\n\n router.get('/folders', async ({ url }) => {\n const source = sourceOf(url)\n const requested = url.searchParams.get('accountId')\n if (source === 'server' && requested === 'all') {\n throw new HttpError(400, '\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD')\n }\n const accounts =\n requested === 'all'\n ? runtime.current.accounts.filter((item) => item.enabled !== false)\n : [accountFrom(runtime, url).account]\n if (source === 'server') {\n const provider = runtime.provider(accounts[0] as Account)\n const boxes = await provider.mailboxes(url.searchParams.get('refresh') === '1')\n return {\n items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages })),\n }\n }\n const counts = new Map<string, number>()\n let drafts = 0\n for (const account of accounts) {\n try {\n for (const box of runtime.database(account).mailboxes()) {\n counts.set(box.name, (counts.get(box.name) ?? 0) + box.count)\n }\n } catch {\n // An account without a backup simply contributes no folders.\n }\n try {\n drafts += runtime.drafts(account).list().length\n } catch {\n // Ignore a broken drafts file.\n }\n }\n const items = [...counts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count)\n items.unshift({ name: '__drafts', count: drafts })\n return { items }\n })\n\n router.get('/messages', async ({ url }) => {\n const source = sourceOf(url)\n const requested = url.searchParams.get('accountId')\n const mailbox = url.searchParams.get('mailbox') ?? 'all'\n const query = url.searchParams.get('query') ?? ''\n const offset = Number(url.searchParams.get('offset') ?? 0)\n const limit = Number(url.searchParams.get('limit') ?? runtime.current.render.listLimit)\n if (requested === 'all') {\n if (source === 'server') throw new HttpError(400, '\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD')\n return listAllAccounts(runtime, { mailbox, query, offset, limit })\n }\n const { account } = accountFrom(runtime, url)\n if (source === 'server') {\n const items = await listServer(runtime, account, mailbox, query, limit, offset)\n return { ...items, offset, limit }\n }\n const page = runtime\n .database(account)\n .list({ mailbox: mailbox === '' ? 'all' : mailbox, query, offset, limit })\n return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) }\n })\n\n router.get('/messages/lookup', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = url.searchParams.get('key') ?? ''\n if (key.startsWith('local:')) {\n const id = Number(key.slice(6))\n const detail = runtime.database(account).get(id)\n if (detail === undefined) throw new HttpError(404, '\u90AE\u4EF6\u4E0D\u5B58\u5728')\n return { id: String(id), mailbox: detail.mailbox, subject: detail.subject }\n }\n if (key.startsWith('mid:')) {\n const found = runtime.database(account).findByMessageId(key.slice(4))\n if (found === undefined) throw new HttpError(404, '\u672C\u5730\u5907\u4EFD\u91CC\u627E\u4E0D\u5230\u8FD9\u5C01\u90AE\u4EF6')\n return { id: String(found.id), mailbox: found.mailbox, subject: '' }\n }\n throw new HttpError(400, '\u65E0\u6CD5\u5B9A\u4F4D\u8FD9\u5C01\u90AE\u4EF6')\n })\n\n router.get('/messages/:id', async ({ url, params }) => {\n const { account } = accountFrom(runtime, url)\n const source = sourceOf(url)\n const mailbox = url.searchParams.get('mailbox') ?? 'INBOX'\n return loadDetail(runtime, account, source, params.id ?? '', mailbox)\n })\n\n router.post('/messages/:id/read', async ({ url, params, body }) => {\n const { account } = accountFrom(runtime, url)\n const source = sourceOf(url)\n const mailbox = url.searchParams.get('mailbox') ?? 'INBOX'\n const payload = await body()\n const read = payload.read !== false\n if (source === 'server') {\n const provider = runtime.provider(account)\n const result = await provider.setSeen(mailbox, Number(params.id), read)\n return { ok: true, read: result.read }\n }\n const db = runtime.database(account)\n const detail = db.get(Number(params.id))\n if (detail === undefined) throw new HttpError(404, '\u90AE\u4EF6\u4E0D\u5B58\u5728')\n const store = runtime.readState(account)\n const key = detail.messageId !== '' ? `mid:${detail.messageId}` : `local:${detail.id}`\n store.setUnread(key, !read)\n // Mirror the change to the server when the archived message still exists there.\n if (typeof detail.messageId === 'string' && detail.messageId !== '') {\n try {\n const provider = runtime.provider(account)\n const mailboxName = detail.mailbox === '' ? 'INBOX' : detail.mailbox\n await provider.setSeenByMessageId(mailboxName, detail.messageId, read)\n } catch {\n // Offline or removed on the server: the local record is the source of truth.\n }\n }\n return { ok: true, read }\n })\n\n router.get('/attachment', async ({ url, res }) => {\n const { account } = accountFrom(runtime, url)\n const source = sourceOf(url)\n if (source === 'server') {\n const mailbox = url.searchParams.get('mailbox') ?? 'INBOX'\n const uid = Number(url.searchParams.get('uid') ?? 0)\n const part = Number(url.searchParams.get('part') ?? 0)\n const provider = runtime.provider(account)\n const file = await provider.attachment(mailbox, uid, part)\n writeAttachment(res, file.filename, file.contentType, Buffer.from(file.dataBase64, 'base64'))\n return undefined\n }\n const emailId = Number(url.searchParams.get('emailId') ?? 0)\n const attachmentId = Number(url.searchParams.get('attachmentId') ?? 0)\n const file = runtime.database(account).attachment(emailId, attachmentId)\n if (file === undefined) throw new HttpError(404, '\u9644\u4EF6\u4E0D\u5B58\u5728')\n writeAttachment(res, file.filename, file.contentType, Buffer.from(file.data))\n return undefined\n })\n\n router.get('/drafts', ({ url }) => {\n const { account } = accountFrom(runtime, url)\n return { items: runtime.drafts(account).list() }\n })\n\n router.post('/drafts', async ({ url, body }) => {\n const { account } = accountFrom(runtime, url)\n const payload = await body()\n const draft = runtime.drafts(account).save(normalizeDraftInput(payload, account.id))\n return { draft }\n })\n\n router.delete('/drafts/:id', ({ url, params }) => {\n const { account } = accountFrom(runtime, url)\n const removed = runtime.drafts(account).remove(params.id ?? '')\n if (!removed) throw new HttpError(404, '\u8349\u7A3F\u4E0D\u5B58\u5728')\n return { ok: true }\n })\n\n router.post('/send', async ({ url, body }) => {\n const { account } = accountFrom(runtime, url)\n const payload = await body()\n const result = await sendFromPayload(runtime, account, payload)\n return result\n })\n\n router.get('/unread', async ({ url }) => {\n const requested = url.searchParams.get('accountId')\n const accounts =\n requested === null || requested === '' || requested === 'all'\n ? runtime.current.accounts.filter((item) => item.enabled !== false)\n : [accountById(runtime, requested)]\n let server = 0\n let answered = 0\n await Promise.all(\n accounts.map(async (account) => {\n if (!runtime.hasCredentials(account)) return\n try {\n const result = await runtime.provider(account).unseen()\n server += result.count\n answered += 1\n } catch {\n // An unreachable server falls back to the local counter.\n }\n }),\n )\n if (answered > 0) return { unread: server, source: 'server' }\n const local = accounts.reduce((sum, account) => sum + runtime.readState(account).count(), 0)\n return { unread: local, source: 'local' }\n })\n\n router.get('/accounts', () => ({ accounts: runtime.current.accounts }))\n\n router.post('/accounts', async ({ body }) => {\n const payload = await body()\n const input = payload as unknown as NewAccountInput & { connect?: boolean; remember?: boolean }\n if (typeof input.email !== 'string') throw new HttpError(400, '\u7F3A\u5C11\u90AE\u7BB1\u5730\u5740')\n const password = typeof input.password === 'string' ? input.password : ''\n const config = runtime.current\n const account = buildAccount(input, config.accounts)\n if (password === '') throw new HttpError(400, '\u8BF7\u8F93\u5165\u5BC6\u7801\u6216\u5E94\u7528\u4E13\u7528\u5BC6\u7801')\n\n if (input.connect !== false) {\n const provider = await verifyImap(account, password)\n await provider.dispose()\n try {\n await verifySmtp(account.smtp, { user: account.username, pass: password })\n } catch {\n // IMAP is the gate; SMTP problems are reported by the connection check.\n }\n }\n runtime.credentials.remember(account, password, input.remember !== false)\n const accounts = [...config.accounts, account]\n saveAccounts(accounts, account.id)\n runtime.refresh()\n return { account, state: appState(runtime) }\n })\n\n router.patch('/accounts/:id', async ({ params, body }) => {\n const config = runtime.current\n const id = params.id ?? ''\n const existing = config.accounts.find((account) => account.id === id)\n if (existing === undefined) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n const payload = await body()\n const merged = {\n ...existing,\n label: typeof payload.label === 'string' ? payload.label : existing.label,\n username: typeof payload.username === 'string' ? payload.username : existing.username,\n imap: { ...existing.imap, ...asObject(payload.imap) },\n smtp: { ...existing.smtp, ...asObject(payload.smtp) },\n caldav:\n payload.caldav === null\n ? null\n : payload.caldav === undefined\n ? existing.caldav\n : { ...asObject(payload.caldav) },\n enabled: payload.enabled === undefined ? existing.enabled : payload.enabled !== false,\n }\n const account = normalizeAccount(\n merged as Partial<Account>,\n 0,\n config.accounts.map((item) => item.id),\n )\n // Preserve the id and creation time; the editor cannot reassign them.\n account.id = existing.id\n account.createdAt = existing.createdAt\n const accounts = config.accounts.map((item) => (item.id === existing.id ? account : item))\n saveAccounts(accounts, config.activeAccountId)\n runtime.invalidate(existing.id)\n return { account }\n })\n\n router.delete('/accounts/:id', ({ params }) => {\n const config = runtime.current\n const id = params.id ?? ''\n const accounts = config.accounts.filter((account) => account.id !== id)\n if (accounts.length === config.accounts.length) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n const active = config.activeAccountId === id ? (accounts[0]?.id ?? null) : config.activeAccountId\n saveAccounts(accounts, active)\n runtime.invalidate(id)\n return { ok: true }\n })\n\n router.post('/accounts/:id/activate', ({ params }) => {\n const config = runtime.current\n const id = params.id ?? ''\n if (!config.accounts.some((account) => account.id === id)) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n saveAccounts(config.accounts, id)\n runtime.refresh()\n return { ok: true, activeAccountId: id }\n })\n\n router.post('/accounts/:id/login', async ({ params, body }) => {\n const account = accountById(runtime, params.id)\n const payload = await body()\n const password = typeof payload.password === 'string' ? payload.password : ''\n if (password === '') throw new HttpError(400, '\u8BF7\u8F93\u5165\u5BC6\u7801\u6216\u5E94\u7528\u4E13\u7528\u5BC6\u7801')\n const provider = await verifyImap(account, password)\n await provider.dispose()\n const stored = runtime.credentials.remember(account, password, payload.remember !== false)\n return { ok: true, stored: stored.stored }\n })\n\n router.post('/accounts/:id/logout', ({ params }) => {\n const account = accountById(runtime, params.id)\n runtime.credentials.forget(account)\n runtime.invalidate(account.id)\n return { ok: true }\n })\n\n router.post('/accounts/:id/check', async ({ params }) => {\n const account = accountById(runtime, params.id)\n const provider = runtime.provider(account)\n const imap = await provider.check()\n let smtp: { ok: boolean; error?: string } = { ok: false }\n try {\n const credentials = runtime.credentialsFor(account)\n await verifySmtp(account.smtp, credentials)\n smtp = { ok: true }\n } catch (error) {\n smtp = { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n return { ok: true, imap, smtp }\n })\n\n router.get('/ai/thread', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = (url.searchParams.get('key') ?? '').trim()\n if (key === '') throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key')\n return { thread: runtime.aiThreads(account).get(key) }\n })\n\n router.delete('/ai/thread', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = (url.searchParams.get('key') ?? '').trim()\n if (key === '') throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key')\n return { ok: runtime.aiThreads(account).clear(key) }\n })\n\n router.delete('/ai/cards', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = (url.searchParams.get('key') ?? '').trim()\n const cardId = (url.searchParams.get('cardId') ?? '').trim()\n if (key === '' || cardId === '') throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key \u6216\u5361\u7247 id')\n return { thread: runtime.aiThreads(account).removeCard(key, cardId) }\n })\n\n router.post('/ai/cards', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const key = String(payload.key ?? '').trim()\n if (key === '' || key.length > 200) throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key')\n const kind = String(payload.kind ?? '')\n if (!['summary', 'reply', 'translate', 'answer'].includes(kind)) {\n throw new HttpError(400, `\u672A\u77E5\u7684\u5361\u7247\u7C7B\u578B: ${kind}`)\n }\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n const config = runtime.current\n const instruction = typeof payload.instruction === 'string' ? payload.instruction : ''\n const language = payload.language === 'en' ? 'en' : 'zh'\n const target = payload.target === 'en' ? 'en' : 'zh'\n const ai = runtime.ai()\n try {\n let output = ''\n let html: string | undefined\n let meta: Record<string, string> | undefined\n if (kind === 'summary') {\n output = await ai.summarize(detail)\n } else if (kind === 'reply') {\n output = await ai.reply(account, detail, { instruction, language })\n html = textToHtml(output)\n meta = {\n to: detail.from.address,\n subject: /^re:/i.test(detail.subject) ? detail.subject : `Re: ${detail.subject}`,\n inReplyTo: detail.messageId ?? '',\n references: [detail.references.join(' '), detail.messageId ?? ''].join(' ').trim(),\n language,\n }\n } else if (kind === 'translate') {\n const source = detail.html !== null && detail.html !== '' ? detail.html : detail.text\n const translated = normalizeTranslated(await ai.translate(source, target))\n output = translated.text\n html = translated.html\n meta = { target }\n } else {\n const earlier =\n runtime\n .aiThreads(account)\n .get(key)\n ?.cards.filter((card) => card.kind === 'answer')\n .map((card) => card.output) ?? []\n output = await ai.ask(detail, instruction, earlier)\n }\n const card = {\n id: randomUUID(),\n kind: kind as 'summary' | 'reply' | 'translate' | 'answer',\n instruction,\n output,\n ...(html === undefined ? {} : { html }),\n ...(meta === undefined ? {} : { meta }),\n provider: config.ai.provider,\n model: config.ai.model,\n createdAt: new Date().toISOString(),\n }\n const thread = runtime.aiThreads(account).appendCard({\n key,\n accountId: account.id,\n subject: detail.subject,\n messageId: detail.messageId ?? '',\n card,\n })\n return { card, thread }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/search', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const query = String(payload.query ?? '').trim()\n if (query === '') throw new HttpError(400, '\u641C\u7D22\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A')\n const source = sourceOfPayload(payload)\n const mailbox = String(payload.mailbox ?? 'all')\n try {\n const filters = await runtime.ai().parseSearch(query)\n const hasCondition =\n filters.keywords.length > 0 ||\n filters.from !== '' ||\n filters.subject !== '' ||\n filters.since !== '' ||\n filters.before !== ''\n if (!hasCondition) throw new HttpError(400, '\u6CA1\u6709\u7406\u89E3\u51FA\u53EF\u7528\u7684\u641C\u7D22\u6761\u4EF6\uFF0C\u8BF7\u6362\u4E2A\u8BF4\u6CD5')\n if (source === 'server') {\n const provider = runtime.provider(account)\n const result = await provider.search(mailbox === 'all' ? 'INBOX' : mailbox, {\n ...filters,\n limit: 60,\n })\n return {\n items: result.items.map((item) => ({ ...item, accountId: account.id })),\n total: result.total,\n offset: 0,\n limit: 60,\n ai: filters,\n }\n }\n const database = runtime.database(account)\n // Local search is cross-folder: a search should find a mail wherever it is.\n const keywords = [...filters.keywords]\n let page = database.searchAdvanced({ ...filters, subject: '', keywords, mailbox: 'all', limit: 60 })\n if (page.total === 0 && filters.subject !== '') {\n page = database.searchAdvanced({\n ...filters,\n subject: '',\n keywords: [...keywords, filters.subject],\n mailbox: 'all',\n limit: 60,\n })\n }\n return {\n ...page,\n items: page.items.map((item) => ({ ...item, accountId: account.id })),\n ai: filters,\n }\n } catch (error) {\n if (error instanceof HttpError) throw error\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/compose', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const mode = payload.mode === 'polish' ? 'polish' : payload.mode === 'shorten' ? 'shorten' : 'draft'\n const html = typeof payload.html === 'string' ? payload.html : ''\n const instruction = typeof payload.instruction === 'string' ? payload.instruction : ''\n const language = payload.language === 'en' ? 'en' : 'zh'\n try {\n const text = await runtime.ai().compose(account, { mode, instruction, html, language })\n return { text, html: /<[a-z][\\s\\S]*>/i.test(text) ? text : textToHtml(text) }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.get('/ai/status', ({ url }) => {\n const requested = url.searchParams.get('accountId')\n const account =\n requested !== null && requested !== '' && requested !== 'all'\n ? runtime.current.accounts.find((item) => item.id === requested)\n : runtime.active()\n return runtime.ai().status(account ?? null)\n })\n\n router.get('/ai/models', async () => ({ providers: await runtime.ai().models() }))\n\n router.post('/ai/summary', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n try {\n const summary = await runtime.ai().summarize(detail)\n return { summary, blocks: summaryBlocks(summary) }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/reply', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n const language = payload.language === 'en' ? 'en' : 'zh'\n const instruction = typeof payload.instruction === 'string' ? payload.instruction : ''\n try {\n const text = await runtime.ai().reply(account, detail, { instruction, language })\n return { text, html: textToHtml(text) }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/translate', async ({ body }) => {\n const payload = await body()\n const html = typeof payload.html === 'string' ? payload.html : ''\n const target = payload.target === 'en' ? 'en' : 'zh'\n try {\n const translated = normalizeTranslated(await runtime.ai().translate(html, target))\n return { text: translated.text, html: translated.html }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.get('/ai/style', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n return { style: runtime.ai().style(account) }\n })\n\n router.put('/ai/style', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n try {\n const style = runtime.ai().saveStyle(account, String(payload.profile ?? ''))\n return { style }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/style/learn', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n try {\n const samples = runtime.database(account).sentMessages(100)\n const style = await runtime.ai().learnStyle(account, samples)\n return { style }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n // ---- tasks ----\n\n router.get('/tasks', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const items = runtime.tasks().list({\n accountId: account.id,\n status: url.searchParams.get('status') ?? '',\n projectId: url.searchParams.get('projectId') ?? '',\n query: url.searchParams.get('query') ?? '',\n })\n return { items: items.map(taskView) }\n })\n\n router.post('/tasks', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const task = runtime.tasks().create({\n accountId: account.id,\n title: String(payload.title ?? ''),\n notes: typeof payload.notes === 'string' ? payload.notes : '',\n due: typeof payload.due === 'string' ? payload.due : '',\n status: typeof payload.status === 'string' ? payload.status : 'NEEDS-ACTION',\n priority: Number(payload.priority ?? 0),\n owner: typeof payload.owner === 'string' ? payload.owner : '',\n projectId: typeof payload.projectId === 'string' ? payload.projectId : null,\n messageKey: typeof payload.messageKey === 'string' ? payload.messageKey : '',\n messageSubject: typeof payload.messageSubject === 'string' ? payload.messageSubject : '',\n })\n return { task: taskView(task) }\n })\n\n router.patch('/tasks/:id', async ({ params, body }) => {\n const payload = await body()\n const task = runtime.tasks().update(String(params.id ?? ''), {\n ...(typeof payload.title === 'string' ? { title: payload.title } : {}),\n ...(typeof payload.notes === 'string' ? { notes: payload.notes } : {}),\n ...(typeof payload.due === 'string' ? { due: payload.due } : {}),\n ...(typeof payload.status === 'string' ? { status: payload.status } : {}),\n ...(typeof payload.priority === 'number' ? { priority: payload.priority } : {}),\n ...(typeof payload.owner === 'string' ? { owner: payload.owner } : {}),\n ...(payload.projectId === null || typeof payload.projectId === 'string'\n ? { projectId: payload.projectId }\n : {}),\n })\n return { task: taskView(task) }\n })\n\n router.delete('/tasks/:id', ({ params }) => {\n const removed = runtime.tasks().remove(String(params.id ?? ''))\n if (!removed) throw new HttpError(404, '\u4EFB\u52A1\u4E0D\u5B58\u5728')\n return { ok: true }\n })\n\n router.post('/tasks/extract', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n try {\n const suggestions = await runtime.ai().extractTasks(detail)\n return { suggestions }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n // ---- projects ----\n\n router.get('/projects', () => ({ items: runtime.projects().list().map(projectView) }))\n\n router.get('/projects/for', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = url.searchParams.get('key') ?? ''\n return {\n project:\n key === ''\n ? null\n : (() => {\n const found = runtime.projects().findByMessage(account.id, key)\n return found === undefined ? null : projectView(found)\n })(),\n }\n })\n\n router.post('/projects/pick-directory', async () => {\n const path = await pickDirectory()\n return { path }\n })\n\n router.post('/projects', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const name = String(payload.name ?? '').trim()\n const root = String(payload.root ?? '').trim() || runtime.current.projects.root\n const conversation = typeof payload.conversation === 'string' ? payload.conversation : ''\n const message = asObject(payload.message)\n const key = typeof message.key === 'string' ? message.key : ''\n const result = await createProject({\n store: runtime.projects(),\n input: {\n name,\n root,\n conversation,\n accountId: account.id,\n ...(key === ''\n ? {}\n : {\n message: {\n accountId: account.id,\n key,\n subject: String(message.subject ?? ''),\n date: String(message.date ?? ''),\n },\n }),\n ...(key === '' ? {} : { thread: { accountId: account.id, key } }),\n },\n registry: runtime.workspaceRegistry,\n sessions: runtime.sessions,\n })\n return { project: projectView(result.project), warnings: result.warnings }\n })\n\n router.post('/projects/:id/messages', async ({ params, body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const project = runtime.projects().addMessage(String(params.id ?? ''), {\n accountId: account.id,\n key: String(payload.key ?? ''),\n subject: String(payload.subject ?? ''),\n date: String(payload.date ?? ''),\n })\n if (project === undefined) throw new HttpError(404, '\u9879\u76EE\u4E0D\u5B58\u5728')\n return { project: projectView(project) }\n })\n\n router.delete('/projects/:id/messages', ({ params, url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = url.searchParams.get('key') ?? ''\n const project = runtime.projects().removeMessage(String(params.id ?? ''), account.id, key)\n if (project === undefined) throw new HttpError(404, '\u9879\u76EE\u4E0D\u5B58\u5728')\n return { project: projectView(project) }\n })\n\n router.post('/projects/:id/transfer', async ({ params, body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const project = runtime.projects().get(String(params.id ?? ''))\n if (project === undefined) throw new HttpError(404, '\u9879\u76EE\u4E0D\u5B58\u5728')\n const conversation = typeof payload.conversation === 'string' ? payload.conversation : ''\n if (conversation.trim() === '') throw new HttpError(400, '\u6CA1\u6709\u53EF\u8F6C\u79FB\u7684 AI \u8BB0\u5F55')\n appendConversation(project, conversation)\n const key = typeof payload.key === 'string' ? payload.key : ''\n const updated =\n key === ''\n ? runtime.projects().update(project.id, {})\n : runtime.projects().addThread(project.id, { accountId: account.id, key })\n return { project: projectView(updated ?? project) }\n })\n\n router.get('/settings', () => settingsView(runtime.current))\n\n router.patch('/settings', async ({ body }) => {\n const payload = await body()\n const patch: Record<string, unknown> = {}\n if (typeof payload.signature === 'string') patch.signature = payload.signature.slice(0, 5000)\n if (payload.ai !== undefined) patch.ai = asObject(payload.ai)\n if (payload.read !== undefined) patch.read = asObject(payload.read)\n if (payload.render !== undefined) patch.render = asObject(payload.render)\n if (payload.projects !== undefined) {\n const root = String(asObject(payload.projects).root ?? '').trim()\n if (root === '') throw new HttpError(400, '\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A')\n patch.projects = { root: resolveUserPath(root, '\u9879\u76EE\u76EE\u5F55') }\n }\n saveSettings(patch)\n runtime.refresh()\n return { settings: settingsView(runtime.current) }\n })\n\n router.get('/backup/status', ({ url }) => {\n const { account } = accountFrom(runtime, url)\n return runtime.backupStatus(account)\n })\n\n router.post('/backup/start', async ({ url }) => {\n const { account } = accountFrom(runtime, url)\n runtime.startBackup(account)\n return { ok: true, status: runtime.backupStatus(account) }\n })\n\n router.post('/backup/stop', ({ url }) => {\n const { account } = accountFrom(runtime, url)\n const stopped = runtime.stopBackup(account)\n return { ok: stopped, status: runtime.backupStatus(account) }\n })\n\n return router\n}\n\n/** Load one message from either source; shared by the mail and AI routes. */\nasync function loadDetail(\n runtime: MailHarborRuntime,\n account: Account,\n source: 'local' | 'server',\n id: string,\n mailbox: string,\n): Promise<MessageDetail> {\n if (source === 'server') {\n const message = await runtime.provider(account).read(mailbox, Number(id))\n return serverDetail(runtime, account, message, mailbox)\n }\n const detail = runtime.database(account).get(Number(id))\n if (detail === undefined) throw new HttpError(404, '\u90AE\u4EF6\u4E0D\u5B58\u5728')\n return localDetail(runtime, account, detail)\n}\n\n/** Merge the local backup of every enabled account into one newest-first page. */\nfunction listAllAccounts(\n runtime: MailHarborRuntime,\n options: { mailbox: string; query: string; offset: number; limit: number },\n): Page<MessageSummary> {\n const accounts = runtime.current.accounts.filter((item) => item.enabled !== false)\n const collected: MessageSummary[] = []\n let total = 0\n for (const account of accounts) {\n try {\n const page = runtime.database(account).list({\n mailbox: options.mailbox === '' ? 'all' : options.mailbox,\n query: options.query,\n offset: 0,\n limit: options.offset + options.limit,\n })\n total += page.total\n for (const item of page.items) collected.push(localSummary(runtime, account, item))\n } catch {\n // An account without a backup contributes nothing.\n }\n }\n collected.sort((left, right) => right.date.localeCompare(left.date))\n return {\n items: collected.slice(options.offset, options.offset + options.limit),\n total,\n offset: options.offset,\n limit: options.limit,\n }\n}\n\nfunction accountFromPayload(runtime: MailHarborRuntime, payload: Record<string, unknown>): Account {\n const requested = typeof payload.accountId === 'string' ? payload.accountId : ''\n if (requested !== '' && requested !== 'all') {\n const found = runtime.current.accounts.find((item) => item.id === requested)\n if (found !== undefined) return found\n }\n const active = runtime.active()\n if (active !== null) return active\n throw new HttpError(400, '\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n}\n\nfunction sourceOfPayload(payload: Record<string, unknown>): 'local' | 'server' {\n return payload.source === 'server' ? 'server' : 'local'\n}\n\nfunction mailboxOfPayload(payload: Record<string, unknown>): string {\n return typeof payload.mailbox === 'string' && payload.mailbox !== '' ? payload.mailbox : 'INBOX'\n}\n\nfunction aiHttpError(error: unknown): HttpError {\n if (error instanceof HttpError) return error\n return new HttpError(400, error instanceof Error ? error.message : String(error))\n}\n\nfunction accountById(runtime: MailHarborRuntime, id: string | undefined): Account {\n const account = runtime.current.accounts.find((item) => item.id === id)\n if (account === undefined) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n return account\n}\n\nfunction accountFrom(runtime: MailHarborRuntime, url: URL): { account: Account; config: MailHarborConfig } {\n const requested = url.searchParams.get('accountId')\n const config = runtime.current\n const account =\n requested === null || requested === '' || requested === 'active'\n ? (config.accounts.find((item) => item.id === config.activeAccountId) ?? config.accounts[0])\n : config.accounts.find((item) => item.id === requested)\n if (account === undefined) throw new HttpError(400, '\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n return { account, config }\n}\n\nfunction sourceOf(url: URL): 'local' | 'server' {\n return url.searchParams.get('source') === 'server' ? 'server' : 'local'\n}\n\nasync function verifyImap(account: Account, password: string): Promise<ImapProvider> {\n const { ImapProvider } = await import('./imap.js')\n const provider = new ImapProvider(account.imap, { user: account.username || account.email, pass: password })\n try {\n await provider.check()\n return provider\n } catch (error) {\n await provider.dispose()\n throw new HttpError(400, `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`)\n }\n}\n\nfunction appState(runtime: MailHarborRuntime): AppState {\n const config = runtime.current\n const account = config.accounts.find((item) => item.id === config.activeAccountId) ?? null\n return {\n version: VERSION,\n settings: settingsView(config),\n accounts: config.accounts,\n activeAccountId: config.activeAccountId,\n backup: account === null ? emptyBackup() : runtime.backupStatus(account),\n unread: account === null ? 0 : runtime.readState(account).count(),\n loggedIn: account !== null && runtime.hasCredentials(account),\n }\n}\n\nfunction emptyBackup(): BackupStatus {\n return {\n running: false,\n accountId: null,\n startedAt: null,\n finishedAt: null,\n lastSuccessAt: null,\n total: 0,\n added: 0,\n message: '',\n output: [],\n exitCode: null,\n }\n}\n\nfunction settingsView(config: MailHarborConfig): SettingsView {\n return {\n dataDir: config.dataDir,\n configFile: config.configFile,\n signature: config.signature,\n accounts: config.accounts,\n activeAccountId: config.activeAccountId,\n ai: {\n enabled: config.ai.enabled,\n provider: config.ai.provider,\n model: config.ai.model,\n styleSampleCount: config.ai.styleSampleCount,\n },\n read: { markOnOpen: config.read.markOnOpen, pollSeconds: config.read.pollSeconds },\n render: { listLimit: config.render.listLimit },\n projects: { root: config.projects.root },\n }\n}\n\nfunction taskView(task: TaskRecord): Task {\n return {\n id: task.id,\n accountId: task.accountId,\n projectId: task.projectId,\n title: task.title,\n notes: task.notes,\n due: task.due,\n status: task.status as Task['status'],\n priority: task.priority,\n owner: task.owner,\n messageKey: task.messageKey,\n messageSubject: task.messageSubject,\n dirty: task.dirty,\n createdAt: task.createdAt,\n updatedAt: task.updatedAt,\n completedAt: task.completedAt,\n }\n}\n\nfunction projectView(project: ProjectRecord): Project {\n return {\n id: project.id,\n name: project.name,\n path: project.path,\n workspaceId: project.workspaceId,\n sessionId: project.sessionId,\n accountIds: [...project.accountIds],\n messages: project.messages.map((message) => ({ ...message })),\n threads: project.threads.map((thread) => ({ ...thread })),\n createdAt: project.createdAt,\n updatedAt: project.updatedAt,\n }\n}\n\nfunction localSummary(runtime: MailHarborRuntime, account: Account, row: MessageSummary): MessageSummary {\n const store = runtime.readState(account)\n const key = row.messageId !== undefined && row.messageId !== '' ? `mid:${row.messageId}` : `local:${row.id}`\n return { ...row, accountId: account.id, unread: store.isUnread(key), backedUp: true }\n}\n\nfunction localDetail(runtime: MailHarborRuntime, account: Account, detail: MessageDetail): MessageDetail {\n return localSummary(runtime, account, detail) as MessageDetail\n}\n\nfunction serverSummary(\n account: Account,\n message: {\n uid: number\n messageId: string\n subject: string\n from: Address\n to: Address[]\n date: string\n read: boolean\n size: number\n },\n mailbox: string,\n backedUp: boolean,\n): MessageSummary {\n return {\n id: String(message.uid),\n accountId: account.id,\n uid: message.uid,\n messageId: message.messageId,\n subject: message.subject,\n from: message.from,\n to: message.to,\n date: message.date,\n unread: !message.read,\n flagged: false,\n hasAttachments: false,\n snippet: '',\n size: message.size,\n mailbox,\n backedUp,\n }\n}\n\nfunction serverDetail(\n runtime: MailHarborRuntime,\n account: Account,\n message: {\n uid: number\n messageId: string\n subject: string\n from: Address\n to: Address[]\n cc: Address[]\n date: string\n read: boolean\n size: number\n text: string\n html: string | null\n attachments: AttachmentMeta[]\n inReplyTo: string\n references: string[]\n },\n mailbox: string,\n): MessageDetail {\n let backedUp = false\n try {\n backedUp = message.messageId !== '' && runtime.database(account).hasMessageId(message.messageId)\n } catch {\n backedUp = false\n }\n return {\n id: String(message.uid),\n accountId: account.id,\n uid: message.uid,\n messageId: message.messageId,\n subject: message.subject,\n from: message.from,\n to: message.to,\n cc: message.cc,\n replyTo: [],\n date: message.date,\n unread: !message.read,\n flagged: false,\n hasAttachments: message.attachments.length > 0,\n snippet: '',\n size: message.size,\n mailbox,\n backedUp,\n text: message.text,\n html: message.html,\n attachments: message.attachments,\n inReplyTo: message.inReplyTo,\n references: message.references,\n }\n}\n\nasync function listServer(\n runtime: MailHarborRuntime,\n account: Account,\n mailbox: string,\n query: string,\n limit: number,\n offset = 0,\n): Promise<Omit<Page<MessageSummary>, 'offset' | 'limit'>> {\n const provider = runtime.provider(account)\n const result = await provider.list(mailbox === 'all' ? 'INBOX' : mailbox, Math.min(200, limit * 2), offset)\n let database: ReturnType<MailHarborRuntime['database']> | null = null\n try {\n database = runtime.database(account)\n } catch {\n database = null\n }\n const text = query.trim().toLowerCase()\n const items = result.items\n .filter((message) => {\n if (text === '') return true\n return [message.subject, message.from.name, message.from.address, formatAddressList(message.to)]\n .join(' ')\n .toLowerCase()\n .includes(text)\n })\n .slice(0, limit)\n .map((message) =>\n serverSummary(account, message, mailbox, database?.hasMessageId(message.messageId) ?? false),\n )\n return { items, total: result.total }\n}\n\nfunction writeAttachment(res: ServerResponse, filename: string, contentType: string, data: Buffer): void {\n res.statusCode = 200\n res.setHeader('content-type', contentType)\n res.setHeader('content-length', String(data.length))\n res.setHeader('content-disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`)\n res.setHeader('cache-control', 'no-store')\n res.setHeader('x-content-type-options', 'nosniff')\n res.end(data)\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n}\n\nfunction normalizeDraftInput(payload: Record<string, unknown>, accountId: string): Partial<Draft> {\n const attachments: DraftAttachment[] = Array.isArray(payload.attachments)\n ? payload.attachments\n .map((item) => asObject(item))\n .filter((item) => typeof item.filename === 'string' && typeof item.data === 'string')\n .map((item) => ({\n filename: String(item.filename),\n contentType: typeof item.contentType === 'string' ? item.contentType : 'application/octet-stream',\n data: String(item.data),\n }))\n : []\n return {\n ...(typeof payload.id === 'string' ? { id: payload.id } : {}),\n accountId,\n to: stringOf(payload.to),\n cc: stringOf(payload.cc),\n bcc: stringOf(payload.bcc),\n subject: stringOf(payload.subject),\n html: stringOf(payload.html),\n attachments,\n ...(typeof payload.inReplyTo === 'string' ? { inReplyTo: payload.inReplyTo } : {}),\n ...(typeof payload.references === 'string' ? { references: payload.references } : {}),\n }\n}\n\nfunction stringOf(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\nasync function sendFromPayload(\n runtime: MailHarborRuntime,\n account: Account,\n payload: Record<string, unknown>,\n): Promise<{ ok: true; result: SendResult }> {\n const to = stringOf(payload.to)\n if (to.trim() === '') throw new HttpError(400, '\u6536\u4EF6\u4EBA\u4E0D\u80FD\u4E3A\u7A7A')\n const subject = stringOf(payload.subject)\n const html = stringOf(payload.html)\n const text = stringOf(payload.text) !== '' ? stringOf(payload.text) : htmlToText(html)\n const attachments = Array.isArray(payload.attachments)\n ? payload.attachments\n .map((item) => asObject(item))\n .filter((item) => typeof item.filename === 'string' && typeof item.data === 'string')\n .map((item) => ({\n filename: String(item.filename),\n contentType:\n typeof item.contentType === 'string' && item.contentType !== ''\n ? item.contentType\n : 'application/octet-stream',\n content: Buffer.from(String(item.data), 'base64'),\n }))\n : []\n const totalBytes = attachments.reduce((sum, file) => sum + file.content.length, 0)\n if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, '\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB')\n\n const { sendMail } = await import('./smtp.js')\n const signature = runtime.current.signature\n const bodyText = signature === '' ? text : `${text}\\n\\n--\\n${signature}`\n const bodyHtml =\n signature === '' ? html : `${html}<p>--</p><p>${escapeHtml(signature).replaceAll('\\n', '<br>')}</p>`\n\n const result = await sendMail(\n account.smtp,\n runtime.credentialsFor(account),\n account.username || account.email,\n {\n to,\n cc: stringOf(payload.cc),\n subject,\n text: bodyText,\n html: bodyHtml,\n ...(typeof payload.inReplyTo === 'string' ? { inReplyTo: payload.inReplyTo } : {}),\n ...(typeof payload.references === 'string' ? { references: payload.references } : {}),\n attachments,\n },\n )\n return { ok: true, result }\n}\n\n/** Kept for the tools layer and tests. */\nexport { addressOf, parseAddressList, randomUUID, backups }\n\nfunction backups(account: Account, runtime: MailHarborRuntime): BackupStatus {\n return runtime.backupStatus(account)\n}\n\nexport { htmlToText, escapeHtml, BackupEngine }\n", "import { createHash, randomUUID } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Account, CalDavSettings, ImapSettings, NewAccountInput, SmtpSettings } from '../shared/types.js'\nimport { CONFIG_PATH, DATA_ROOT, accountAttachmentsDir, accountDataDir, accountMailDb } from './paths.js'\n\nexport interface AiSettings {\n enabled: boolean\n provider: string\n model: string\n styleSampleCount: number\n maxChars: number\n}\n\nexport interface ReadSettings {\n markOnOpen: boolean\n pollSeconds: number\n}\n\nexport interface RenderSettings {\n listLimit: number\n}\n\nexport interface ProjectSettings {\n /** Default parent directory for projects created from mail. */\n root: string\n}\n\nexport interface OAuthSettings {\n google: { clientId: string; redirectPort: number; scopes: string[] }\n microsoft: { clientId: string; scopes: string[] }\n}\n\n/** The resolved configuration the rest of the Host reads. */\nexport interface MailHarborConfig {\n dataDir: string\n configFile: string\n accounts: Account[]\n activeAccountId: string | null\n signature: string\n ai: AiSettings\n read: ReadSettings\n render: RenderSettings\n projects: ProjectSettings\n oauth: OAuthSettings\n /** Bumped whenever the file is written; caches key off this value. */\n revision: number\n}\n\nexport interface AccountRuntime {\n account: Account\n root: string\n mailDb: string\n attachments: string\n keychainService: string\n imap: ImapSettings\n smtp: SmtpSettings\n auth: Account['auth']\n}\n\nconst DEFAULTS = {\n signature: '',\n ai: {\n enabled: true,\n provider: 'deepseek-account',\n model: 'deepseek-flash',\n styleSampleCount: 100,\n maxChars: 40000,\n },\n read: { markOnOpen: true, pollSeconds: 60 },\n render: { listLimit: 40 },\n projects: { root: join(homedir(), 'work', 'projects') },\n oauth: {\n google: { clientId: '', redirectPort: 8765, scopes: ['https://mail.google.com/'] },\n microsoft: {\n clientId: '',\n scopes: [\n 'offline_access',\n 'https://outlook.office.com/IMAP.AccessAsUser.All',\n 'https://outlook.office.com/SMTP.Send',\n ],\n },\n },\n} satisfies Omit<MailHarborConfig, 'dataDir' | 'configFile' | 'accounts' | 'activeAccountId' | 'revision'>\n\nconst ACCOUNT_ID = /^[A-Za-z0-9_-]{1,64}$/\nconst EMAIL = /^[^@\\s]+@[^@\\s]+$/\n\nfunction stringOr(value: unknown, fallback: string): string {\n return typeof value === 'string' ? value : fallback\n}\n\nfunction intOr(value: unknown, fallback: number, min: number, max: number): number {\n const parsed = Number(value)\n if (!Number.isInteger(parsed) || parsed < min || parsed > max) return fallback\n return parsed\n}\n\nfunction hostOr(value: unknown, fallback: string): string {\n const text = stringOr(value, fallback).trim()\n return text === '' ? fallback : text\n}\n\n/** Deterministic, filesystem-safe account id derived from the address. */\nexport function deriveAccountId(email: string, existing: Iterable<string> = []): string {\n const taken = new Set(existing)\n const base = `acc_${createHash('sha1').update(email.trim().toLowerCase()).digest('hex').slice(0, 8)}`\n if (!taken.has(base)) return base\n for (let attempt = 0; attempt < 100; attempt += 1) {\n const candidate = `acc_${createHash('sha1').update(`${email}:${attempt}`).digest('hex').slice(0, 8)}`\n if (!taken.has(candidate)) return candidate\n }\n return `acc_${randomUUID().slice(0, 8)}`\n}\n\n/** Validate and fill in one account record. Throws on unusable input. */\nexport function normalizeAccount(\n input: Partial<Account> & { email?: string },\n index = 0,\n existing: string[] = [],\n): Account {\n const email = stringOr(input.email, '').trim()\n if (!EMAIL.test(email)) throw new Error('\u90AE\u7BB1\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E')\n const id =\n typeof input.id === 'string' && ACCOUNT_ID.test(input.id)\n ? input.id\n : deriveAccountId(email, [...existing, `index:${String(index)}`])\n const domain = email.split('@')[1] ?? 'example.com'\n const username = stringOr(input.username, email).trim() || email\n return {\n id,\n label: stringOr(input.label, email).trim() || email,\n email,\n username,\n imap: {\n host: hostOr(input.imap?.host, `imap.${domain}`),\n port: intOr(input.imap?.port, 993, 1, 65535),\n secure: input.imap?.secure !== false,\n },\n smtp: {\n host: hostOr(input.smtp?.host, `smtp.${domain}`),\n port: intOr(input.smtp?.port, 465, 1, 65535),\n secure: input.smtp?.secure !== false,\n fromName: stringOr(input.smtp?.fromName, ''),\n },\n keychainService: stringOr(input.keychainService, `dsh-mailharbor:${id}`),\n dataDir: stringOr(input.dataDir, `accounts/${id}`),\n auth:\n input.auth?.type === 'oauth2'\n ? { type: 'oauth2', provider: input.auth.provider }\n : { type: 'password' },\n caldav: normalizeCaldav(input.caldav),\n enabled: input.enabled !== false,\n createdAt: stringOr(input.createdAt, new Date().toISOString()),\n }\n}\n\nfunction normalizeCaldav(value: CalDavSettings | null | undefined): CalDavSettings | null {\n if (value === null || value === undefined) return null\n const url = stringOr(value.url, '').trim()\n if (url === '') return null\n return {\n url,\n username: stringOr(value.username, '').trim(),\n ...(typeof value.calendarHref === 'string' && value.calendarHref !== ''\n ? { calendarHref: value.calendarHref }\n : {}),\n }\n}\n\n/** Read the config file, tolerating a missing or partly broken file. */\nfunction readFileConfig(): Record<string, unknown> {\n try {\n if (!existsSync(CONFIG_PATH)) return {}\n const parsed: unknown = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'))\n return parsed !== null && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\n/** Resolve the on-disk file into the runtime configuration. */\nexport function loadConfig(): MailHarborConfig {\n const file = readFileConfig()\n const rawAccounts = Array.isArray(file.accounts) ? file.accounts : []\n const accounts: Account[] = []\n for (const [index, raw] of rawAccounts.entries()) {\n if (raw === null || typeof raw !== 'object') continue\n try {\n accounts.push(\n normalizeAccount(\n raw as Partial<Account>,\n index,\n accounts.map((account) => account.id),\n ),\n )\n } catch {\n // A broken record is skipped rather than breaking every other account.\n }\n }\n // Deduplicate by id; the first record wins.\n const seen = new Set<string>()\n const unique = accounts.filter((account) => {\n if (seen.has(account.id)) return false\n seen.add(account.id)\n return true\n })\n const requested = stringOr(file.activeAccountId, '')\n const activeAccountId = unique.some((account) => account.id === requested)\n ? requested\n : (unique.find((account) => account.enabled !== false)?.id ?? null)\n const aiRaw = (file.ai ?? {}) as Record<string, unknown>\n const readRaw = (file.read ?? {}) as Record<string, unknown>\n const renderRaw = (file.render ?? {}) as Record<string, unknown>\n const projectsRaw = (file.projects ?? {}) as Record<string, unknown>\n const oauthRaw = (file.oauth ?? {}) as Record<string, unknown>\n const googleRaw = (oauthRaw.google ?? {}) as Record<string, unknown>\n const microsoftRaw = (oauthRaw.microsoft ?? {}) as Record<string, unknown>\n return {\n dataDir: DATA_ROOT,\n configFile: CONFIG_PATH,\n accounts: unique,\n activeAccountId,\n signature: stringOr(file.signature, DEFAULTS.signature),\n ai: {\n enabled: aiRaw.enabled !== false,\n provider: stringOr(aiRaw.provider, DEFAULTS.ai.provider),\n model: stringOr(aiRaw.model, DEFAULTS.ai.model),\n styleSampleCount: intOr(aiRaw.styleSampleCount, DEFAULTS.ai.styleSampleCount, 10, 200),\n maxChars: intOr(aiRaw.maxChars, DEFAULTS.ai.maxChars, 1000, 200000),\n },\n read: {\n markOnOpen: readRaw.markOnOpen !== false,\n pollSeconds: intOr(readRaw.pollSeconds, DEFAULTS.read.pollSeconds, 15, 3600),\n },\n render: {\n listLimit: intOr(renderRaw.listLimit, DEFAULTS.render.listLimit, 10, 200),\n },\n projects: {\n root: stringOr(projectsRaw.root, DEFAULTS.projects.root),\n },\n oauth: {\n google: {\n clientId: stringOr(googleRaw.clientId, DEFAULTS.oauth.google.clientId),\n redirectPort: intOr(googleRaw.redirectPort, DEFAULTS.oauth.google.redirectPort, 1024, 65535),\n scopes: Array.isArray(googleRaw.scopes)\n ? googleRaw.scopes.filter((item): item is string => typeof item === 'string')\n : DEFAULTS.oauth.google.scopes,\n },\n microsoft: {\n clientId: stringOr(microsoftRaw.clientId, DEFAULTS.oauth.microsoft.clientId),\n scopes: Array.isArray(microsoftRaw.scopes)\n ? microsoftRaw.scopes.filter((item): item is string => typeof item === 'string')\n : DEFAULTS.oauth.microsoft.scopes,\n },\n },\n revision: Date.now(),\n }\n}\n\n/** Merge `patch` into the config file atomically and return the merged file. */\nexport function writeConfigFile(patch: Record<string, unknown>): Record<string, unknown> {\n const current = readFileConfig()\n const next = { ...current, ...patch }\n if (current.accounts !== undefined || patch.accounts !== undefined) {\n next.accounts = patch.accounts ?? current.accounts\n }\n mkdirSync(DATA_ROOT, { recursive: true })\n const temp = `${CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`\n writeFileSync(temp, `${JSON.stringify(next, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temp, CONFIG_PATH)\n return next\n}\n\n/** Persist the mutable settings the client may change. */\nexport function saveSettings(patch: {\n signature?: string\n ai?: Partial<AiSettings>\n read?: Partial<ReadSettings>\n render?: Partial<RenderSettings>\n}): void {\n writeConfigFile(patch as Record<string, unknown>)\n}\n\n/** Persist the full account list and the active account. */\nexport function saveAccounts(accounts: Account[], activeAccountId: string | null): void {\n writeConfigFile({ accounts, activeAccountId })\n}\n\n/** Build a new account record from client input. */\nexport function buildAccount(input: NewAccountInput, existing: Account[]): Account {\n const email = input.email.trim()\n if (!EMAIL.test(email)) throw new Error('\u90AE\u7BB1\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E')\n const id = deriveAccountId(\n email,\n existing.map((account) => account.id),\n )\n return normalizeAccount(\n {\n id,\n email,\n label: input.label,\n username: input.username,\n imap: input.imap as ImapSettings | undefined,\n smtp: input.smtp as SmtpSettings | undefined,\n auth: { type: 'password' },\n caldav: null,\n enabled: true,\n createdAt: new Date().toISOString(),\n },\n 0,\n existing.map((account) => account.id),\n )\n}\n\n/** Per-account runtime paths and connection settings. */\nexport function accountRuntime(account: Account): AccountRuntime {\n const root = accountDataDir(account)\n return {\n account,\n root,\n mailDb: accountMailDb(account),\n attachments: accountAttachmentsDir(account),\n keychainService: account.keychainService || `dsh-mailharbor:${account.id}`,\n imap: account.imap,\n smtp: account.smtp,\n auth: account.auth,\n }\n}\n\n/** Find an account by id, or throw a user-facing error. */\nexport function requireAccount(config: MailHarborConfig, id: string | null | undefined): Account {\n const account =\n config.accounts.find((item) => item.id === id) ??\n config.accounts.find((item) => item.id === config.activeAccountId)\n if (account === undefined) throw new Error('\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n return account\n}\n\n/** The active account, or null when no account exists yet. */\nexport function activeAccount(config: MailHarborConfig): Account | null {\n return config.accounts.find((account) => account.id === config.activeAccountId) ?? null\n}\n\n/** Directory used for one account's attachments. */\nexport function attachmentsRoot(account: Account): string {\n return join(accountDataDir(account), 'attachments')\n}\n", "import { homedir } from 'node:os'\nimport { isAbsolute, join } from 'node:path'\n\n/**\n * One data root per OS user. It lives beside the DSH home so the plugin behaves\n * the same on macOS, Windows and Linux. `DSH_MAILHARBOR_DATA_DIR` moves it.\n */\nexport const DATA_ROOT = process.env.DSH_MAILHARBOR_DATA_DIR ?? join(homedir(), '.dsh', 'mailharbor')\n\n/** Plugin state (config, drafts, read state, logs). */\nexport const CONFIG_PATH = process.env.DSH_MAILHARBOR_CONFIG ?? join(DATA_ROOT, 'config.json')\n\nexport const ACCOUNTS_DIR = join(DATA_ROOT, 'accounts')\nexport const STATE_DIR = join(DATA_ROOT, 'state')\nexport const REPORTS_DIR = join(DATA_ROOT, 'reports')\nexport const LOGS_DIR = join(DATA_ROOT, 'logs')\n\n/** Resolve `~`, reject relative paths, and normalize separators. */\nexport function resolveUserPath(value: string, label: string): string {\n const text = value.trim()\n if (text === '') throw new Error(`${label}\u4E0D\u80FD\u4E3A\u7A7A`)\n if (text.length > 4096) throw new Error(`${label}\u8FC7\u957F`)\n const expanded = text === '~' ? homedir() : text.startsWith('~/') ? join(homedir(), text.slice(2)) : text\n if (!isAbsolute(expanded)) throw new Error(`${label}\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84`)\n return expanded\n}\n\n/** Account directory names stay portable; IDs are validated by the config layer. */\nexport function accountRoot(id: string): string {\n const safe = id.replace(/[^A-Za-z0-9_-]/g, '_') || 'unknown'\n return join(ACCOUNTS_DIR, safe)\n}\n\n/** Absolute directory for an account record's data. */\nexport function accountDataDir(account: { id: string; dataDir?: string | null }): string {\n const configured = typeof account.dataDir === 'string' ? account.dataDir.trim() : ''\n if (configured === '') return accountRoot(account.id)\n return isAbsolute(configured) ? configured : join(DATA_ROOT, configured)\n}\n\nexport function accountMailDb(account: { id: string; dataDir?: string | null }): string {\n return join(accountDataDir(account), 'mail.db')\n}\n\nexport function accountAttachmentsDir(account: { id: string; dataDir?: string | null }): string {\n return join(accountDataDir(account), 'attachments')\n}\n", "import { execFile } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport { promisify } from 'node:util'\n\nconst run = promisify(execFile)\n\nasync function electronDialog(defaultPath?: string): Promise<string | null | undefined> {\n try {\n const require = createRequire(import.meta.url)\n const electron = require('electron') as {\n dialog?: {\n showOpenDialog(options: Record<string, unknown>): Promise<{ canceled: boolean; filePaths: string[] }>\n }\n }\n if (electron.dialog === undefined) return undefined\n const result = await electron.dialog.showOpenDialog({\n title: '\u9009\u62E9\u9879\u76EE\u76EE\u5F55',\n ...(defaultPath === undefined ? {} : { defaultPath }),\n properties: ['openDirectory', 'createDirectory'],\n })\n return result.canceled ? null : (result.filePaths[0] ?? null)\n } catch {\n return undefined\n }\n}\n\n/**\n * Native directory picker for the desktop app.\n *\n * Tries Electron's dialog first (the Host runs inside the desktop main\n * process), then the platform's own dialog. Returns null when the user\n * cancels and throws when no picker is available.\n */\nexport async function pickDirectory(defaultPath?: string): Promise<string | null> {\n const electron = await electronDialog(defaultPath)\n if (electron !== undefined) return electron\n\n if (process.platform === 'darwin') {\n try {\n const { stdout } = await run(\n 'osascript',\n ['-e', 'POSIX path of (choose folder with prompt \"\u9009\u62E9\u9879\u76EE\u76EE\u5F55\")'],\n { timeout: 180_000 },\n )\n const path = stdout.trim()\n return path === '' ? null : path.replace(/\\/$/, '')\n } catch {\n // Cancel exits non-zero; treat it as \"no selection\".\n return null\n }\n }\n\n if (process.platform === 'win32') {\n const script = [\n 'Add-Type -AssemblyName System.Windows.Forms',\n '$d = New-Object System.Windows.Forms.FolderBrowserDialog',\n '$d.Description = \"\u9009\u62E9\u9879\u76EE\u76EE\u5F55\"',\n 'if ($d.ShowDialog() -eq \"OK\") { Write-Output $d.SelectedPath }',\n ].join('; ')\n try {\n const { stdout } = await run('powershell', ['-NoProfile', '-Command', script], { timeout: 180_000 })\n return stdout.trim() === '' ? null : stdout.trim()\n } catch {\n return null\n }\n }\n\n try {\n const { stdout } = await run('zenity', ['--file-selection', '--directory', '--title=\u9009\u62E9\u9879\u76EE\u76EE\u5F55'], {\n timeout: 180_000,\n })\n return stdout.trim() === '' ? null : stdout.trim()\n } catch {\n throw new Error('\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u6253\u5F00\u76EE\u5F55\u9009\u62E9\u5668\uFF0C\u8BF7\u624B\u52A8\u8F93\u5165\u8DEF\u5F84')\n }\n}\n", "import { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { ImapFlow } from 'imapflow'\nimport { simpleParser } from 'mailparser'\nimport type { BackupStatus } from '../shared/types.js'\nimport { addressOf, formatAddressList, parseAddressList } from './address.js'\nimport type { AccountRuntime } from './config.js'\nimport { DatabaseSync } from 'node:sqlite'\nimport { ensureMailSchema } from './mail-db.js'\nimport type { ImapCredentials } from './imap.js'\n\nexport interface BackupEvent {\n mailbox?: string\n added?: number\n total?: number\n message: string\n}\n\nexport interface BackupResult {\n ok: true\n newCount: number\n failCount: number\n total: number\n mailboxes: { name: string; added: number }[]\n}\n\nexport interface BackupOptions {\n signal?: AbortSignal\n onEvent?: (event: BackupEvent) => void\n batchSize?: number\n}\n\nconst BATCH = 25\n\nfunction sha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex')\n}\n\nfunction normalizeId(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/^<|>$/g, '')\n .trim()\n}\n\n/** Local wall-clock stamp, matching what the previous backup tool wrote. */\nfunction stamp(value: unknown): string {\n const date =\n value instanceof Date\n ? value\n : value === undefined || value === null\n ? undefined\n : new Date(String(value))\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return ''\n const pad = (number: number): string => String(number).padStart(2, '0')\n return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`\n}\n\nfunction abortIfNeeded(signal: AbortSignal | undefined): void {\n if (signal?.aborted === true) {\n const error = new Error('\u5907\u4EFD\u5DF2\u53D6\u6D88')\n ;(error as { code?: string }).code = 'backup-cancelled'\n throw error\n }\n}\n\n/**\n * Incremental IMAP \u2192 SQLite backup for one account.\n *\n * It writes the same schema the previous tool created, so an existing database\n * keeps working. Messages are fetched by UID range (`lastUid+1:*`) instead of\n * enumerating every UID on each run, and each batch commits together with its\n * attachment files and search rows, so an interrupted run resumes cleanly.\n */\nexport class BackupEngine {\n private readonly runtime: AccountRuntime\n private readonly credentials: ImapCredentials\n readonly batchSize: number\n\n constructor(runtime: AccountRuntime, credentials: ImapCredentials, options: { batchSize?: number } = {}) {\n this.runtime = runtime\n this.credentials = credentials\n this.batchSize = options.batchSize ?? BATCH\n }\n\n private attachmentsDir(): string {\n return this.runtime.attachments\n }\n\n private openDb(): DatabaseSync {\n ensureMailSchema(this.runtime.mailDb)\n const db = new DatabaseSync(this.runtime.mailDb)\n db.exec('PRAGMA journal_mode=WAL')\n db.exec('PRAGMA busy_timeout=10000')\n db.exec('PRAGMA synchronous=NORMAL')\n return db\n }\n\n private writeAttachment(data: Buffer): { hash: string; size: number } {\n const hash = sha256(data)\n const dir = join(this.attachmentsDir(), hash.slice(0, 2), hash.slice(2, 4))\n const file = join(dir, hash)\n mkdirSync(dir, { recursive: true })\n if (!existsSync(file)) writeFileSync(file, data, { mode: 0o600 })\n return { hash, size: data.length }\n }\n\n private insertEmail(\n db: DatabaseSync,\n mailbox: string,\n message: RawBackupMessage,\n parsed: Awaited<ReturnType<typeof simpleParser>>,\n logId: number,\n ): number {\n const source = message.source ?? Buffer.alloc(0)\n const hash = sha256(source)\n const from = parsed.from?.value?.[0] ?? { name: '', address: '' }\n const messageId = normalizeId(parsed.messageId ?? message.envelope?.messageId)\n const toText =\n parsedAddressText(parsed.to) || formatAddressList(parseAddressList(addressTextOf(message.envelope?.to)))\n const ccText =\n parsedAddressText(parsed.cc) || formatAddressList(parseAddressList(addressTextOf(message.envelope?.cc)))\n const subject = String(parsed.subject ?? message.envelope?.subject ?? '(\u65E0\u4E3B\u9898)')\n const text = typeof parsed.text === 'string' ? parsed.text : ''\n const html = typeof parsed.html === 'string' ? parsed.html : ''\n const inserted = db\n .prepare(\n `INSERT OR IGNORE INTO emails\n (mailbox, uid, subject, from_name, from_addr, to_addr, cc, date, message_id,\n body_text, body_html, raw_eml, hash, backup_log_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n mailbox,\n Number(message.uid),\n subject,\n String(from.name ?? ''),\n String(from.address ?? ''),\n toText,\n ccText,\n stamp(parsed.date ?? message.internalDate ?? message.envelope?.date),\n messageId === '' ? null : messageId,\n text,\n html,\n source,\n hash,\n logId,\n )\n if (inserted.changes === 0) return 0\n const emailId = Number(inserted.lastInsertRowid)\n const attachments = parsed.attachments ?? []\n const insertAttachment = db.prepare(\n 'INSERT INTO attachments (email_id, filename, content_type, size, hash) VALUES (?, ?, ?, ?, ?)',\n )\n for (const attachment of attachments) {\n const data = attachment.content\n if (data === undefined || data === null) continue\n const stored = this.writeAttachment(Buffer.isBuffer(data) ? data : Buffer.from(data))\n insertAttachment.run(\n emailId,\n String(attachment.filename ?? 'attachment'),\n String(attachment.contentType ?? 'application/octet-stream'),\n stored.size,\n stored.hash,\n )\n }\n try {\n db.prepare(\n `INSERT INTO emails_fts (rowid, subject, from_name, from_addr, to_addr, cc, body_text, att_filename)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n emailId,\n subject,\n String(from.name ?? ''),\n String(from.address ?? ''),\n toText,\n ccText,\n text,\n attachments\n .map((attachment) => attachment.filename)\n .filter(Boolean)\n .join(' '),\n )\n } catch {\n // Search index is best effort; the message itself is stored.\n }\n return 1\n }\n\n async run(options: BackupOptions = {}): Promise<BackupResult> {\n const emit = (event: BackupEvent): void => options.onEvent?.(event)\n const signal = options.signal\n const db = this.openDb()\n const startedAt = new Date().toISOString()\n const logId = Number(\n db.prepare(`INSERT INTO backup_logs (started_at, status) VALUES (?, 'running')`).run(startedAt)\n .lastInsertRowid,\n )\n const client = new ImapFlow({\n host: this.runtime.imap.host,\n port: Number(this.runtime.imap.port),\n secure: this.runtime.imap.secure !== false,\n auth:\n this.credentials.accessToken !== undefined\n ? { user: this.credentials.user, accessToken: this.credentials.accessToken }\n : { user: this.credentials.user, pass: this.credentials.pass ?? '' },\n logger: false,\n tls: { rejectUnauthorized: true },\n connectionTimeout: 20_000,\n greetingTimeout: 20_000,\n socketTimeout: 120_000,\n })\n client.on('error', () => {})\n let newCount = 0\n let failCount = 0\n const mailboxes: { name: string; added: number }[] = []\n emit({ message: `\u5F00\u59CB\u5907\u4EFD ${this.runtime.account.email}` })\n try {\n await client.connect()\n const boxes = await client.list()\n for (const box of boxes) {\n abortIfNeeded(signal)\n if (box.flags?.has?.('\\\\Noselect') === true) continue\n const path = box.path\n emit({ mailbox: path, message: `\u68C0\u67E5 ${path}` })\n const lock = await client.getMailboxLock(path)\n let boxAdded = 0\n try {\n const uidValidity = String(\n (client.mailbox as { uidValidity?: bigint | number } | undefined)?.uidValidity ?? '',\n )\n let state = db\n .prepare('SELECT uid_validity, last_uid FROM backup_state WHERE mailbox = ?')\n .get(path) as { uid_validity?: string; last_uid?: number } | undefined\n if (state === undefined) {\n const dbMax = Number(\n db.prepare('SELECT MAX(uid) AS maxUid FROM emails WHERE mailbox = ?').get(path)?.maxUid ?? 0,\n )\n this.saveState(db, path, uidValidity, dbMax)\n state = { uid_validity: uidValidity, last_uid: dbMax }\n } else if (uidValidity !== '' && String(state.uid_validity ?? '') !== uidValidity) {\n this.saveState(db, path, uidValidity, 0)\n state = { uid_validity: uidValidity, last_uid: 0 }\n emit({ mailbox: path, message: `${path} \u7684 UIDVALIDITY \u53D8\u5316\uFF0C\u91CD\u65B0\u540C\u6B65` })\n }\n const lastUid = Number(state.last_uid ?? 0)\n // Only ask the server for UIDs newer than the last committed one.\n const found = await client.search({ uid: `${String(lastUid + 1)}:*` }, { uid: true })\n const uids = (Array.isArray(found) ? found : [])\n .filter((uid) => Number(uid) > lastUid)\n .sort((left, right) => left - right)\n let committedUid = lastUid\n for (let offset = 0; offset < uids.length; offset += this.batchSize) {\n abortIfNeeded(signal)\n const batch = uids.slice(offset, offset + this.batchSize)\n db.exec('BEGIN')\n try {\n let batchLastUid = committedUid\n for await (const message of client.fetch(\n batch.join(','),\n { uid: true, source: true, flags: true, internalDate: true },\n { uid: true },\n )) {\n abortIfNeeded(signal)\n try {\n const parsed = await simpleParser(message.source ?? Buffer.alloc(0))\n const added = this.insertEmail(\n db,\n path,\n message as unknown as RawBackupMessage,\n parsed,\n logId,\n )\n boxAdded += added\n newCount += added\n batchLastUid = Math.max(batchLastUid, Number(message.uid))\n } catch (error) {\n failCount += 1\n emit({\n mailbox: path,\n message: `\u90AE\u4EF6 ${String(message.uid)} \u89E3\u6790\u5931\u8D25: ${messageOf(error)}`,\n })\n }\n }\n this.saveState(db, path, uidValidity, batchLastUid)\n db.exec('COMMIT')\n committedUid = batchLastUid\n emit({ mailbox: path, added: boxAdded, message: `${path}: \u5DF2\u65B0\u589E ${String(boxAdded)} \u5C01` })\n } catch (error) {\n db.exec('ROLLBACK')\n throw error\n }\n }\n mailboxes.push({ name: path, added: boxAdded })\n emit({ mailbox: path, added: boxAdded, message: `${path}: \u65B0\u589E ${String(boxAdded)} \u5C01` })\n } finally {\n lock.release()\n }\n }\n const total = Number(db.prepare('SELECT COUNT(*) AS n FROM emails').get()?.n ?? 0)\n db.prepare(\n `UPDATE backup_logs SET completed_at = ?, mailboxes = ?, new_count = ?, fail_count = ?,\n total_emails = ?, status = 'completed' WHERE id = ?`,\n ).run(new Date().toISOString(), JSON.stringify(mailboxes), newCount, failCount, total, logId)\n emit({\n total,\n added: newCount,\n message: `\u5907\u4EFD\u5B8C\u6210\uFF1A\u65B0\u589E ${String(newCount)} \u5C01\uFF0C\u5171 ${String(total)} \u5C01`,\n })\n return { ok: true, newCount, failCount, total, mailboxes }\n } catch (error) {\n const total = Number(db.prepare('SELECT COUNT(*) AS n FROM emails').get()?.n ?? 0)\n db.prepare(\n `UPDATE backup_logs SET completed_at = ?, mailboxes = ?, new_count = ?, fail_count = ?,\n total_emails = ?, status = 'failed', error_info = ? WHERE id = ?`,\n ).run(\n new Date().toISOString(),\n JSON.stringify(mailboxes),\n newCount,\n failCount,\n total,\n messageOf(error).slice(0, 1000),\n logId,\n )\n throw error\n } finally {\n try {\n await client.logout()\n } catch {\n // Already closed.\n }\n db.close()\n }\n }\n\n private saveState(db: DatabaseSync, mailbox: string, uidValidity: string, lastUid: number): void {\n db.prepare(\n `INSERT INTO backup_state (mailbox, uid_validity, last_uid, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(mailbox) DO UPDATE SET uid_validity = excluded.uid_validity,\n last_uid = excluded.last_uid, updated_at = excluded.updated_at`,\n ).run(mailbox, uidValidity, Number(lastUid), new Date().toISOString())\n }\n\n /** Latest backup summary for the settings page. */\n static status(dbFile: string): BackupStatus {\n const empty: BackupStatus = {\n running: false,\n accountId: null,\n startedAt: null,\n finishedAt: null,\n lastSuccessAt: null,\n total: 0,\n added: 0,\n message: '',\n output: [],\n exitCode: null,\n }\n if (!existsSync(dbFile)) return empty\n try {\n const db = new DatabaseSync(dbFile, { readOnly: true })\n try {\n const latest = db.prepare('SELECT * FROM backup_logs ORDER BY id DESC LIMIT 1').get()\n const success = db\n .prepare(\"SELECT completed_at FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1\")\n .get()\n const total = Number(db.prepare('SELECT COUNT(*) AS n FROM emails').get()?.n ?? 0)\n return {\n ...empty,\n running: latest?.status === 'running',\n startedAt: latest?.started_at === undefined ? null : String(latest.started_at),\n finishedAt: latest?.completed_at === undefined ? null : String(latest.completed_at),\n lastSuccessAt: success?.completed_at === undefined ? null : String(success.completed_at),\n total,\n added: Number(latest?.new_count ?? 0),\n message: latest?.status === 'failed' ? String(latest?.error_info ?? '\u5907\u4EFD\u5931\u8D25') : '',\n exitCode: latest?.status === 'failed' ? 1 : 0,\n }\n } finally {\n db.close()\n }\n } catch {\n return empty\n }\n }\n}\n\ninterface RawBackupMessage {\n uid: number\n source?: Buffer\n envelope?: {\n messageId?: string\n subject?: string\n from?: { name?: string; address?: string }[]\n to?: { name?: string; address?: string }[]\n cc?: { name?: string; address?: string }[]\n date?: string | Date\n }\n internalDate?: string | Date\n}\n\n/** Format `parsed.to`/`parsed.cc`, which is an address object or a list. */\nfunction parsedAddressText(value: unknown): string {\n if (value === undefined || value === null) return ''\n const items = Array.isArray(value) ? value : [value]\n return formatAddressList(\n items\n .map((item) => ({\n name: String((item as { name?: string }).name ?? ''),\n address: String((item as { address?: string }).address ?? ''),\n }))\n .filter((address) => address.address !== ''),\n )\n}\n\nfunction addressTextOf(list: unknown[] | undefined): string {\n return (list ?? [])\n .map((item) => {\n const entry = item as { name?: string; address?: string }\n return entry.name ? `${entry.name} <${String(entry.address ?? '')}>` : String(entry.address ?? '')\n })\n .filter((text) => text !== '')\n .join(', ')\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nexport { addressOf }\n", "import { existsSync, readFileSync } from 'node:fs'\nimport { DatabaseSync } from 'node:sqlite'\nimport type { AttachmentMeta, MessageDetail, MessageSummary, Page } from '../shared/types.js'\nimport { parseAddressList } from './address.js'\n\n/** The layout the backup engine writes; read here by the panel and tools. */\nconst SCHEMA = `\n CREATE TABLE IF NOT EXISTS emails (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n mailbox TEXT NOT NULL,\n uid INTEGER,\n subject TEXT,\n from_name TEXT,\n from_addr TEXT,\n to_addr TEXT,\n cc TEXT,\n date TEXT,\n message_id TEXT UNIQUE,\n body_text TEXT,\n body_html TEXT,\n raw_eml BLOB,\n hash TEXT,\n backup_log_id INTEGER,\n created_at TEXT DEFAULT (datetime('now'))\n );\n CREATE TABLE IF NOT EXISTS attachments (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n email_id INTEGER NOT NULL,\n filename TEXT NOT NULL,\n content_type TEXT,\n size INTEGER,\n data BLOB,\n hash TEXT\n );\n CREATE TABLE IF NOT EXISTS backup_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n mailboxes TEXT,\n new_count INTEGER DEFAULT 0,\n fail_count INTEGER DEFAULT 0,\n total_emails INTEGER DEFAULT 0,\n cumulative_hash TEXT,\n status TEXT DEFAULT 'running',\n error_info TEXT\n );\n CREATE TABLE IF NOT EXISTS backup_state (\n mailbox TEXT PRIMARY KEY,\n uid_validity TEXT,\n last_uid INTEGER,\n updated_at TEXT\n );\n`\n\nconst INDEXES = `\n CREATE INDEX IF NOT EXISTS idx_emails_mailbox_date ON emails(mailbox, date);\n CREATE INDEX IF NOT EXISTS idx_emails_date ON emails(date);\n CREATE INDEX IF NOT EXISTS idx_attachments_email ON attachments(email_id);\n`\n\nconst FTS = `CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(\n subject, from_name, from_addr, to_addr, cc, body_text, att_filename,\n content='', tokenize='trigram'\n)`\n\n/** Create the mail database layout, including the search index when available. */\nexport function ensureMailSchema(file: string): void {\n const db = new DatabaseSync(file)\n try {\n db.exec('PRAGMA journal_mode=WAL')\n db.exec(SCHEMA)\n db.exec(INDEXES)\n try {\n db.exec(FTS)\n } catch {\n // A build without FTS5 still backs up mail; search falls back to LIKE.\n }\n } finally {\n db.close()\n }\n}\n\nexport interface ListQuery {\n mailbox?: string\n query?: string\n offset?: number\n limit?: number\n since?: string\n order?: 'newest' | 'oldest'\n}\n\nexport interface MailDatabaseOptions {\n /** Hard cap on stored body text returned to the reader. */\n maxBodyChars?: number\n /** Root that holds content-addressed attachment files. */\n attachmentsRoot?: string\n}\n\n/**\n * Read-only view over one account's mail backup.\n *\n * The backup engine owns the file; this class never writes to it and opens it\n * with SQLITE_OPEN_READONLY. A single connection is reused because opening a\n * large database per request is wasteful.\n */\nexport class MailDatabase {\n private readonly file: string\n private readonly options: MailDatabaseOptions\n private db: DatabaseSync | undefined\n private ftsChecked = false\n private ftsAvailable = false\n\n constructor(file: string, options: MailDatabaseOptions = {}) {\n this.file = file\n this.options = options\n }\n\n get path(): string {\n return this.file\n }\n\n isOpen(): boolean {\n return this.db !== undefined\n }\n\n open(): DatabaseSync {\n if (this.db !== undefined) return this.db\n if (!existsSync(this.file)) throw new Error(`\u90AE\u4EF6\u6570\u636E\u5E93\u4E0D\u5B58\u5728: ${this.file}`)\n this.db = new DatabaseSync(this.file, { readOnly: true })\n this.db.exec('PRAGMA busy_timeout = 3000')\n return this.db\n }\n\n close(): void {\n if (this.db === undefined) return\n try {\n this.db.close()\n } catch {\n // Closing an already-closed handle is harmless.\n }\n this.db = undefined\n }\n\n status(): { ok: boolean; database: string; total: number; error?: string } {\n try {\n const row = this.open().prepare('SELECT COUNT(*) AS count FROM emails').get()\n return { ok: true, database: this.file, total: Number(row?.count ?? 0) }\n } catch (error) {\n return { ok: false, database: this.file, total: 0, error: messageOf(error) }\n }\n }\n\n /** Per-mailbox counts, largest first. */\n mailboxes(): { name: string; count: number }[] {\n const rows = this.open()\n .prepare('SELECT mailbox, COUNT(*) AS count FROM emails GROUP BY mailbox ORDER BY count DESC')\n .all()\n return rows.map((row) => ({ name: String(row.mailbox), count: Number(row.count ?? 0) }))\n }\n\n private supportsFts(): boolean {\n if (this.ftsChecked) return this.ftsAvailable\n this.ftsChecked = true\n try {\n const row = this.open()\n .prepare(\"SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'emails_fts'\")\n .get()\n this.ftsAvailable = row !== undefined\n } catch {\n this.ftsAvailable = false\n }\n return this.ftsAvailable\n }\n\n list(query: ListQuery = {}): Page<MessageSummary> {\n const db = this.open()\n const mailbox = String(query.mailbox ?? 'all')\n const offset = Math.max(0, Math.trunc(query.offset ?? 0))\n const limit = Math.min(500, Math.max(1, Math.trunc(query.limit ?? 40)))\n const where: string[] = []\n const params: (string | number)[] = []\n if (mailbox !== '' && mailbox !== 'all') {\n where.push('e.mailbox = ?')\n params.push(mailbox)\n }\n if (typeof query.since === 'string' && query.since !== '') {\n where.push('e.date >= ?')\n params.push(query.since)\n }\n const text = String(query.query ?? '').trim()\n if (text !== '') {\n const tokens = text.split(/\\s+/).filter(Boolean)\n const short = tokens.some((token) => token.length < 3)\n if (!short && this.supportsFts()) {\n const like = `%${text}%`\n where.push(\n '(e.id IN (SELECT rowid FROM emails_fts WHERE emails_fts MATCH ?) OR e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ?)',\n )\n params.push(tokens.map(quoteMatch).join(' AND '), like, like, like, like)\n } else {\n for (const token of tokens) {\n const like = `%${token}%`\n where.push(\n '(e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ? OR e.body_text LIKE ?)',\n )\n params.push(like, like, like, like, like)\n }\n }\n }\n const clause = where.length === 0 ? '' : `WHERE ${where.join(' AND ')}`\n const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,\n e.message_id, e.date, length(e.body_text) AS text_length,\n (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,\n substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`\n const total = Number(\n db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0,\n )\n const direction = query.order === 'oldest' ? 'ASC' : 'DESC'\n const rows = db\n .prepare(\n `${columns} FROM emails e ${clause} ORDER BY e.date ${direction}, e.id ${direction} LIMIT ? OFFSET ?`,\n )\n .all(...params, limit, offset)\n return { items: rows.map((row) => rowToSummary(row as Record<string, unknown>)), total, offset, limit }\n }\n\n /** Structured search used by AI search (keywords + sender + dates). */\n searchAdvanced(options: {\n mailbox?: string\n keywords?: string[]\n from?: string\n subject?: string\n since?: string\n before?: string\n limit?: number\n }): Page<MessageSummary> {\n const db = this.open()\n const where: string[] = []\n const params: (string | number)[] = []\n const mailbox = String(options.mailbox ?? 'all')\n if (mailbox !== '' && mailbox !== 'all') {\n where.push('e.mailbox = ?')\n params.push(mailbox)\n }\n const keywords = (options.keywords ?? []).filter((keyword) => keyword !== '')\n if (keywords.length > 0) {\n const clauses: string[] = []\n for (const keyword of keywords) {\n clauses.push(\n '(e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ? OR e.body_text LIKE ?)',\n )\n const like = `%${keyword}%`\n params.push(like, like, like, like, like)\n }\n where.push(`(${clauses.join(' OR ')})`)\n }\n if ((options.from ?? '') !== '') {\n const like = `%${String(options.from)}%`\n where.push('(e.from_name LIKE ? OR e.from_addr LIKE ?)')\n params.push(like, like)\n }\n if ((options.subject ?? '') !== '') {\n where.push('e.subject LIKE ?')\n params.push(`%${String(options.subject)}%`)\n }\n if ((options.since ?? '') !== '') {\n where.push('substr(e.date, 1, 10) >= ?')\n params.push(String(options.since))\n }\n if ((options.before ?? '') !== '') {\n where.push('substr(e.date, 1, 10) <= ?')\n params.push(String(options.before))\n }\n const limit = Math.min(100, Math.max(1, options.limit ?? 60))\n if (where.length === 0) return { items: [], total: 0, offset: 0, limit }\n const clause = `WHERE ${where.join(' AND ')}`\n const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,\n e.message_id, e.date, length(e.body_text) AS text_length,\n (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,\n substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`\n const total = Number(\n db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0,\n )\n const rows = db\n .prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`)\n .all(...params, limit)\n return { items: rows.map((row) => rowToSummary(row as Record<string, unknown>)), total, offset: 0, limit }\n }\n\n get(localId: number): MessageDetail | undefined {\n const db = this.open()\n const row = db\n .prepare(\n `SELECT id, mailbox, uid, subject, from_name, from_addr, to_addr, cc, date, message_id,\n body_text, body_html\n FROM emails WHERE id = ?`,\n )\n .get(localId)\n if (row === undefined) return undefined\n const attachments = db\n .prepare('SELECT id, filename, content_type, size FROM attachments WHERE email_id = ? ORDER BY id')\n .all(localId)\n .map((item: Record<string, unknown>): AttachmentMeta => ({\n id: String(item.id),\n filename: String(item.filename ?? 'attachment'),\n contentType: String(item.content_type ?? 'application/octet-stream'),\n size: Number(item.size ?? 0),\n }))\n const max = this.options.maxBodyChars ?? 400_000\n const bodyText = String(row.body_text ?? '')\n const bodyHtml = String(row.body_html ?? '')\n return {\n ...rowToSummary(row),\n cc: parseAddressList(String(row.cc ?? '')),\n replyTo: [],\n text: truncate(bodyText, max),\n html: bodyHtml === '' ? null : truncate(bodyHtml, max * 2),\n attachments,\n references: [],\n }\n }\n\n /** Read one attachment's bytes; the payload may live in a file (content-addressed). */\n attachment(\n emailId: number,\n attachmentId: number,\n ): { filename: string; contentType: string; data: Uint8Array } | undefined {\n const db = this.open()\n const row = db\n .prepare('SELECT filename, content_type, data, hash FROM attachments WHERE id = ? AND email_id = ?')\n .get(attachmentId, emailId)\n if (row === undefined) return undefined\n let data: Uint8Array | null = row.data instanceof Uint8Array ? row.data : null\n if (data === null && typeof row.hash === 'string' && row.hash !== '') {\n const root = this.options.attachmentsRoot ?? ''\n if (root !== '') {\n const file = `${root}/${row.hash.slice(0, 2)}/${row.hash.slice(2, 4)}/${row.hash}`\n if (existsSync(file)) data = readFileSyncSafe(file)\n }\n }\n if (data === null) return undefined\n return {\n filename: String(row.filename ?? 'attachment'),\n contentType: String(row.content_type ?? 'application/octet-stream'),\n data,\n }\n }\n\n /** Locate a local row by Message-ID, for links from tasks and projects. */\n findByMessageId(messageId: string): { id: number; mailbox: string } | undefined {\n const id = messageId.trim().replace(/^<|>$/g, '')\n if (id === '') return undefined\n const row = this.open().prepare('SELECT id, mailbox FROM emails WHERE message_id = ? LIMIT 1').get(id)\n if (row === undefined) return undefined\n return { id: Number(row.id), mailbox: String(row.mailbox ?? '') }\n }\n\n /** Whether the backup already holds a message with this Message-ID. */\n hasMessageId(messageId: string): boolean {\n const id = messageId.trim().replace(/^<|>$/g, '')\n if (id === '') return false\n try {\n return (\n this.open().prepare('SELECT 1 AS found FROM emails WHERE message_id = ? LIMIT 1').get(id) !==\n undefined\n )\n } catch {\n return false\n }\n }\n\n /** Recent sent messages, for learning the user's writing style. */\n sentMessages(limit = 100): { subject: string; text: string }[] {\n const rows = this.open()\n .prepare(\n `SELECT subject, body_text FROM emails\n WHERE lower(mailbox) LIKE '%sent%' OR mailbox LIKE '%\u5DF2\u53D1\u9001%'\n ORDER BY date DESC, id DESC LIMIT ?`,\n )\n .all(limit)\n return rows.map((row) => ({\n subject: String(row.subject ?? ''),\n text: String(row.body_text ?? ''),\n }))\n }\n\n /** Latest backup runs, for the settings page. */\n backupSummary(): {\n ok: boolean\n lastCompleted: Record<string, unknown> | null\n latest: Record<string, unknown> | null\n total: number\n error?: string\n } {\n try {\n const db = this.open()\n const lastCompleted =\n db\n .prepare(\n `SELECT id, started_at, completed_at, new_count, total_emails, status\n FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1`,\n )\n .get() ?? null\n const latest =\n db\n .prepare(\n 'SELECT id, started_at, completed_at, new_count, total_emails, status FROM backup_logs ORDER BY id DESC LIMIT 1',\n )\n .get() ?? null\n return {\n ok: true,\n lastCompleted: toPlain(lastCompleted),\n latest: toPlain(latest),\n total: Number(db.prepare('SELECT COUNT(*) AS count FROM emails').get()?.count ?? 0),\n }\n } catch (error) {\n return { ok: false, lastCompleted: null, latest: null, total: 0, error: messageOf(error) }\n }\n }\n}\n\nfunction toPlain(row: unknown): Record<string, unknown> | null {\n if (row === undefined || row === null || typeof row !== 'object') return null\n return { ...(row as Record<string, unknown>) }\n}\n\nfunction readFileSyncSafe(file: string): Uint8Array | null {\n try {\n return readFileSync(file)\n } catch {\n return null\n }\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nfunction truncate(value: string, max: number): string {\n return value.length <= max ? value : `${value.slice(0, max)}\\n\\n\u2026\uFF08\u5DF2\u622A\u65AD\uFF09`\n}\n\nfunction quoteMatch(text: string): string {\n return `\"${text.replaceAll('\"', '\"\"')}\"`\n}\n\nfunction rowToSummary(row: Record<string, unknown>): MessageSummary {\n const fromName = String(row.from_name ?? '')\n const fromAddr = String(row.from_addr ?? '')\n return {\n id: String(row.id),\n accountId: '',\n uid: row.uid === null || row.uid === undefined ? undefined : Number(row.uid),\n messageId: String(row.message_id ?? ''),\n subject: String(row.subject ?? '(\u65E0\u4E3B\u9898)'),\n from: { name: fromName, address: fromAddr },\n to: parseAddressList(String(row.to_addr ?? '')),\n date: String(row.date ?? ''),\n unread: false,\n flagged: false,\n hasAttachments: Number(row.attachment_count ?? 0) > 0,\n snippet: String(row.snippet ?? '')\n .replace(/\\s+/g, ' ')\n .trim(),\n mailbox: String(row.mailbox ?? ''),\n }\n}\n", "import type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport class HttpError extends Error {\n readonly status: number\n readonly code: string | undefined\n\n constructor(status: number, message: string, code?: string) {\n super(message)\n this.status = status\n this.code = code\n }\n}\n\nexport function sendJson(res: ServerResponse, status: number, payload: unknown): void {\n const body = Buffer.from(JSON.stringify(payload), 'utf8')\n res.statusCode = status\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('content-length', String(body.length))\n res.setHeader('cache-control', 'no-store')\n res.setHeader('x-content-type-options', 'nosniff')\n res.end(body)\n}\n\nexport function sendError(res: ServerResponse, error: unknown): void {\n const status = error instanceof HttpError ? error.status : 500\n const message = error instanceof Error ? error.message : String(error)\n const code = error instanceof HttpError ? error.code : undefined\n sendJson(res, status, { error: message, ...(code === undefined ? {} : { code }) })\n}\n\n/** Read a JSON request body with a hard size cap. */\nexport async function readJsonBody(\n req: IncomingMessage,\n limit = 2 * 1024 * 1024,\n): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string)\n size += buffer.length\n if (size > limit) throw new HttpError(413, '\u8BF7\u6C42\u4F53\u8FC7\u5927')\n chunks.push(buffer)\n }\n if (size === 0) return {}\n try {\n const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new HttpError(400, '\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61')\n }\n return parsed as Record<string, unknown>\n } catch (error) {\n if (error instanceof HttpError) throw error\n throw new HttpError(400, '\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON')\n }\n}\n\n/**\n * Reject cross-site writes. The Host only listens on loopback, so a request\n * whose Origin is a web page on another site is a CSRF attempt; the desktop\n * shell uses the `dsh-app:` origin and is accepted explicitly.\n */\nexport function isTrustedRequest(req: IncomingMessage): boolean {\n const origin = req.headers.origin\n if (origin === undefined || origin === '') return true\n if (origin.startsWith('dsh-app://')) return true\n try {\n const parsed = new URL(origin)\n const host = req.headers.host ?? ''\n return parsed.host === host\n } catch {\n return false\n }\n}\n\nexport function assertTrusted(req: IncomingMessage): void {\n if (!isTrustedRequest(req)) throw new HttpError(403, '\u6765\u6E90\u4E0D\u88AB\u5141\u8BB8', 'forbidden')\n}\n\nexport interface RouteMatch {\n handler: RouteHandler\n params: Record<string, string>\n path: string\n}\n\nexport interface RequestContext {\n req: IncomingMessage\n res: ServerResponse\n url: URL\n params: Record<string, string>\n body: () => Promise<Record<string, unknown>>\n}\n\nexport type RouteHandler = (context: RequestContext) => Promise<unknown> | unknown\n\ninterface Route {\n method: string\n segments: string[]\n handler: RouteHandler\n}\n\n/** Tiny method + path router with `:param` segments. */\nexport class Router {\n private readonly routes: Route[] = []\n\n add(method: string, path: string, handler: RouteHandler): void {\n this.routes.push({\n method: method.toUpperCase(),\n segments: path.split('/').filter((segment) => segment !== ''),\n handler,\n })\n }\n\n get(path: string, handler: RouteHandler): void {\n this.add('GET', path, handler)\n }\n\n post(path: string, handler: RouteHandler): void {\n this.add('POST', path, handler)\n }\n\n patch(path: string, handler: RouteHandler): void {\n this.add('PATCH', path, handler)\n }\n\n put(path: string, handler: RouteHandler): void {\n this.add('PUT', path, handler)\n }\n\n delete(path: string, handler: RouteHandler): void {\n this.add('DELETE', path, handler)\n }\n\n match(method: string, pathname: string): RouteMatch | null {\n const parts = pathname.split('/').filter((segment) => segment !== '')\n for (const route of this.routes) {\n if (route.method !== method.toUpperCase()) continue\n if (route.segments.length !== parts.length) continue\n const params: Record<string, string> = {}\n let matched = true\n for (const [index, segment] of route.segments.entries()) {\n const actual = parts[index] ?? ''\n if (segment.startsWith(':')) {\n params[segment.slice(1)] = decodeURIComponent(actual)\n } else if (segment !== actual) {\n matched = false\n break\n }\n }\n if (matched) return { handler: route.handler, params, path: pathname }\n }\n return null\n }\n}\n", "import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { Account, MessageDetail } from '../shared/types.js'\nimport type { MailHarborConfig } from './config.js'\nimport type { DshLlm } from './dsh.js'\n\nexport interface StyleProfile {\n /** Free-form Chinese description of the user's writing habits. */\n profile: string\n updatedAt: string\n sampleCount: number\n}\n\nexport interface AiStatus {\n available: boolean\n enabled: boolean\n provider: string\n model: string\n styleUpdatedAt: string\n styleSampleCount: number\n error?: string\n}\n\nexport interface AiDeps {\n config: () => MailHarborConfig\n llm: () => DshLlm | undefined\n /** Directory that holds the account's mail data (style profile lives here). */\n accountRoot: (account: Account) => string\n}\n\nconst TIMEOUT_MS = 90_000\nconst MAX_PROMPT_CHARS = 40_000\n\nconst SUMMARY_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u7406\uFF0C\u670D\u52A1\u4E8E\u4E2D\u6587\u7528\u6237\u3002\u8BF7\u7528\u7B80\u4F53\u4E2D\u6587\u603B\u7ED3\u90AE\u4EF6\uFF0C\u4E25\u683C\u6309\u4E0B\u9762\u56DB\u6BB5\u8F93\u51FA\uFF0C\u4F7F\u7528\u7ED9\u5B9A\u7684\u5C0F\u6807\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9\uFF1A\n\n\u7ED3\u8BBA\n\uFF081-2 \u53E5\u8BDD\u8BF4\u660E\u8FD9\u5C01\u90AE\u4EF6\u662F\u4EC0\u4E48\u4E8B\uFF09\n\n\u9700\u8981\u4F60\u505A\u4EC0\u4E48\n\uFF08\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u9700\u64CD\u4F5C\u300D\uFF1B\u6709\u5C31\u5217 1-3 \u6761\uFF0C\u9879\u76EE\u7B26\u53F7\uFF09\n\n\u5173\u952E\u4FE1\u606F\n\uFF08\u65F6\u95F4\u3001\u91D1\u989D\u3001\u4EBA\u540D\u3001\u5355\u53F7\u3001\u5730\u5740\u7B49\uFF0C\u9879\u76EE\u7B26\u53F7\uFF1B\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u300D\uFF09\n\n\u5EFA\u8BAE\u56DE\u590D\u8981\u70B9\n\uFF081-3 \u6761\u4E2D\u6587\u8981\u70B9\uFF09\n\n\u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\uFF0C\u4E0D\u8981\u7F16\u9020\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B\u3002`\n\nconst REPLY_SYSTEM = (\n language: string,\n style: string,\n): string => `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u539F\u59CB\u90AE\u4EF6\u548C\u7528\u6237\u7684\u8981\u6C42\uFF0C\u5199\u4E00\u5C01\u53EF\u4EE5\u76F4\u63A5\u53D1\u9001\u7684\u56DE\u590D\u6B63\u6587\u3002\n\n\u8981\u6C42\uFF1A\n- \u8BED\u8A00\uFF1A${language === 'en' ? '\u82F1\u6587' : '\u7B80\u4F53\u4E2D\u6587'}\n- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48\n- \u8BED\u6C14\u4E13\u4E1A\u3001\u7B80\u6D01\u3001\u793C\u8C8C\uFF1B\u4E2D\u6587\u7528\u300C\u60A8\u300D\uFF0C\u82F1\u6587\u907F\u514D\u8FC7\u5EA6\u6B63\u5F0F\n- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB\uFF08\u79F0\u547C\u3001\u6B63\u6587\u3001\u7ED3\u5C3E\uFF09\uFF0C\u4E0D\u8981\u5F15\u53F7\u5305\u88F9\n${style === '' ? '' : `\\n\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A\\n${style}`}`\n\nconst TRANSLATE_SYSTEM = (\n target: string,\n): string => `\u4F60\u662F\u90AE\u4EF6\u7FFB\u8BD1\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7FFB\u8BD1\u6210${target === 'en' ? '\u82F1\u6587' : '\u7B80\u4F53\u4E2D\u6587'}\u3002\n\n\u89C4\u5219\uFF1A\n- \u53EA\u8F93\u51FA\u8BD1\u6587\uFF0C\u4E0D\u6DFB\u52A0\u89E3\u91CA\n- \u4FDD\u7559\u6BB5\u843D\u7ED3\u6784\n- \u5982\u679C\u6B63\u6587\u662F HTML\uFF0C\u4FDD\u7559\u539F\u6709\u6807\u7B7E\u3001\u94FE\u63A5\u548C\u56FE\u7247\uFF0C\u53EA\u7FFB\u8BD1\u53EF\u89C1\u6587\u5B57\n- \u4E13\u6709\u540D\u8BCD\u3001\u90AE\u7BB1\u5730\u5740\u3001\u94FE\u63A5\u3001\u5355\u53F7\u4FDD\u6301\u4E0D\u53D8`\n\nconst ASK_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u624B\u3002\u57FA\u4E8E\u7528\u6237\u63D0\u4F9B\u7684\u90AE\u4EF6\u548C\u5DF2\u6709\u7684 AI \u8BB0\u5F55\u56DE\u7B54\u95EE\u9898\u3002\n\n\u8981\u6C42\uFF1A\n- \u7528\u7B80\u4F53\u4E2D\u6587\u56DE\u7B54\uFF0C\u7B80\u6D01\u76F4\u63A5\n- \u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\u548C\u5DF2\u6709\u8BB0\u5F55\uFF0C\u4E0D\u786E\u5B9A\u5C31\u8BF4\u660E\u300C\u90AE\u4EF6\u91CC\u6CA1\u6709\u63D0\u5230\u300D\n- \u4E0D\u8981\u590D\u8FF0\u95EE\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B`\n\nconst COMPOSE_SYSTEM = (mode: 'draft' | 'polish' | 'shorten', language: string, style: string): string => {\n const shared = `- \u8BED\u8A00\uFF1A${language === 'en' ? '\u82F1\u6587' : '\u7B80\u4F53\u4E2D\u6587'}\\n- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48\\n- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB`\n const styleLine = style === '' ? '' : `\\n\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A\\n${style}`\n if (mode === 'polish') {\n return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6DA6\u8272\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\uFF0C\u4FDD\u6301\u539F\u610F\u548C\u4E8B\u5B9E\uFF0C\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u81EA\u7136\u3002\\n\\n\u8981\u6C42\uFF1A\\n${shared}${styleLine}`\n }\n if (mode === 'shorten') {\n return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7F29\u77ED\u5230\u7EA6\u4E00\u534A\uFF0C\u4FDD\u7559\u5173\u952E\u4FE1\u606F\u548C\u4E8B\u5B9E\u3002\\n\\n\u8981\u6C42\uFF1A\\n${shared}${styleLine}`\n }\n return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u7528\u6237\u7ED9\u51FA\u7684\u8981\u70B9\u5199\u4E00\u5C01\u90AE\u4EF6\u6B63\u6587\u3002\\n\\n\u8981\u6C42\uFF1A\\n${shared}${styleLine}`\n}\n\nconst SEARCH_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u641C\u7D22\u52A9\u624B\u3002\u628A\u7528\u6237\u7684\u4E00\u53E5\u8BDD\u641C\u7D22\u9700\u6C42\u7FFB\u8BD1\u6210\u7ED3\u6784\u5316\u6761\u4EF6\u3002\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u89E3\u91CA\u6216\u4EE3\u7801\u5757\uFF1A\n{\"keywords\": [\"\u5173\u952E\u8BCD\"], \"from\": \"\u53D1\u4EF6\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32\", \"subject\": \"\u4E3B\u9898\u5173\u952E\u8BCD\u6216\u7A7A\u5B57\u7B26\u4E32\", \"since\": \"YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32\", \"before\": \"YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32\", \"explanation\": \"\u4E00\u53E5\u4E2D\u6587\u8BF4\u660E\u4F60\u7406\u89E3\u6210\u4E86\u4EC0\u4E48\"}\n\n\u89C4\u5219\uFF1A\n- \u4ECA\u5929\u662F {{today}}\uFF0C\u76F8\u5BF9\u65F6\u95F4\uFF08\u4E0A\u5468\u3001\u4E0A\u4E2A\u6708\u3001\u6700\u8FD1\u4E09\u5929\uFF09\u6362\u7B97\u6210 since/before\n- keywords \u53EA\u653E\u6B63\u6587/\u4E3B\u9898\u91CC\u53EF\u80FD\u51FA\u73B0\u7684\u5B9E\u8BCD\uFF0C2-4 \u4E2A\u5373\u53EF\n- subject \u53EA\u5728\u7528\u6237\u660E\u786E\u8BF4\u201C\u4E3B\u9898\u662F/\u6807\u9898\u662F\u201D\u65F6\u586B\u5199\uFF0C\u5426\u5219\u7559\u7A7A\uFF1B\u4E0D\u8981\u628A\u666E\u901A\u5173\u952E\u8BCD\u585E\u8FDB subject\n- \u4E0D\u786E\u5B9A\u7684\u5B57\u6BB5\u7559\u7A7A\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u7F16\u9020\n- \u6CA1\u6709\u53EF\u7528\u6761\u4EF6\u65F6 keywords \u4E3A\u7A7A\u6570\u7EC4`\n\nconst TASK_SYSTEM = `\u4F60\u662F\u4EFB\u52A1\u63D0\u53D6\u52A9\u624B\u3002\u4ECE\u90AE\u4EF6\u4E2D\u63D0\u53D6\u9700\u8981\u7528\u6237\u5904\u7406\u7684\u5177\u4F53\u4E8B\u9879\u3002\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001Markdown \u6216\u4EE3\u7801\u5757\u6807\u8BB0\u3002\u6BCF\u4E2A\u5143\u7D20\uFF1A\n{\"title\": \"\u4EFB\u52A1\u6807\u9898\uFF08\u52A8\u8BCD\u5F00\u5934\uFF0C\u5C3D\u91CF\u5177\u4F53\uFF09\", \"due\": \"YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32\", \"priority\": 0 \u5230 9 \u7684\u6574\u6570, \"owner\": \"\u8D1F\u8D23\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32\", \"notes\": \"\u4E00\u53E5\u6765\u6E90\u4F9D\u636E\"}\n\n\u89C4\u5219\uFF1A\n- \u53EA\u63D0\u53D6\u90AE\u4EF6\u91CC\u660E\u786E\u8981\u6C42\u3001\u6216\u660E\u663E\u9700\u8981\u8DDF\u8FDB\u7684\u4E8B\u9879\n- \u622A\u6B62\u65F6\u95F4\u80FD\u786E\u5B9A\u624D\u586B\uFF0C\u5426\u5219\u7559\u7A7A\n- \u6CA1\u6709\u53EF\u6267\u884C\u4E8B\u9879\u65F6\u8F93\u51FA []\n- \u4E0D\u8981\u7F16\u9020\u90AE\u4EF6\u91CC\u6CA1\u6709\u7684\u4FE1\u606F`\n\nconst STYLE_SYSTEM = `\u4F60\u662F\u5199\u4F5C\u98CE\u683C\u5206\u6790\u5E08\u3002\u9605\u8BFB\u7528\u6237\u6700\u8FD1\u53D1\u51FA\u7684\u90AE\u4EF6\uFF0C\u7528\u7B80\u4F53\u4E2D\u6587\u5199\u4E00\u6BB5 150-300 \u5B57\u7684\u5199\u4F5C\u98CE\u683C\u6863\u6848\uFF0C\u8986\u76D6\uFF1A\n\u5E38\u7528\u79F0\u547C\u3001\u5F00\u5934\u65B9\u5F0F\u3001\u7ED3\u5C3E\u4E0E\u7F72\u540D\u3001\u8BED\u6C14\uFF08\u6B63\u5F0F/\u4EB2\u5207/\u76F4\u63A5\uFF09\u3001\u53E5\u5B50\u957F\u77ED\u3001\u5E38\u7528\u8868\u8FBE\u6216\u53E3\u5934\u7985\u3002\n\u53EA\u8F93\u51FA\u6863\u6848\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u6807\u9898\u3001\u5217\u8868\u7B26\u53F7\u6216\u5206\u6790\u8FC7\u7A0B\u3002`\n\nfunction stylePath(root: string): string {\n return join(root, 'style.json')\n}\n\nfunction readStyle(root: string): StyleProfile | null {\n try {\n const parsed: unknown = JSON.parse(readFileSync(stylePath(root), 'utf8'))\n if (parsed === null || typeof parsed !== 'object') return null\n const value = parsed as Partial<StyleProfile>\n if (typeof value.profile !== 'string' || value.profile.trim() === '') return null\n return {\n profile: value.profile,\n updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : '',\n sampleCount: typeof value.sampleCount === 'number' ? value.sampleCount : 0,\n }\n } catch {\n return null\n }\n}\n\nfunction writeStyle(root: string, profile: StyleProfile): void {\n const file = stylePath(root)\n mkdirSync(dirname(file), { recursive: true })\n const temp = `${file}.${String(process.pid)}.tmp`\n writeFileSync(temp, `${JSON.stringify(profile, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temp, file)\n}\n\nfunction clip(value: string, max = MAX_PROMPT_CHARS): string {\n return value.length <= max ? value : `${value.slice(0, max - 2000)}\\n\u2026\uFF08\u4E2D\u95F4\u7701\u7565\uFF09\u2026\\n${value.slice(-2000)}`\n}\n\n/** Plain-text body of a mail message, falling back from HTML. */\nfunction bodyText(message: MessageDetail): string {\n if (message.text.trim() !== '') return message.text\n return (message.html ?? '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6])>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .trim()\n}\n\nfunction contextOf(message: MessageDetail): string {\n const lines = [\n `\u4E3B\u9898: ${message.subject}`,\n `\u53D1\u4EF6\u4EBA: ${message.from.name !== '' ? `${message.from.name} <${message.from.address}>` : message.from.address}`,\n `\u6536\u4EF6\u4EBA: ${message.to.map((item) => (item.name !== '' ? `${item.name} <${item.address}>` : item.address)).join(', ')}`,\n ...(message.cc.length === 0 ? [] : [`\u6284\u9001: ${message.cc.map((item) => item.address).join(', ')}`]),\n `\u65F6\u95F4: ${message.date}`,\n '',\n clip(bodyText(message)),\n ]\n return lines.join('\\n')\n}\n\n/**\n * Reading and writing assistance backed by the Host's model service.\n *\n * The service never sends mail: it only produces text for the panel, and every\n * send still needs the user's click. Nothing is sent to the model when AI is\n * disabled or the composition has no model service.\n */\nexport class AiService {\n private readonly deps: AiDeps\n\n constructor(deps: AiDeps) {\n this.deps = deps\n }\n\n private llm(): DshLlm | undefined {\n try {\n return this.deps.llm()\n } catch {\n return undefined\n }\n }\n\n status(account: Account | null): AiStatus {\n const config = this.deps.config()\n const llm = this.llm()\n const style = account === null ? null : this.style(account)\n return {\n available: llm !== undefined,\n enabled: config.ai.enabled,\n provider: config.ai.provider,\n model: config.ai.model,\n styleUpdatedAt: style?.updatedAt ?? '',\n styleSampleCount: style?.sampleCount ?? 0,\n ...(llm === undefined ? { error: '\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1' } : {}),\n }\n }\n\n style(account: Account): StyleProfile | null {\n return readStyle(this.deps.accountRoot(account))\n }\n\n saveStyle(account: Account, profile: string): StyleProfile {\n const text = profile.trim()\n if (text === '') throw new Error('\u5199\u4F5C\u98CE\u683C\u4E0D\u80FD\u4E3A\u7A7A')\n if (text.length > 8000) throw new Error('\u5199\u4F5C\u98CE\u683C\u8FC7\u957F\uFF08\u6700\u591A 8000 \u5B57\uFF09')\n const previous = this.style(account)\n const next: StyleProfile = {\n profile: text,\n updatedAt: new Date().toISOString(),\n sampleCount: previous?.sampleCount ?? 0,\n }\n writeStyle(this.deps.accountRoot(account), next)\n return next\n }\n\n private requireReady(): { provider: string; model: string } {\n const config = this.deps.config()\n if (!config.ai.enabled) throw new Error('AI \u529F\u80FD\u5DF2\u5173\u95ED\uFF0C\u8BF7\u5230\u8BBE\u7F6E\u91CC\u5F00\u542F')\n const llm = this.llm()\n if (llm === undefined) throw new Error('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1')\n if (config.ai.provider === '' || config.ai.model === '') throw new Error('\u8BF7\u5148\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 AI \u6A21\u578B')\n return { provider: config.ai.provider, model: config.ai.model }\n }\n\n private async complete(system: string, user: string, signal?: AbortSignal): Promise<string> {\n const { provider, model } = this.requireReady()\n const llm = this.llm()\n if (llm === undefined) throw new Error('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1')\n const controller = new AbortController()\n const timer = setTimeout(() => {\n controller.abort()\n }, TIMEOUT_MS)\n const onAbort = (): void => {\n controller.abort()\n }\n signal?.addEventListener('abort', onAbort, { once: true })\n let text = ''\n try {\n const stream = llm.stream({\n provider,\n model,\n messages: [\n { role: 'system', content: [{ type: 'text', text: system }] },\n { role: 'user', content: [{ type: 'text', text: user }] },\n ],\n signal: controller.signal,\n })\n for await (const chunk of stream) {\n if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {\n text += chunk.text\n } else if (chunk.type === 'finish') {\n const reason = chunk.reason\n if (reason?.kind === 'error') {\n throw new Error(reason.failure?.message ?? reason.failure?.code ?? '\u6A21\u578B\u8C03\u7528\u5931\u8D25')\n }\n if (reason?.kind === 'aborted') throw new Error('\u6A21\u578B\u8C03\u7528\u5DF2\u53D6\u6D88')\n }\n }\n } finally {\n clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n }\n const trimmed = text.trim()\n if (trimmed === '') throw new Error('\u6A21\u578B\u6CA1\u6709\u8FD4\u56DE\u5185\u5BB9')\n return trimmed\n }\n\n async summarize(message: MessageDetail, signal?: AbortSignal): Promise<string> {\n return this.complete(SUMMARY_SYSTEM, contextOf(message), signal)\n }\n\n async reply(\n account: Account,\n message: MessageDetail,\n options: { instruction?: string; language?: 'zh' | 'en' } = {},\n signal?: AbortSignal,\n ): Promise<string> {\n const style = this.style(account)?.profile ?? ''\n const instruction = (options.instruction ?? '').trim()\n const user = [\n '\u539F\u59CB\u90AE\u4EF6\uFF1A',\n contextOf(message),\n '',\n instruction === '' ? '\u7528\u6237\u6CA1\u6709\u989D\u5916\u8981\u6C42\uFF0C\u8BF7\u5199\u4E00\u5C01\u5F97\u4F53\u7684\u56DE\u590D\u3002' : `\u7528\u6237\u7684\u8981\u6C42\uFF1A${instruction}`,\n ].join('\\n')\n return this.complete(REPLY_SYSTEM(options.language ?? 'zh', style), user, signal)\n }\n\n async translate(html: string, target: 'zh' | 'en', signal?: AbortSignal): Promise<string> {\n const text = html.trim()\n if (text === '') throw new Error('\u6B63\u6587\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u7FFB\u8BD1')\n return this.complete(TRANSLATE_SYSTEM(target), clip(text, 20_000), signal)\n }\n\n /** Free-form question about a mail, with up to three earlier AI outputs as context. */\n async ask(\n message: MessageDetail,\n question: string,\n history: string[],\n signal?: AbortSignal,\n ): Promise<string> {\n const prompt = (question ?? '').trim()\n if (prompt === '') throw new Error('\u95EE\u9898\u4E0D\u80FD\u4E3A\u7A7A')\n const earlier = history\n .filter((item) => item.trim() !== '')\n .slice(-3)\n .map((item, index) => `\u3010\u5DF2\u6709\u8BB0\u5F55 ${String(index + 1)}\u3011\\n${clip(item, 4000)}`)\n .join('\\n\\n')\n const user = [\n '\u90AE\u4EF6\uFF1A',\n contextOf(message),\n ...(earlier === '' ? [] : ['', '\u5DF2\u6709 AI \u8BB0\u5F55\uFF1A', earlier]),\n '',\n `\u7528\u6237\u7684\u95EE\u9898\uFF1A${prompt}`,\n ].join('\\n')\n return this.complete(ASK_SYSTEM, user, signal)\n }\n\n /** Translate a natural-language search request into structured filters. */\n async parseSearch(query: string, signal?: AbortSignal): Promise<AiSearchFilters> {\n const today = new Date().toISOString().slice(0, 10)\n const system = SEARCH_SYSTEM.replace('{{today}}', today)\n const text = await this.complete(system, `\u641C\u7D22\u9700\u6C42\uFF1A${query.trim()}`, signal)\n return parseSearchJson(text)\n }\n\n /** Extract actionable tasks from one mail. */\n async extractTasks(message: MessageDetail, signal?: AbortSignal): Promise<TaskSuggestion[]> {\n const text = await this.complete(TASK_SYSTEM, contextOf(message), signal)\n return parseTaskJson(text)\n }\n\n /** Compose-body assistance: draft from points, polish, or shorten. */\n async compose(\n account: Account,\n options: {\n mode: 'draft' | 'polish' | 'shorten'\n instruction?: string\n html: string\n language?: 'zh' | 'en'\n },\n signal?: AbortSignal,\n ): Promise<string> {\n const style = this.style(account)?.profile ?? ''\n const language = options.language ?? 'zh'\n const body = options.html.trim()\n if (options.mode !== 'draft' && body === '') throw new Error('\u6B63\u6587\u4E3A\u7A7A')\n const source =\n options.mode === 'draft'\n ? `\u7528\u6237\u8981\u70B9\uFF1A${(options.instruction ?? '').trim() === '' ? '\u5199\u4E00\u5C01\u7B80\u77ED\u7684\u90AE\u4EF6' : (options.instruction ?? '').trim()}`\n : `\u9700\u8981\u5904\u7406\u7684\u6B63\u6587\uFF1A\\n${clip(htmlToPlain(body), 12000)}`\n return this.complete(COMPOSE_SYSTEM(options.mode, language, style), source, signal)\n }\n\n async learnStyle(\n account: Account,\n samples: { subject: string; text: string }[],\n signal?: AbortSignal,\n ): Promise<StyleProfile> {\n if (samples.length === 0) throw new Error('\u6CA1\u6709\u53EF\u7528\u7684\u5DF2\u53D1\u9001\u90AE\u4EF6\uFF0C\u5148\u5907\u4EFD\u4E00\u6B21\u90AE\u7BB1')\n const digest = samples\n .slice(0, 100)\n .map((sample, index) => `\u3010${String(index + 1)}\u3011\u4E3B\u9898\uFF1A${sample.subject}\\n${clip(sample.text, 800)}`)\n .join('\\n\\n')\n const profile = await this.complete(STYLE_SYSTEM, clip(digest, 35_000), signal)\n const next: StyleProfile = {\n profile,\n updatedAt: new Date().toISOString(),\n sampleCount: samples.length,\n }\n writeStyle(this.deps.accountRoot(account), next)\n return next\n }\n\n async models(): Promise<{ provider: string; name: string; models: { id: string; name: string }[] }[]> {\n const llm = this.llm()\n if (llm === undefined) return []\n const providers = llm.listProviders()\n const catalog: { provider: string; name: string; models: { id: string; name: string }[] }[] = []\n for (const provider of providers) {\n try {\n const models = await llm.listModels(provider.id)\n catalog.push({\n provider: provider.id,\n name: provider.name ?? provider.id,\n models: models.map((model) => ({ id: model.id, name: model.name ?? model.id })),\n })\n } catch {\n catalog.push({ provider: provider.id, name: provider.name ?? provider.id, models: [] })\n }\n }\n return catalog\n }\n}\n\n/**\n * The model may answer a translation with a complete HTML document. Keep the\n * body for insertion, and derive plain text for display so the panel never\n * shows raw tags.\n */\nexport function normalizeTranslated(value: string): { text: string; html: string } {\n const raw = value.trim()\n if (raw === '') return { text: '', html: '' }\n if (!/<[a-z][\\s\\S]*>/i.test(raw)) return { text: raw, html: textToHtml(raw) }\n let body = raw\n const match = /<body[^>]*>([\\s\\S]*?)<\\/body>/i.exec(raw)\n if (match !== null) body = match[1] ?? ''\n body = body\n .replace(/<!doctype[^>]*>/gi, '')\n .replace(/<head[\\s\\S]*?<\\/head>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .trim()\n const text = htmlToPlain(body)\n if (body === '' || text === '') return { text: raw, html: textToHtml(raw) }\n return { text, html: body }\n}\n\n/** Strip HTML tags for prompts that must receive plain text. */\nexport function htmlToPlain(html: string): string {\n return html\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6])>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nexport interface AiSearchFilters {\n keywords: string[]\n from: string\n subject: string\n since: string\n before: string\n explanation: string\n}\n\n/** Parse the model's search JSON, tolerating code fences and prose. */\nexport function parseSearchJson(text: string): AiSearchFilters {\n const start = text.indexOf('{')\n const end = text.lastIndexOf('}')\n const empty: AiSearchFilters = {\n keywords: [],\n from: '',\n subject: '',\n since: '',\n before: '',\n explanation: '',\n }\n if (start < 0 || end <= start) return empty\n let parsed: unknown\n try {\n parsed = JSON.parse(text.slice(start, end + 1))\n } catch {\n return empty\n }\n if (parsed === null || typeof parsed !== 'object') return empty\n const value = parsed as Record<string, unknown>\n const keywords = Array.isArray(value.keywords)\n ? value.keywords\n .map((item) => String(item).trim())\n .filter((item) => item !== '')\n .slice(0, 4)\n : []\n const date = (input: unknown): string =>\n /^\\d{4}-\\d{2}-\\d{2}$/.test(String(input ?? '')) ? String(input) : ''\n return {\n keywords,\n from: String(value.from ?? '')\n .trim()\n .slice(0, 120),\n subject: String(value.subject ?? '')\n .trim()\n .slice(0, 120),\n since: date(value.since),\n before: date(value.before),\n explanation: String(value.explanation ?? '')\n .trim()\n .slice(0, 200),\n }\n}\n\nexport interface TaskSuggestion {\n title: string\n due: string\n priority: number\n owner: string\n notes: string\n}\n\n/** Parse the model's JSON array, tolerating code fences and prose around it. */\nexport function parseTaskJson(text: string): TaskSuggestion[] {\n const start = text.indexOf('[')\n const end = text.lastIndexOf(']')\n if (start < 0 || end <= start) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(text.slice(start, end + 1))\n } catch {\n return []\n }\n if (!Array.isArray(parsed)) return []\n const result: TaskSuggestion[] = []\n for (const item of parsed) {\n if (item === null || typeof item !== 'object') continue\n const value = item as Record<string, unknown>\n const title = String(value.title ?? '').trim()\n if (title === '') continue\n result.push({\n title: title.slice(0, 300),\n due: /^\\d{4}-\\d{2}-\\d{2}/.test(String(value.due ?? '')) ? String(value.due).slice(0, 10) : '',\n priority: Math.max(0, Math.min(9, Number(value.priority ?? 0) || 0)),\n owner: String(value.owner ?? '').slice(0, 120),\n notes: String(value.notes ?? '').slice(0, 1000),\n })\n }\n return result.slice(0, 30)\n}\n\n/** Markdown-ish summary \u2192 simple blocks for the panel. */\nexport function summaryBlocks(text: string): { title: string; body: string }[] {\n const blocks: { title: string; body: string }[] = []\n let current: { title: string; lines: string[] } = { title: '', lines: [] }\n const push = (): void => {\n const body = current.lines.join('\\n').trim()\n if (current.title !== '' || body !== '') blocks.push({ title: current.title, body })\n current = { title: '', lines: [] }\n }\n for (const line of text.split('\\n')) {\n const heading =\n /^#{1,4}\\s+(.*)$/.exec(line) ?? /^(\u7ED3\u8BBA|\u9700\u8981\u4F60\u505A\u4EC0\u4E48|\u5173\u952E\u4FE1\u606F|\u5EFA\u8BAE\u56DE\u590D\u8981\u70B9)\\s*$/.exec(line)\n if (heading !== null) {\n push()\n current.title = heading[1] ?? ''\n } else {\n current.lines.push(line)\n }\n }\n push()\n return blocks\n}\n\nexport function textToHtml(text: string): string {\n return text\n .split(/\\n{2,}/)\n .map(\n (paragraph) =>\n `<p>${paragraph.trim().replaceAll('&', '&').replaceAll('<', '<').replaceAll('\\n', '<br>')}</p>`,\n )\n .join('')\n}\n\nexport { existsSync }\n", "import { randomUUID } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { DshSessions, DshWorkspaceRegistry } from './dsh.js'\n\nexport interface ProjectMessage {\n accountId: string\n key: string\n subject: string\n date: string\n}\n\nexport interface ProjectThread {\n accountId: string\n key: string\n}\n\nexport interface ProjectRecord {\n id: string\n name: string\n path: string\n workspaceId: string\n sessionId: string\n accountIds: string[]\n messages: ProjectMessage[]\n threads: ProjectThread[]\n createdAt: string\n updatedAt: string\n}\n\ninterface ProjectFile {\n version: 1\n projects: ProjectRecord[]\n}\n\n/** JSON store for mail-linked projects (few records, no need for SQLite). */\nexport class ProjectStore {\n private readonly file: string\n private cache: ProjectFile | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n private load(): ProjectFile {\n if (this.cache !== null) return this.cache\n let parsed: unknown = null\n try {\n if (existsSync(this.file)) parsed = JSON.parse(readFileSync(this.file, 'utf8'))\n } catch {\n parsed = null\n }\n const projects =\n parsed !== null && typeof parsed === 'object' && Array.isArray((parsed as ProjectFile).projects)\n ? (parsed as ProjectFile).projects\n : []\n this.cache = { version: 1, projects: projects.filter(isProject) }\n return this.cache\n }\n\n private flush(): void {\n const data = this.load()\n mkdirSync(join(this.file, '..'), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.tmp`\n writeFileSync(temp, `${JSON.stringify(data, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temp, this.file)\n }\n\n list(): ProjectRecord[] {\n return [...this.load().projects].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n }\n\n get(id: string): ProjectRecord | undefined {\n return this.load().projects.find((project) => project.id === id)\n }\n\n findByMessage(accountId: string, key: string): ProjectRecord | undefined {\n return this.load().projects.find((project) =>\n project.messages.some((message) => message.accountId === accountId && message.key === key),\n )\n }\n\n create(record: ProjectRecord): ProjectRecord {\n const data = this.load()\n data.projects.push(record)\n this.flush()\n return record\n }\n\n update(id: string, patch: Partial<ProjectRecord>): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n Object.assign(project, patch, { updatedAt: new Date().toISOString() })\n this.flush()\n return project\n }\n\n addMessage(id: string, message: ProjectMessage): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n const exists = project.messages.some(\n (item) => item.accountId === message.accountId && item.key === message.key,\n )\n if (!exists) project.messages.push(message)\n if (!project.accountIds.includes(message.accountId)) project.accountIds.push(message.accountId)\n project.updatedAt = new Date().toISOString()\n this.flush()\n return project\n }\n\n removeMessage(id: string, accountId: string, key: string): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n project.messages = project.messages.filter((item) => !(item.accountId === accountId && item.key === key))\n project.updatedAt = new Date().toISOString()\n this.flush()\n return project\n }\n\n addThread(id: string, thread: ProjectThread): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n if (!project.threads.some((item) => item.accountId === thread.accountId && item.key === thread.key)) {\n project.threads.push(thread)\n }\n project.updatedAt = new Date().toISOString()\n this.flush()\n return project\n }\n}\n\nfunction isProject(value: unknown): value is ProjectRecord {\n return (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as ProjectRecord).id === 'string' &&\n typeof (value as ProjectRecord).path === 'string'\n )\n}\n\nexport function slugify(name: string): string {\n const text = name\n .trim()\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n return text === '' ? 'project' : text.slice(0, 60)\n}\n\nexport interface CreateProjectInput {\n name: string\n root: string\n conversation: string\n accountId: string\n message?: ProjectMessage\n thread?: ProjectThread\n}\n\nexport interface CreateProjectResult {\n project: ProjectRecord\n warnings: string[]\n}\n\n/**\n * Create a project folder with a brief and the transferred AI conversation,\n * register it as a DSH Workspace, and open a session that starts from it.\n */\nexport async function createProject(options: {\n store: ProjectStore\n input: CreateProjectInput\n registry?: DshWorkspaceRegistry\n sessions?: DshSessions\n}): Promise<CreateProjectResult> {\n const { store, input } = options\n const name = input.name.trim()\n if (name === '') throw new Error('\u9879\u76EE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A')\n if (name.length > 120) throw new Error('\u9879\u76EE\u540D\u79F0\u8FC7\u957F')\n const root = input.root.trim()\n if (root === '') throw new Error('\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A')\n\n const warnings: string[] = []\n const id = `prj_${randomUUID().slice(0, 8)}`\n const base = slugify(name)\n let path = join(root, base)\n let attempt = 2\n while (existsSync(path)) {\n path = join(root, `${base}-${String(attempt)}`)\n attempt += 1\n }\n mkdirSync(path, { recursive: true })\n\n const created = new Date().toISOString()\n const brief = renderBrief(name, input)\n writeFileSync(join(path, 'BRIEF.md'), brief, { mode: 0o600 })\n writeFileSync(join(path, 'CONVERSATION.md'), renderConversation(input.conversation, created), {\n mode: 0o600,\n })\n\n let workspaceId = ''\n if (options.registry !== undefined) {\n try {\n const workspace = await options.registry.create(path, name)\n workspaceId = String(workspace?.id ?? '')\n } catch (error) {\n warnings.push(`\u5DE5\u4F5C\u7A7A\u95F4\u6CE8\u518C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)\n }\n } else {\n warnings.push('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 workspaceRegistry\uFF0C\u9879\u76EE\u76EE\u5F55\u5DF2\u521B\u5EFA\u4F46\u6CA1\u6709\u6CE8\u518C\u5230\u4FA7\u680F')\n }\n\n let sessionId = ''\n if (options.sessions !== undefined) {\n try {\n sessionId = randomUUID()\n const session = options.sessions.create(sessionId, { meta: { cwd: path } })\n session.append(\n 'user/message',\n {\n role: 'user',\n content: [{ type: 'text', text: `\u6211\u521B\u5EFA\u4E86\u4E00\u4E2A\u90AE\u4EF6\u9879\u76EE\u300C${name}\u300D\u3002\\n\\n${brief}` }],\n source: { kind: 'user' },\n },\n { surfaceOp: 'append' },\n )\n } catch (error) {\n warnings.push(`\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)\n sessionId = ''\n }\n } else {\n warnings.push('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 sessions \u670D\u52A1\uFF0C\u672A\u521B\u5EFA\u9879\u76EE\u4F1A\u8BDD')\n }\n\n const project: ProjectRecord = {\n id,\n name,\n path,\n workspaceId,\n sessionId,\n accountIds: [input.accountId],\n messages: input.message === undefined ? [] : [input.message],\n threads: input.thread === undefined ? [] : [input.thread],\n createdAt: created,\n updatedAt: created,\n }\n writeFileSync(\n join(path, 'project.json'),\n `${JSON.stringify({ ...project, conversation: undefined }, null, 2)}\\n`,\n {\n mode: 0o600,\n },\n )\n store.create(project)\n return { project, warnings }\n}\n\n/** Append new AI cards to an existing project's conversation file. */\nexport function appendConversation(project: ProjectRecord, conversation: string): string {\n const stamp = new Date().toISOString()\n const file = join(project.path, 'CONVERSATION.md')\n const block = `\\n\\n## \u8FFD\u52A0\u8BB0\u5F55 \u00B7 ${stamp}\\n\\n${conversation.trim()}\\n`\n writeFileSync(file, block, { flag: 'a', mode: 0o600 })\n return file\n}\n\nfunction renderBrief(name: string, input: CreateProjectInput): string {\n const lines = [`# ${name}`, '', `- \u521B\u5EFA\u65F6\u95F4\uFF1A${new Date().toISOString()}`, `- \u6765\u6E90\u8D26\u53F7\uFF1A${input.accountId}`]\n if (input.message !== undefined) {\n lines.push(`- \u6765\u6E90\u90AE\u4EF6\uFF1A${input.message.subject}\uFF08${input.message.date}\uFF09`)\n }\n lines.push(\n '',\n '## \u80CC\u666F',\n '',\n '\u7531 MailHarbor \u4ECE\u90AE\u4EF6\u751F\u6210\u3002\u4EE5\u4E0B\u662F\u5F53\u65F6 AI \u5DE5\u4F5C\u53F0\u7684\u8BB0\u5F55\u3002',\n '',\n '## AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55',\n '',\n input.conversation.trim(),\n '',\n )\n return lines.join('\\n')\n}\n\nfunction renderConversation(conversation: string, created: string): string {\n return `# AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55\\n\\n- \u8F6C\u79FB\u5230\u9879\u76EE\uFF1A${created}\\n\\n${conversation.trim()}\\n`\n}\n", "/** Single source of the Host-side version (`npm run version` keeps it in sync). */\nexport const VERSION = '0.7.0'\n", "/** Convert the composer's HTML into a plain-text alternative for the message. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6])>/gi, '\\n')\n .replace(/<li[^>]*>/gi, '\u2022 ')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nexport function escapeHtml(value: string): string {\n return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n}\n", "import { statSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Account, BackupStatus, ImapSettings } from '../shared/types.js'\nimport { CredentialManager } from './accounts.js'\nimport { AiService } from './ai.js'\nimport { AiThreadStore } from './ai-store.js'\nimport { ProjectStore } from './projects.js'\nimport { TaskStore } from './tasks.js'\nimport type { DshLlm, DshSessions, DshWorkspaceRegistry } from './dsh.js'\nimport { BackupEngine } from './backup.js'\nimport { DraftStore, draftPath } from './drafts.js'\nimport {\n type AccountRuntime,\n type MailHarborConfig,\n accountRuntime,\n activeAccount as pickActive,\n loadConfig,\n requireAccount,\n} from './config.js'\nimport { accountDataDir } from './paths.js'\nimport { ImapProvider, type ImapCredentials } from './imap.js'\nimport { MailDatabase } from './mail-db.js'\nimport { ReadStateStore } from './read-state.js'\n\ninterface SignatureEntry<T> {\n signature: string\n value: T\n}\n\nexport interface BackupJob {\n status: BackupStatus\n controller: AbortController\n promise: Promise<void>\n}\n\nconst MAX_OUTPUT_LINES = 200\n\n/**\n * Process-wide state for the plugin: one config, one credential manager, and\n * lazily created per-account providers and stores. Host code calls\n * {@link refresh} at the start of every request so external config edits are\n * picked up without a restart.\n */\nexport class MailHarborRuntime {\n private config: MailHarborConfig\n private configStamp = 0\n readonly credentials: CredentialManager\n private readonly providers = new Map<string, SignatureEntry<ImapProvider>>()\n private readonly databases = new Map<string, SignatureEntry<MailDatabase>>()\n private readonly draftStores = new Map<string, SignatureEntry<DraftStore>>()\n private readonly readStores = new Map<string, SignatureEntry<ReadStateStore>>()\n private readonly aiStores = new Map<string, SignatureEntry<AiThreadStore>>()\n private readonly backups = new Map<string, BackupJob>()\n private readonly llmProvider: () => DshLlm | undefined\n private readonly workspaceRegistryProvider: () => DshWorkspaceRegistry | undefined\n readonly sessions: DshSessions | undefined\n private aiService: AiService | null = null\n private projectStore: ProjectStore | null = null\n private taskStore: TaskStore | null = null\n private disposed = false\n\n constructor(\n options: {\n credentials?: CredentialManager\n llm?: () => DshLlm | undefined\n workspaceRegistry?: () => DshWorkspaceRegistry | undefined\n sessions?: DshSessions\n } = {},\n ) {\n this.config = loadConfig()\n this.credentials = options.credentials ?? new CredentialManager()\n this.llmProvider = options.llm ?? (() => undefined)\n this.workspaceRegistryProvider = options.workspaceRegistry ?? (() => undefined)\n this.sessions = options.sessions\n }\n\n get workspaceRegistry(): DshWorkspaceRegistry | undefined {\n return this.workspaceRegistryProvider()\n }\n\n projects(): ProjectStore {\n if (this.projectStore === null) {\n this.projectStore = new ProjectStore(join(this.config.dataDir, 'projects.json'))\n }\n return this.projectStore\n }\n\n tasks(): TaskStore {\n if (this.taskStore === null) {\n this.taskStore = new TaskStore(join(this.config.dataDir, 'state', 'state.db'))\n }\n return this.taskStore\n }\n\n /** The Host model service, when the composition provides one. */\n llm(): DshLlm | undefined {\n try {\n return this.llmProvider()\n } catch {\n return undefined\n }\n }\n\n ai(): AiService {\n if (this.aiService === null) {\n this.aiService = new AiService({\n config: () => this.config,\n llm: () => this.llm(),\n accountRoot: (account) => accountDataDir(account),\n })\n }\n return this.aiService\n }\n\n get current(): MailHarborConfig {\n return this.config\n }\n\n /** Re-read the config file when it changed on disk. */\n refresh(): MailHarborConfig {\n if (this.disposed) return this.config\n let stamp = 0\n try {\n const stats = statSync(this.config.configFile)\n stamp = stats.mtimeMs\n } catch {\n stamp = 0\n }\n if (stamp !== this.configStamp) {\n this.configStamp = stamp\n this.config = loadConfig()\n this.prune()\n }\n return this.config\n }\n\n private prune(): void {\n const live = new Set(this.config.accounts.map((account) => account.id))\n for (const id of [...this.providers.keys()]) {\n if (!live.has(id)) {\n void this.providers.get(id)?.value.dispose()\n this.providers.delete(id)\n }\n }\n for (const id of [...this.databases.keys()]) {\n if (!live.has(id)) {\n this.databases.get(id)?.value.close()\n this.databases.delete(id)\n }\n }\n for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id)\n for (const id of [...this.readStores.keys()]) if (!live.has(id)) this.readStores.delete(id)\n for (const id of [...this.aiStores.keys()]) if (!live.has(id)) this.aiStores.delete(id)\n }\n\n /** Drop cached connections and stores for one account after a settings change. */\n invalidate(accountId: string): void {\n void this.providers.get(accountId)?.value.dispose()\n this.providers.delete(accountId)\n this.databases.get(accountId)?.value.close()\n this.databases.delete(accountId)\n this.draftStores.delete(accountId)\n this.readStores.delete(accountId)\n this.aiStores.delete(accountId)\n this.refresh()\n }\n\n active(): Account | null {\n return pickActive(this.config)\n }\n\n account(id?: string | null): Account {\n return requireAccount(this.config, id)\n }\n\n paths(account: Account): AccountRuntime {\n return accountRuntime(account)\n }\n\n /** Credentials for an account, with a clear message when the user must log in. */\n credentialsFor(account: Account): ImapCredentials {\n if (account.auth.type === 'oauth2') {\n throw new Error('OAuth2 \u8D26\u53F7\u6682\u672A\u5728\u6B64\u7248\u672C\u542F\u7528\uFF0C\u8BF7\u4F7F\u7528\u5E94\u7528\u4E13\u7528\u5BC6\u7801\u767B\u5F55')\n }\n const resolved = this.credentials.resolve(account)\n if (resolved === undefined) throw new Error('\u8BF7\u5148\u767B\u5F55\u90AE\u7BB1\u8D26\u53F7')\n return { user: account.username || account.email, pass: resolved.password }\n }\n\n hasCredentials(account: Account): boolean {\n return this.credentials.resolve(account) !== undefined\n }\n\n imapSettings(account: Account): ImapSettings {\n return account.imap\n }\n\n provider(account: Account): ImapProvider {\n const signature = `${account.imap.host}:${String(account.imap.port)}:${String(account.imap.secure)}:${account.username}:${account.auth.type}`\n const existing = this.providers.get(account.id)\n if (existing !== undefined && existing.signature === signature) return existing.value\n if (existing !== undefined) void existing.value.dispose()\n const created = new ImapProvider(account.imap, this.credentialsFor(account), {\n onReconnect: () => {\n // Connection drops are expected on long-lived accounts; the next\n // operation reconnects transparently.\n },\n })\n this.providers.set(account.id, { signature, value: created })\n return created\n }\n\n database(account: Account): MailDatabase {\n const root = this.paths(account)\n const existing = this.databases.get(account.id)\n if (existing !== undefined && existing.signature === root.mailDb) return existing.value\n if (existing !== undefined) existing.value.close()\n const created = new MailDatabase(root.mailDb, {\n maxBodyChars: 400_000,\n attachmentsRoot: root.attachments,\n })\n this.databases.set(account.id, { signature: root.mailDb, value: created })\n return created\n }\n\n drafts(account: Account): DraftStore {\n const file = draftPath(this.paths(account).root)\n const existing = this.draftStores.get(account.id)\n if (existing !== undefined && existing.signature === file) return existing.value\n const created = new DraftStore(file, account.id)\n this.draftStores.set(account.id, { signature: file, value: created })\n return created\n }\n\n aiThreads(account: Account): AiThreadStore {\n const file = join(this.paths(account).root, 'ai-threads.json')\n const existing = this.aiStores.get(account.id)\n if (existing !== undefined && existing.signature === file) return existing.value\n const created = new AiThreadStore(file)\n this.aiStores.set(account.id, { signature: file, value: created })\n return created\n }\n\n readState(account: Account): ReadStateStore {\n const file = join(this.paths(account).root, 'read-state.json')\n const existing = this.readStores.get(account.id)\n if (existing !== undefined && existing.signature === file) return existing.value\n const created = new ReadStateStore(file)\n this.readStores.set(account.id, { signature: file, value: created })\n return created\n }\n\n backupStatus(account: Account): BackupStatus {\n const job = this.backups.get(account.id)\n if (job !== undefined) return { ...job.status, output: [...job.status.output] }\n const persisted = BackupEngine.status(this.paths(account).mailDb)\n return { ...persisted, accountId: account.id }\n }\n\n /** Start an incremental backup; returns the job so the caller can stream output. */\n startBackup(\n account: Account,\n onEvent?: (event: { message: string; mailbox?: string; added?: number }) => void,\n ): BackupJob {\n const running = this.backups.get(account.id)\n if (running !== undefined && running.status.running) throw new Error('\u5907\u4EFD\u5DF2\u5728\u8FDB\u884C\u4E2D')\n const controller = new AbortController()\n const status: BackupStatus = {\n running: true,\n accountId: account.id,\n startedAt: new Date().toISOString(),\n finishedAt: null,\n lastSuccessAt: null,\n total: 0,\n added: 0,\n message: '\u51C6\u5907\u5907\u4EFD',\n output: [],\n exitCode: null,\n }\n const job: BackupJob = { status, controller, promise: Promise.resolve() }\n const engine = new BackupEngine(accountRuntime(account), this.credentialsFor(account))\n job.promise = (async () => {\n try {\n const result = await engine.run({\n signal: controller.signal,\n onEvent: (event) => {\n status.message = event.message\n if (event.mailbox !== undefined) status.message = event.message\n if (event.added !== undefined) status.added = event.added\n if (event.total !== undefined) status.total = event.total\n status.output.push(event.message)\n if (status.output.length > MAX_OUTPUT_LINES)\n status.output.splice(0, status.output.length - MAX_OUTPUT_LINES)\n onEvent?.(event)\n },\n })\n status.total = result.total\n status.added = result.newCount\n status.exitCode = 0\n status.message = `\u5907\u4EFD\u5B8C\u6210\uFF1A\u65B0\u589E ${String(result.newCount)} \u5C01\uFF0C\u5171 ${String(result.total)} \u5C01`\n } catch (error) {\n status.exitCode = 1\n status.message = error instanceof Error ? error.message : String(error)\n status.output.push(status.message)\n } finally {\n status.running = false\n status.finishedAt = new Date().toISOString()\n if (status.exitCode === 0) status.lastSuccessAt = status.finishedAt\n this.databases.get(account.id)?.value.close()\n this.databases.delete(account.id)\n }\n })()\n this.backups.set(account.id, job)\n return job\n }\n\n stopBackup(account: Account): boolean {\n const job = this.backups.get(account.id)\n if (job === undefined || !job.status.running) return false\n job.controller.abort()\n return true\n }\n\n async dispose(): Promise<void> {\n this.disposed = true\n for (const job of this.backups.values()) job.controller.abort()\n for (const provider of this.providers.values()) await provider.value.dispose()\n this.providers.clear()\n for (const db of this.databases.values()) db.value.close()\n this.databases.clear()\n this.taskStore?.close()\n }\n}\n", "import { spawnSync } from 'node:child_process'\n\nexport type SecretProvider = 'macos' | 'libsecret' | 'windows' | 'none'\n\nexport interface CommandResult {\n ok: boolean\n stdout: string\n stderr: string\n}\n\nexport interface SecretStoreOptions {\n platform?: NodeJS.Platform\n run?: (command: string, args: string[], options?: { input?: string; timeout?: number }) => CommandResult\n hasCommand?: (command: string, args: string[]) => boolean\n}\n\nfunction defaultRun(\n command: string,\n args: string[],\n options: { input?: string; timeout?: number } = {},\n): CommandResult {\n const result = spawnSync(command, args, {\n encoding: 'utf8',\n timeout: options.timeout ?? 10_000,\n input: options.input,\n stdio: options.input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],\n })\n return { ok: result.status === 0, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }\n}\n\nfunction defaultHasCommand(command: string, args: string[]): boolean {\n const result = spawnSync(command, args, { timeout: 3000, stdio: 'ignore' })\n return result.error === undefined && result.status === 0\n}\n\nfunction psString(value: string): string {\n return `'${value.replaceAll(\"'\", \"''\")}'`\n}\n\n/**\n * Cross-platform secret persistence.\n *\n * macOS uses the Keychain (`security`), Linux uses libsecret (`secret-tool`),\n * Windows uses the Credential Locker through PowerShell. When no provider is\n * available the store reports unavailable; callers keep the password in this\n * process's session and never write it to the config.\n */\nexport class SecretStore {\n readonly provider: SecretProvider\n private readonly run: NonNullable<SecretStoreOptions['run']>\n private readonly hasCommand: NonNullable<SecretStoreOptions['hasCommand']>\n\n constructor(options: SecretStoreOptions = {}) {\n const platform = options.platform ?? process.platform\n this.run = options.run ?? defaultRun\n this.hasCommand = options.hasCommand ?? defaultHasCommand\n if (platform === 'darwin') {\n this.provider = 'macos'\n } else if (platform === 'win32') {\n this.provider = this.hasCommand('powershell.exe', [\n '-NoProfile',\n '-NonInteractive',\n '-Command',\n 'exit 0',\n ])\n ? 'windows'\n : 'none'\n } else if (platform === 'linux') {\n this.provider = this.hasCommand('secret-tool', ['--version']) ? 'libsecret' : 'none'\n } else {\n this.provider = 'none'\n }\n }\n\n available(): boolean {\n return this.provider !== 'none'\n }\n\n read(service: string, account: string): string | undefined {\n if (service === '' || account === '') return undefined\n if (this.provider === 'macos') {\n const result = this.run('security', ['find-generic-password', '-s', service, '-a', account, '-w'])\n if (!result.ok) return undefined\n return trimSecret(result.stdout)\n }\n if (this.provider === 'libsecret') {\n const result = this.run('secret-tool', ['lookup', 'service', service, 'account', account])\n if (!result.ok) return undefined\n return trimSecret(result.stdout)\n }\n if (this.provider === 'windows') {\n const script = [\n \"$ErrorActionPreference = 'Stop'\",\n '[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null',\n '$vault = New-Object Windows.Security.Credentials.PasswordVault',\n `$resource = ${psString(service)}`,\n `$user = ${psString(account)}`,\n '$cred = $vault.FindAllByResource($resource) | Where-Object { $_.UserName -eq $user } | Select-Object -First 1',\n 'if ($null -eq $cred) { exit 1 }',\n '$cred.RetrievePassword()',\n '[Console]::Out.Write($cred.Password)',\n ].join('\\n')\n const result = this.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {\n input: script,\n })\n if (!result.ok) return undefined\n return trimSecret(result.stdout)\n }\n return undefined\n }\n\n write(service: string, account: string, secret: string): boolean {\n if (service === '' || account === '' || secret === '') return false\n if (this.provider === 'macos') {\n return this.run('security', ['add-generic-password', '-U', '-s', service, '-a', account, '-w', secret])\n .ok\n }\n if (this.provider === 'libsecret') {\n return this.run('secret-tool', ['store', '--label', service, 'service', service, 'account', account], {\n input: secret,\n }).ok\n }\n if (this.provider === 'windows') {\n const encoded = Buffer.from(secret, 'utf8').toString('base64')\n const script = [\n \"$ErrorActionPreference = 'Stop'\",\n '[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null',\n '$vault = New-Object Windows.Security.Credentials.PasswordVault',\n `$resource = ${psString(service)}`,\n `$user = ${psString(account)}`,\n `$password = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,\n '$vault.FindAllByResource($resource) | Where-Object { $_.UserName -eq $user } | ForEach-Object { $vault.Remove($_) }',\n '$vault.Add((New-Object Windows.Security.Credentials.PasswordCredential($resource, $user, $password)))',\n ].join('\\n')\n return this.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {\n input: script,\n }).ok\n }\n return false\n }\n\n delete(service: string, account: string): boolean {\n if (service === '' || account === '') return false\n if (this.provider === 'macos') {\n return this.run('security', ['delete-generic-password', '-s', service, '-a', account]).ok\n }\n if (this.provider === 'libsecret') {\n return this.run('secret-tool', ['clear', 'service', service, 'account', account]).ok\n }\n if (this.provider === 'windows') {\n const script = [\n \"$ErrorActionPreference = 'Stop'\",\n '[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null',\n '$vault = New-Object Windows.Security.Credentials.PasswordVault',\n `$resource = ${psString(service)}`,\n `$user = ${psString(account)}`,\n '$vault.FindAllByResource($resource) | Where-Object { $_.UserName -eq $user } | ForEach-Object { $vault.Remove($_) }',\n ].join('\\n')\n return this.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {\n input: script,\n }).ok\n }\n return false\n }\n}\n\nfunction trimSecret(value: string): string | undefined {\n const text = value.replace(/\\r?\\n$/, '')\n return text === '' ? undefined : text\n}\n", "import type { Account } from '../shared/types.js'\nimport { SecretStore } from './secrets.js'\n\nexport type PasswordSource = 'session' | 'env' | 'keychain'\n\nexport interface ResolvedPassword {\n password: string\n source: PasswordSource\n}\n\nexport interface CredentialStatus {\n hasPassword: boolean\n source: PasswordSource | null\n provider: string\n}\n\nfunction envKeyFor(account: Account): string {\n const suffix = account.id.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()\n return `DSH_MAILHARBOR_PASSWORD_${suffix}`\n}\n\n/**\n * Account passwords live in three places, in priority order: this process's\n * session (a fresh login), the environment (CI and scripted installs), and the\n * OS credential store. They are never written to `config.json`.\n */\nexport class CredentialManager {\n private readonly session = new Map<string, string>()\n private readonly secrets: SecretStore\n\n constructor(secrets: SecretStore = new SecretStore()) {\n this.secrets = secrets\n }\n\n get provider(): string {\n return this.secrets.provider\n }\n\n resolve(account: Account): ResolvedPassword | undefined {\n const session = this.session.get(account.id)\n if (session !== undefined && session !== '') return { password: session, source: 'session' }\n const fromEnv = process.env[envKeyFor(account)]\n if (typeof fromEnv === 'string' && fromEnv !== '') return { password: fromEnv, source: 'env' }\n const stored = this.secrets.read(account.keychainService, account.email)\n if (stored !== undefined) return { password: stored, source: 'keychain' }\n return undefined\n }\n\n /** Keep the password for this run and, when asked, for later runs. */\n remember(account: Account, password: string, persist: boolean): { stored: boolean } {\n if (password === '') throw new Error('\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A')\n this.session.set(account.id, password)\n if (!persist) return { stored: false }\n return { stored: this.secrets.write(account.keychainService, account.email, password) }\n }\n\n forget(account: Account): void {\n this.session.delete(account.id)\n this.secrets.delete(account.keychainService, account.email)\n }\n\n /** Read an arbitrary secret (for example a CalDAV app password). */\n readSecret(service: string, account: string): string | undefined {\n return this.secrets.read(service, account)\n }\n\n writeSecret(service: string, account: string, secret: string): boolean {\n return this.secrets.write(service, account, secret)\n }\n\n deleteSecret(service: string, account: string): boolean {\n return this.secrets.delete(service, account)\n }\n\n status(account: Account): CredentialStatus {\n const resolved = this.resolve(account)\n return {\n hasPassword: resolved !== undefined,\n source: resolved?.source ?? null,\n provider: this.secrets.provider,\n }\n }\n}\n", "import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport type { AiCard, AiThread } from '../shared/types.js'\n\nconst MAX_THREADS = 50\nconst MAX_CARDS_PER_THREAD = 50\nconst MAX_FILE_BYTES = 2 * 1024 * 1024\n\ninterface ThreadFile {\n version: 1\n threads: Record<string, AiThread>\n}\n\n/**\n * Per-account store for AI cards.\n *\n * Cards persist so closing the panel, switching mail or restarting DSH never\n * loses an AI result. The file is capped: oldest threads are dropped first,\n * then the oldest cards inside a thread.\n */\nexport class AiThreadStore {\n private readonly file: string\n private cache: ThreadFile | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n private load(): ThreadFile {\n if (this.cache !== null) return this.cache\n let parsed: unknown = null\n try {\n if (existsSync(this.file)) parsed = JSON.parse(readFileSync(this.file, 'utf8'))\n } catch {\n parsed = null\n }\n const threads =\n parsed !== null && typeof parsed === 'object' && (parsed as ThreadFile).threads !== undefined\n ? (parsed as ThreadFile).threads\n : {}\n this.cache = { version: 1, threads: sanitize(threads) }\n return this.cache\n }\n\n private flush(): void {\n const data = this.load()\n prune(data)\n const text = `${JSON.stringify(data, null, 2)}\\n`\n mkdirSync(dirname(this.file), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.tmp`\n writeFileSync(temp, text, { mode: 0o600 })\n renameSync(temp, this.file)\n }\n\n get(key: string): AiThread | null {\n const thread = this.load().threads[key]\n return thread === undefined ? null : { ...thread, cards: [...thread.cards] }\n }\n\n appendCard(input: {\n key: string\n accountId: string\n subject: string\n messageId?: string\n card: AiCard\n }): AiThread {\n const data = this.load()\n const now = new Date().toISOString()\n const existing = data.threads[input.key]\n const thread: AiThread = existing ?? {\n key: input.key,\n accountId: input.accountId,\n subject: input.subject,\n messageId: input.messageId ?? '',\n cards: [],\n updatedAt: now,\n }\n thread.accountId = input.accountId\n thread.subject = input.subject\n if (input.messageId !== undefined && input.messageId !== '') thread.messageId = input.messageId\n thread.cards.push(input.card)\n thread.updatedAt = now\n data.threads[input.key] = thread\n this.flush()\n return { ...thread, cards: [...thread.cards] }\n }\n\n removeCard(key: string, cardId: string): AiThread | null {\n const data = this.load()\n const thread = data.threads[key]\n if (thread === undefined) return null\n thread.cards = thread.cards.filter((card) => card.id !== cardId)\n thread.updatedAt = new Date().toISOString()\n if (thread.cards.length === 0) {\n delete data.threads[key]\n this.flush()\n return null\n }\n this.flush()\n return { ...thread, cards: [...thread.cards] }\n }\n\n clear(key: string): boolean {\n const data = this.load()\n if (data.threads[key] === undefined) return false\n delete data.threads[key]\n this.flush()\n return true\n }\n\n /** Number of threads, for the settings page. */\n size(): number {\n return Object.keys(this.load().threads).length\n }\n}\n\nfunction sanitize(input: unknown): Record<string, AiThread> {\n if (input === null || typeof input !== 'object') return {}\n const source = input as Record<string, unknown>\n const result: Record<string, AiThread> = {}\n for (const [key, raw] of Object.entries(source)) {\n if (raw === null || typeof raw !== 'object') continue\n const thread = raw as Partial<AiThread>\n if (typeof thread.accountId !== 'string' || !Array.isArray(thread.cards)) continue\n const cards: AiCard[] = thread.cards\n .filter(\n (card): card is AiCard => card !== null && typeof card === 'object' && typeof card.id === 'string',\n )\n .map((card) => ({\n id: card.id,\n kind: card.kind,\n instruction: String(card.instruction ?? ''),\n output: String(card.output ?? ''),\n ...(typeof card.html === 'string' ? { html: card.html } : {}),\n ...(card.meta !== undefined ? { meta: card.meta } : {}),\n provider: String(card.provider ?? ''),\n model: String(card.model ?? ''),\n createdAt: String(card.createdAt ?? ''),\n }))\n result[key] = {\n key,\n accountId: thread.accountId,\n subject: String(thread.subject ?? ''),\n messageId: String(thread.messageId ?? ''),\n cards,\n updatedAt: String(thread.updatedAt ?? ''),\n }\n }\n return result\n}\n\nfunction prune(data: ThreadFile): void {\n const entries = Object.entries(data.threads).sort((left, right) =>\n right[1].updatedAt.localeCompare(left[1].updatedAt),\n )\n const kept = new Map(entries.slice(0, MAX_THREADS))\n for (const thread of kept.values()) {\n if (thread.cards.length > MAX_CARDS_PER_THREAD) {\n thread.cards = thread.cards.slice(-MAX_CARDS_PER_THREAD)\n }\n }\n data.threads = Object.fromEntries(kept)\n let size = JSON.stringify(data).length\n while (size > MAX_FILE_BYTES && kept.size > 1) {\n const oldest = [...kept.entries()].sort((left, right) =>\n left[1].updatedAt.localeCompare(right[1].updatedAt),\n )[0]\n if (oldest === undefined) break\n kept.delete(oldest[0])\n data.threads = Object.fromEntries(kept)\n size = JSON.stringify(data).length\n }\n}\n", "import { randomUUID } from 'node:crypto'\nimport { mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { DatabaseSync } from 'node:sqlite'\n\nconst STATUSES = new Set(['NEEDS-ACTION', 'IN-PROCESS', 'COMPLETED', 'CANCELLED'])\n\nfunction now(): string {\n return new Date().toISOString()\n}\n\nexport interface TaskRecord {\n id: string\n accountId: string\n projectId: string | null\n calendarHref: string | null\n href: string | null\n etag: string | null\n uid: string\n title: string\n notes: string\n due: string\n status: string\n priority: number\n owner: string\n messageKey: string\n messageSubject: string\n completedAt: string | null\n dirty: boolean\n deleted: boolean\n createdAt: string\n updatedAt: string\n}\n\nfunction rowToTask(row: Record<string, unknown> | undefined): TaskRecord | undefined {\n if (row === undefined) return undefined\n return {\n id: String(row.id),\n accountId: String(row.account_id),\n projectId: row.project_id === null || row.project_id === undefined ? null : String(row.project_id),\n calendarHref:\n row.calendar_href === null || row.calendar_href === undefined ? null : String(row.calendar_href),\n href: row.href === null || row.href === undefined ? null : String(row.href),\n etag: row.etag === null || row.etag === undefined ? null : String(row.etag),\n uid: String(row.uid),\n title: String(row.title),\n notes: String(row.notes ?? ''),\n due: String(row.due ?? ''),\n status: String(row.status ?? 'NEEDS-ACTION'),\n priority: Number(row.priority ?? 0),\n owner: String(row.owner ?? ''),\n messageKey: String(row.message_key ?? ''),\n messageSubject: String(row.message_subject ?? ''),\n completedAt:\n row.completed_at === null || row.completed_at === undefined ? null : String(row.completed_at),\n dirty: Number(row.dirty) === 1,\n deleted: Number(row.deleted) === 1,\n createdAt: String(row.created_at ?? ''),\n updatedAt: String(row.updated_at ?? ''),\n }\n}\n\nfunction statusOf(value: unknown): string {\n const text = String(value ?? 'NEEDS-ACTION').toUpperCase()\n return STATUSES.has(text) ? text : 'NEEDS-ACTION'\n}\n\n/**\n * Local VTODO store in `<DataRoot>/state/state.db`.\n *\n * Local edits set dirty=1; CalDAV sync pushes dirty rows and applies remote\n * rows when there is no pending local edit. Deletions leave a tombstone until\n * the remote DELETE succeeds.\n */\nexport class TaskStore {\n private readonly file: string\n private db: DatabaseSync | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n close(): void {\n if (this.db !== null) {\n this.db.close()\n this.db = null\n }\n }\n\n private open(): DatabaseSync {\n if (this.db !== null) return this.db\n mkdirSync(dirname(this.file), { recursive: true })\n const db = new DatabaseSync(this.file)\n db.exec('PRAGMA journal_mode=WAL')\n db.exec('PRAGMA busy_timeout=10000')\n db.exec(`\n CREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n account_id TEXT NOT NULL,\n project_id TEXT,\n calendar_href TEXT,\n href TEXT,\n etag TEXT,\n uid TEXT NOT NULL,\n title TEXT NOT NULL,\n notes TEXT,\n due TEXT,\n status TEXT NOT NULL DEFAULT 'NEEDS-ACTION',\n priority INTEGER NOT NULL DEFAULT 0,\n owner TEXT,\n message_key TEXT,\n message_subject TEXT,\n completed_at TEXT,\n dirty INTEGER NOT NULL DEFAULT 1,\n deleted INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS tasks_account_due ON tasks (account_id, deleted, status, due);\n CREATE INDEX IF NOT EXISTS tasks_project ON tasks (project_id);\n CREATE TABLE IF NOT EXISTS calendar_state (\n account_id TEXT PRIMARY KEY,\n calendar_href TEXT,\n display_name TEXT,\n ctag TEXT,\n last_sync TEXT,\n error TEXT\n );\n `)\n this.db = db\n return db\n }\n\n list(options: {\n accountId: string\n status?: string\n projectId?: string\n query?: string\n limit?: number\n }): TaskRecord[] {\n const db = this.open()\n const where = ['account_id = ?', 'deleted = 0']\n const params: (string | number)[] = [options.accountId]\n if (options.status !== undefined && options.status !== '') {\n where.push('status = ?')\n params.push(statusOf(options.status))\n }\n if (options.projectId !== undefined && options.projectId !== '') {\n where.push('project_id = ?')\n params.push(options.projectId)\n }\n const text = String(options.query ?? '').trim()\n if (text !== '') {\n where.push('(title LIKE ? OR notes LIKE ? OR owner LIKE ?)')\n const like = `%${text}%`\n params.push(like, like, like)\n }\n const rows = db\n .prepare(\n `SELECT * FROM tasks WHERE ${where.join(' AND ')}\n ORDER BY CASE WHEN completed_at IS NOT NULL THEN 1 ELSE 0 END,\n CASE WHEN due IS NULL OR due = '' THEN 1 ELSE 0 END, due ASC, created_at DESC\n LIMIT ?`,\n )\n .all(...params, Math.max(1, Math.min(1000, options.limit ?? 500)))\n return rows\n .map((row) => rowToTask(row as Record<string, unknown>))\n .filter((task): task is TaskRecord => task !== undefined)\n }\n\n get(id: string): TaskRecord | undefined {\n return rowToTask(\n this.open().prepare('SELECT * FROM tasks WHERE id = ?').get(id) as Record<string, unknown> | undefined,\n )\n }\n\n create(input: {\n accountId: string\n title: string\n notes?: string\n due?: string\n status?: string\n priority?: number\n owner?: string\n projectId?: string | null\n messageKey?: string\n messageSubject?: string\n }): TaskRecord {\n const title = String(input.title ?? '').trim()\n if (title === '') throw new Error('\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A')\n if (title.length > 300) throw new Error('\u4EFB\u52A1\u6807\u9898\u8FC7\u957F')\n const status = statusOf(input.status)\n const id = `task_${randomUUID()}`\n const stamp = now()\n this.open()\n .prepare(\n `INSERT INTO tasks (id, account_id, project_id, uid, title, notes, due, status, priority, owner,\n message_key, message_subject, completed_at, dirty, deleted, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)`,\n )\n .run(\n id,\n input.accountId,\n input.projectId ?? null,\n `${randomUUID()}@dsh-mailharbor`,\n title,\n String(input.notes ?? ''),\n String(input.due ?? ''),\n status,\n Math.max(0, Math.min(9, Number(input.priority ?? 0))),\n String(input.owner ?? ''),\n String(input.messageKey ?? ''),\n String(input.messageSubject ?? ''),\n status === 'COMPLETED' ? stamp : null,\n stamp,\n stamp,\n )\n const task = this.get(id)\n if (task === undefined) throw new Error('\u4EFB\u52A1\u521B\u5EFA\u5931\u8D25')\n return task\n }\n\n update(id: string, fields: Partial<TaskRecord> & { projectId?: string | null }): TaskRecord {\n const current = this.get(id)\n if (current === undefined) throw new Error('\u4EFB\u52A1\u4E0D\u5B58\u5728')\n const next = {\n title: fields.title === undefined ? current.title : String(fields.title).trim(),\n notes: fields.notes === undefined ? current.notes : String(fields.notes ?? ''),\n due: fields.due === undefined ? current.due : String(fields.due ?? ''),\n status: fields.status === undefined ? current.status : statusOf(fields.status),\n priority: fields.priority === undefined ? current.priority : Number(fields.priority),\n owner: fields.owner === undefined ? current.owner : String(fields.owner ?? ''),\n projectId: fields.projectId === undefined ? current.projectId : fields.projectId,\n messageKey: fields.messageKey === undefined ? current.messageKey : String(fields.messageKey),\n messageSubject:\n fields.messageSubject === undefined ? current.messageSubject : String(fields.messageSubject),\n }\n if (next.title === '') throw new Error('\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A')\n const stamp = now()\n const completedAt = next.status === 'COMPLETED' ? (current.completedAt ?? stamp) : null\n this.open()\n .prepare(\n `UPDATE tasks SET title = ?, notes = ?, due = ?, status = ?, priority = ?, owner = ?, project_id = ?,\n message_key = ?, message_subject = ?, completed_at = ?, dirty = 1, updated_at = ? WHERE id = ?`,\n )\n .run(\n next.title,\n next.notes,\n next.due,\n next.status,\n Math.max(0, Math.min(9, Number(next.priority) || 0)),\n next.owner,\n next.projectId,\n next.messageKey,\n next.messageSubject,\n completedAt,\n stamp,\n id,\n )\n const task = this.get(id)\n if (task === undefined) throw new Error('\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25')\n return task\n }\n\n remove(id: string): boolean {\n const current = this.get(id)\n if (current === undefined) return false\n if (current.href !== null && current.href !== '') {\n this.open()\n .prepare('UPDATE tasks SET deleted = 1, dirty = 1, updated_at = ? WHERE id = ?')\n .run(now(), id)\n } else {\n this.open().prepare('DELETE FROM tasks WHERE id = ?').run(id)\n }\n return true\n }\n\n /** Apply a VTODO fetched from CalDAV; local pending edits win over remote. */\n applyRemote(input: {\n accountId: string\n calendarHref: string\n href: string\n etag: string | null\n todo: {\n uid: string\n title: string\n notes: string\n due: string\n status: string\n priority: number\n completedAt: string | null\n }\n }): TaskRecord | undefined {\n const db = this.open()\n const existing =\n (db\n .prepare('SELECT * FROM tasks WHERE account_id = ? AND href = ?')\n .get(input.accountId, input.href) as Record<string, unknown> | undefined) ??\n (db\n .prepare('SELECT * FROM tasks WHERE account_id = ? AND uid = ?')\n .get(input.accountId, input.todo.uid) as Record<string, unknown> | undefined)\n const status = statusOf(input.todo.status)\n const completedAt = status === 'COMPLETED' ? (input.todo.completedAt ?? now()) : null\n if (existing !== undefined) {\n if (Number(existing.dirty) === 1) return rowToTask(existing)\n db.prepare(\n `UPDATE tasks SET calendar_href = ?, href = ?, etag = ?, title = ?, notes = ?, due = ?, status = ?, priority = ?,\n completed_at = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?`,\n ).run(\n input.calendarHref,\n input.href,\n input.etag,\n String(input.todo.title || existing.title),\n String(input.todo.notes ?? ''),\n String(input.todo.due ?? ''),\n status,\n Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),\n completedAt,\n now(),\n String(existing.id),\n )\n return this.get(String(existing.id))\n }\n const id = `task_${randomUUID()}`\n const stamp = now()\n db.prepare(\n `INSERT INTO tasks (id, account_id, calendar_href, href, etag, uid, title, notes, due, status, priority,\n completed_at, dirty, deleted, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)`,\n ).run(\n id,\n input.accountId,\n input.calendarHref,\n input.href,\n input.etag,\n input.todo.uid || `${randomUUID()}@dsh-mailharbor`,\n String(input.todo.title || '(\u65E0\u6807\u9898)'),\n String(input.todo.notes ?? ''),\n String(input.todo.due ?? ''),\n status,\n Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),\n completedAt,\n stamp,\n stamp,\n )\n return this.get(id)\n }\n\n pending(accountId: string): TaskRecord[] {\n return this.open()\n .prepare('SELECT * FROM tasks WHERE account_id = ? AND dirty = 1')\n .all(accountId)\n .map((row) => rowToTask(row as Record<string, unknown>))\n .filter((task): task is TaskRecord => task !== undefined)\n }\n\n markPushed(id: string, result: { href: string; etag: string | null; uid: string }): TaskRecord | undefined {\n this.open()\n .prepare(\n 'UPDATE tasks SET href = ?, etag = ?, uid = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?',\n )\n .run(result.href, result.etag, result.uid, now(), id)\n return this.get(id)\n }\n\n purge(id: string): void {\n this.open().prepare('DELETE FROM tasks WHERE id = ?').run(id)\n }\n\n /** Remove local copies whose remote VTODO disappeared (not dirty, not tombstoned). */\n pruneMissingRemote(accountId: string, calendarHref: string, hrefs: string[]): number {\n const db = this.open()\n const present = new Set(hrefs)\n const rows = db\n .prepare(\n `SELECT id, href FROM tasks\n WHERE account_id = ? AND dirty = 0 AND deleted = 0 AND href IS NOT NULL\n AND (calendar_href = ? OR calendar_href IS NULL OR calendar_href = '')`,\n )\n .all(accountId, calendarHref)\n let removed = 0\n for (const row of rows) {\n if (present.has(String(row.href))) continue\n db.prepare('DELETE FROM tasks WHERE id = ?').run(String(row.id))\n removed += 1\n }\n return removed\n }\n}\n", "import { randomUUID } from 'node:crypto'\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { Draft, DraftAttachment } from '../shared/types.js'\n\nfunction text(value: unknown): string {\n return value === undefined || value === null ? '' : String(value)\n}\n\nfunction isAttachment(value: unknown): value is DraftAttachment {\n return (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as DraftAttachment).filename === 'string' &&\n typeof (value as DraftAttachment).data === 'string'\n )\n}\n\nfunction isDraft(value: unknown): value is Draft {\n return value !== null && typeof value === 'object' && typeof (value as Draft).id === 'string'\n}\n\nexport function draftPath(root: string): string {\n return join(root, 'drafts.json')\n}\n\n/**\n * JSON-file draft store per account. Drafts never touch the backup database,\n * and an atomic rename means a crash cannot leave a half-written file.\n */\nexport class DraftStore {\n private file: string\n private items: Draft[] = []\n private readonly accountId: string\n\n constructor(file: string, accountId: string) {\n this.file = file\n this.accountId = accountId\n this.load()\n }\n\n private load(): void {\n try {\n const parsed: unknown = JSON.parse(readFileSync(this.file, 'utf8'))\n this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : []\n } catch {\n this.items = []\n }\n }\n\n private flush(): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`\n writeFileSync(temp, `${JSON.stringify(this.items, null, 2)}\\n`, { encoding: 'utf8', mode: 0o600 })\n renameSync(temp, this.file)\n }\n\n list(): Draft[] {\n return [...this.items]\n .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n .map((draft) => ({ ...draft }))\n }\n\n get(id: string): Draft | undefined {\n const found = this.items.find((draft) => draft.id === id)\n return found === undefined ? undefined : { ...found }\n }\n\n save(input: Partial<Draft> & { id?: string }): Draft {\n const now = new Date().toISOString()\n const existing = input.id === undefined ? undefined : this.items.find((draft) => draft.id === input.id)\n const fields = {\n to: text(input.to),\n cc: text(input.cc),\n bcc: text(input.bcc),\n subject: text(input.subject),\n html: text(input.html),\n attachments: Array.isArray(input.attachments)\n ? input.attachments.filter(isAttachment).map((file) => ({\n filename: text(file.filename).slice(0, 200),\n contentType: text(file.contentType).slice(0, 160),\n data: text(file.data),\n }))\n : [],\n inReplyTo: text(input.inReplyTo),\n references: text(input.references),\n }\n if (existing !== undefined) {\n Object.assign(existing, fields, { updatedAt: now })\n this.flush()\n return { ...existing }\n }\n const draft: Draft = {\n id: randomUUID(),\n accountId: this.accountId,\n ...fields,\n updatedAt: now,\n }\n this.items.push(draft)\n this.flush()\n return { ...draft }\n }\n\n remove(id: string): boolean {\n const index = this.items.findIndex((draft) => draft.id === id)\n if (index < 0) return false\n this.items.splice(index, 1)\n this.flush()\n return true\n }\n\n location(): string {\n return this.file\n }\n}\n", "import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/**\n * Local read state for archived mail.\n *\n * The backup database stores no IMAP flags, and most archived messages no\n * longer exist on the server. This store keeps only the keys the user marked\n * unread; every other message is treated as read. Server mail uses its own\n * `\\Seen` flag and is mirrored here by message id when the two overlap.\n */\nexport class ReadStateStore {\n private readonly file: string\n private keys: Set<string> | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n private load(): Set<string> {\n if (this.keys !== null) return this.keys\n try {\n const parsed: unknown = JSON.parse(readFileSync(this.file, 'utf8'))\n const list = Array.isArray(parsed)\n ? parsed\n : Array.isArray((parsed as { unread?: unknown })?.unread)\n ? (parsed as { unread: unknown[] }).unread\n : []\n this.keys = new Set(list.filter((key): key is string => typeof key === 'string' && key !== ''))\n } catch {\n this.keys = new Set()\n }\n return this.keys\n }\n\n private flush(): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.tmp`\n writeFileSync(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}\\n`, {\n encoding: 'utf8',\n mode: 0o600,\n })\n renameSync(temp, this.file)\n }\n\n isUnread(key: string): boolean {\n return key !== '' && this.load().has(key)\n }\n\n setUnread(key: string, unread: boolean): boolean {\n const normalized = key.trim()\n if (normalized === '') return false\n const keys = this.load()\n const has = keys.has(normalized)\n if (unread && !has) {\n keys.add(normalized)\n this.flush()\n return true\n }\n if (!unread && has) {\n keys.delete(normalized)\n this.flush()\n return true\n }\n return false\n }\n\n sync(entries: { key: string; unread: boolean }[]): void {\n let changed = false\n const keys = this.load()\n for (const entry of entries) {\n const key = String(entry.key ?? '').trim()\n if (key === '') continue\n if (entry.unread && !keys.has(key)) {\n keys.add(key)\n changed = true\n } else if (!entry.unread && keys.has(key)) {\n keys.delete(key)\n changed = true\n }\n }\n if (changed) this.flush()\n }\n\n count(): number {\n return this.load().size\n }\n\n list(): string[] {\n return [...this.load()]\n }\n}\n", "import type { MessageDetail, MessageSummary } from '../shared/types.js'\nimport { formatAddressList } from './address.js'\nimport type { DshToolDefinition, DshTools } from './dsh.js'\nimport type { MailHarborRuntime } from './runtime.js'\nimport type { Account } from '../shared/types.js'\n\nconst MAX_BODY = 20_000\nconst MAX_LIST = 50\n\nfunction textOf(args: unknown, key: string): string {\n if (args === null || typeof args !== 'object') return ''\n const value = (args as Record<string, unknown>)[key]\n return typeof value === 'string' ? value : ''\n}\n\nfunction numberOf(args: unknown, key: string): number | undefined {\n if (args === null || typeof args !== 'object') return undefined\n const value = (args as Record<string, unknown>)[key]\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined\n}\n\nfunction clip(text: string, max = MAX_BODY): string {\n return text.length <= max ? text : `${text.slice(0, max)}\\n\u2026\uFF08\u5DF2\u622A\u65AD\uFF0C\u5171 ${text.length} \u5B57\u7B26\uFF09`\n}\n\nfunction line(value: string): string {\n return value.replace(/\\s+/g, ' ').trim()\n}\n\nfunction stamp(value: string): string {\n return value.slice(0, 19).replace('T', ' ')\n}\n\nfunction addressText(list: { name: string; address: string }[]): string {\n return formatAddressList(list)\n}\n\nfunction summaryLine(item: MessageSummary, index: number): string {\n const flags = [\n item.backedUp === false ? '\u672A\u5907\u4EFD' : item.backedUp === true ? '\u5DF2\u5907\u4EFD' : '',\n item.hasAttachments ? '\u6709\u9644\u4EF6' : '',\n item.unread ? '\u672A\u8BFB' : '',\n ].filter(Boolean)\n return `${String(index + 1)}. [id=${item.id}] ${stamp(item.date)} \u00B7 ${line(item.from.name || item.from.address)} \u00B7 ${line(item.subject)}${flags.length === 0 ? '' : ` \u00B7 ${flags.join(' \u00B7 ')}`}\\n ${item.snippet}`\n}\n\nfunction detailText(message: MessageDetail): string {\n const head = [\n `${message.backedUp === true ? '\u5DF2\u5907\u4EFD' : '\u670D\u52A1\u5668\u5B9E\u65F6'}`,\n `\u4E3B\u9898: ${line(message.subject)}`,\n `\u53D1\u4EF6\u4EBA: ${line(message.from.name)} <${message.from.address}>`,\n `\u6536\u4EF6\u4EBA: ${line(addressText(message.to))}`,\n ...(message.cc.length === 0 ? [] : [`\u6284\u9001: ${line(addressText(message.cc))}`]),\n `\u65F6\u95F4: ${stamp(message.date)}`,\n ...(message.attachments.length === 0\n ? []\n : [\n `\u9644\u4EF6: ${message.attachments.map((file) => `${file.filename}(${String(file.size)} B)`).join('\u3001')}`,\n ]),\n '',\n clip(message.text !== '' ? message.text : '(\u65E0\u6B63\u6587)'),\n ]\n return head.join('\\n')\n}\n\nfunction plainSchema(properties: Record<string, unknown>, required: string[] = []): Record<string, unknown> {\n return { type: 'object', properties, required, additionalProperties: false }\n}\n\nfunction textResult(\n _label: string,\n schema: Record<string, unknown> = { type: 'string' },\n): DshToolDefinition['output'] {\n return {\n schema,\n render: (_args, value) => [\n { type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) },\n ],\n }\n}\n\nfunction tool(\n name: string,\n description: string,\n parameters: Record<string, unknown>,\n output: DshToolDefinition['output'],\n execute: (args: unknown) => Promise<unknown>,\n): DshToolDefinition {\n return {\n name,\n description,\n parameters,\n output,\n timeoutMs: 120_000,\n isConcurrencySafe: () => true,\n presentCall: (args) => ({ card: 'generic', title: name, kind: 'read', rawInput: args }),\n execute: async (args: unknown) => execute(args),\n }\n}\n\n/**\n * Register the `mail_*` tools. The model can read mail, search the local\n * backup, and manage drafts; sending stays a user action in the panel, so no\n * tool can put a message on the wire without the user pressing send.\n */\nexport function registerMailTools(tools: DshTools, runtime: MailHarborRuntime): (() => void)[] {\n const activeAccount = (): Account => {\n const account = runtime.active()\n if (account === null) throw new Error('\u8BF7\u5148\u5728 MailHarbor \u9762\u677F\u91CC\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n return account\n }\n\n const definitions: DshToolDefinition[] = [\n tool(\n 'mail_status',\n '\u67E5\u770B\u90AE\u7BB1\u72B6\u6001\uFF1A\u8D26\u53F7\u3001\u662F\u5426\u5DF2\u767B\u5F55\u3001\u672C\u5730\u5907\u4EFD\u90AE\u4EF6\u6570\u3001\u4E0A\u6B21\u5907\u4EFD\u65F6\u95F4\u3002\u53EA\u8BFB\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u5185\u5BB9\u3002',\n plainSchema({}),\n textResult('\u72B6\u6001', {\n type: 'object',\n properties: {\n account: { type: 'string' },\n loggedIn: { type: 'boolean' },\n localTotal: { type: 'number' },\n lastBackup: { type: 'string' },\n backupRunning: { type: 'boolean' },\n },\n required: ['account', 'loggedIn', 'localTotal'],\n }),\n async () => {\n const account = activeAccount()\n const status = runtime.database(account).status()\n const backup = runtime.backupStatus(account)\n return {\n account: account.email,\n loggedIn: runtime.hasCredentials(account),\n localTotal: status.total,\n lastBackup: backup.lastSuccessAt ?? '',\n backupRunning: backup.running,\n }\n },\n ),\n tool(\n 'mail_list',\n '\u5217\u51FA\u672C\u5730\u5907\u4EFD\u6216\u670D\u52A1\u5668\u4E0A\u7684\u90AE\u4EF6\u6458\u8981\u3002source=local \u8BFB\u672C\u5730\u5907\u4EFD\uFF08\u5FEB\u3001\u53EF\u641C\u7D22\uFF09\uFF0Csource=server \u8BFB\u670D\u52A1\u5668\u5B9E\u65F6\u3002',\n plainSchema({\n source: { type: 'string', enum: ['local', 'server'], description: '\u9ED8\u8BA4 local' },\n mailbox: { type: 'string', description: '\u90AE\u7BB1\u6587\u4EF6\u5939\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 all\uFF08\u672C\u5730\uFF09\u6216 INBOX\uFF08\u670D\u52A1\u5668\uFF09' },\n query: { type: 'string', description: '\u5173\u952E\u8BCD\uFF0C\u4EC5\u672C\u5730\u641C\u7D22' },\n limit: { type: 'number', description: '\u6700\u591A\u8FD4\u56DE\u591A\u5C11\u5C01\uFF0C\u9ED8\u8BA4 20\uFF0C\u6700\u5927 50' },\n }),\n textResult('\u90AE\u4EF6\u5217\u8868', {\n type: 'object',\n properties: {\n source: { type: 'string' },\n total: { type: 'number' },\n items: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n subject: { type: 'string' },\n from: { type: 'string' },\n date: { type: 'string' },\n unread: { type: 'boolean' },\n backedUp: { type: 'boolean' },\n },\n required: ['id', 'subject', 'from', 'date', 'unread', 'backedUp'],\n },\n },\n text: { type: 'string' },\n },\n required: ['source', 'total', 'items', 'text'],\n }),\n async (args) => {\n const account = activeAccount()\n const source = textOf(args, 'source') === 'server' ? 'server' : 'local'\n const mailbox =\n textOf(args, 'mailbox') !== '' ? textOf(args, 'mailbox') : source === 'server' ? 'INBOX' : 'all'\n const limit = Math.min(MAX_LIST, Math.max(1, numberOf(args, 'limit') ?? 20))\n const query = textOf(args, 'query')\n let items: MessageSummary[] = []\n let total = 0\n if (source === 'server') {\n const provider = runtime.provider(account)\n const result = await provider.list(mailbox, limit)\n total = result.total\n const db = runtime.database(account)\n items = result.items.map((message) => ({\n id: String(message.uid),\n accountId: account.id,\n uid: message.uid,\n messageId: message.messageId,\n subject: message.subject,\n from: message.from,\n to: message.to,\n date: message.date,\n unread: !message.read,\n flagged: false,\n hasAttachments: false,\n snippet: '',\n mailbox,\n backedUp: message.messageId !== '' && db.hasMessageId(message.messageId),\n }))\n } else {\n const page = runtime.database(account).list({\n mailbox,\n query,\n limit,\n offset: 0,\n })\n total = page.total\n const readState = runtime.readState(account)\n items = page.items.map((item) => ({\n ...item,\n accountId: account.id,\n unread:\n item.messageId !== undefined && item.messageId !== ''\n ? readState.isUnread(`mid:${item.messageId}`)\n : readState.isUnread(`local:${item.id}`),\n backedUp: true,\n }))\n }\n const head = `${source === 'server' ? '\u670D\u52A1\u5668\u5B9E\u65F6' : '\u672C\u5730\u5907\u4EFD'} \u00B7 ${mailbox} \u00B7 ${String(items.length)}/${String(total)} \u5C01`\n return {\n source,\n total,\n items: items.map((item) => ({\n id: item.id,\n subject: item.subject,\n from: item.from.name || item.from.address,\n date: stamp(item.date),\n unread: item.unread,\n backedUp: item.backedUp ?? true,\n })),\n text:\n items.length === 0 ? `${head}\\n\uFF08\u6CA1\u6709\u90AE\u4EF6\uFF09` : `${head}\\n\\n${items.map(summaryLine).join('\\n')}`,\n }\n },\n ),\n tool(\n 'mail_read',\n '\u8BFB\u53D6\u4E00\u5C01\u90AE\u4EF6\u7684\u5168\u6587\u3002\u672C\u5730\u5907\u4EFD\u7528 id\uFF1B\u670D\u52A1\u5668\u90AE\u4EF6\u7528 uid \u5E76\u4F20 source=server\u3001mailbox\u3002',\n plainSchema(\n {\n id: { type: 'string', description: '\u672C\u5730\u90AE\u4EF6 id \u6216\u670D\u52A1\u5668 uid' },\n source: { type: 'string', enum: ['local', 'server'], description: '\u9ED8\u8BA4 local' },\n mailbox: { type: 'string', description: '\u670D\u52A1\u5668\u6587\u4EF6\u5939\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 INBOX' },\n },\n ['id'],\n ),\n textResult('\u90AE\u4EF6\u5168\u6587', {\n type: 'object',\n properties: {\n subject: { type: 'string' },\n from: { type: 'string' },\n date: { type: 'string' },\n text: { type: 'string' },\n attachments: { type: 'array', items: { type: 'object' } },\n },\n required: ['subject', 'from', 'date', 'text', 'attachments'],\n }),\n async (args) => {\n const account = activeAccount()\n const id = textOf(args, 'id')\n if (id === '') throw new Error('\u7F3A\u5C11\u90AE\u4EF6 id')\n const source = textOf(args, 'source') === 'server' ? 'server' : 'local'\n if (source === 'server') {\n const mailbox = textOf(args, 'mailbox') !== '' ? textOf(args, 'mailbox') : 'INBOX'\n const message = await runtime.provider(account).read(mailbox, Number(id))\n const detail = {\n ...message,\n id: String(message.uid),\n accountId: account.id,\n cc: message.cc,\n replyTo: [],\n unread: !message.read,\n flagged: false,\n hasAttachments: message.attachments.length > 0,\n snippet: '',\n mailbox,\n backedUp: message.messageId !== '' && runtime.database(account).hasMessageId(message.messageId),\n } as MessageDetail\n return {\n subject: detail.subject,\n from: `${detail.from.name} <${detail.from.address}>`.trim(),\n date: stamp(detail.date),\n text: clip(detail.text),\n attachments: detail.attachments.map((file) => ({\n filename: file.filename,\n contentType: file.contentType,\n size: file.size,\n })),\n }\n }\n const detail = runtime.database(account).get(Number(id))\n if (detail === undefined) throw new Error('\u90AE\u4EF6\u4E0D\u5B58\u5728')\n return {\n subject: detail.subject,\n from: `${detail.from.name} <${detail.from.address}>`.trim(),\n date: stamp(detail.date),\n text: clip(detail.text),\n attachments: detail.attachments.map((file) => ({\n filename: file.filename,\n contentType: file.contentType,\n size: file.size,\n })),\n }\n },\n ),\n tool(\n 'mail_search',\n '\u5728\u672C\u5730\u5907\u4EFD\u4E2D\u5168\u6587\u641C\u7D22\uFF08\u4E3B\u9898\u3001\u53D1\u4EF6\u4EBA\u3001\u6536\u4EF6\u4EBA\u3001\u6B63\u6587\u3001\u9644\u4EF6\u540D\uFF09\u3002\u53EA\u8BFB\u3002',\n plainSchema(\n {\n query: { type: 'string', description: '\u641C\u7D22\u5173\u952E\u8BCD' },\n limit: { type: 'number', description: '\u6700\u591A\u8FD4\u56DE\u591A\u5C11\u5C01\uFF0C\u9ED8\u8BA4 20' },\n },\n ['query'],\n ),\n textResult('\u641C\u7D22\u7ED3\u679C'),\n async (args) => {\n const account = activeAccount()\n const query = textOf(args, 'query')\n if (query === '') throw new Error('\u7F3A\u5C11\u641C\u7D22\u5173\u952E\u8BCD')\n const limit = Math.min(MAX_LIST, Math.max(1, numberOf(args, 'limit') ?? 20))\n const page = runtime.database(account).list({ mailbox: 'all', query, limit, offset: 0 })\n const readState = runtime.readState(account)\n return page.items.length === 0\n ? `\u6CA1\u6709\u5339\u914D\u300C${query}\u300D\u7684\u90AE\u4EF6`\n : `\u641C\u7D22\u300C${query}\u300D\uFF1A${String(page.total)} \u5C01\u4E2D\u524D ${String(page.items.length)} \u5C01\\n\\n${page.items\n .map((item, index) =>\n summaryLine(\n {\n ...item,\n accountId: account.id,\n unread: readState.isUnread(`local:${item.id}`),\n backedUp: true,\n },\n index,\n ),\n )\n .join('\\n')}`\n },\n ),\n tool(\n 'mail_drafts',\n '\u5217\u51FA\u5F53\u524D\u8D26\u53F7\u7684\u8349\u7A3F\u3002\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u53D1\u9001\u3002',\n plainSchema({}),\n textResult('\u8349\u7A3F\u5217\u8868', {\n type: 'object',\n properties: { drafts: { type: 'array', items: { type: 'object' } }, text: { type: 'string' } },\n required: ['drafts', 'text'],\n }),\n async () => {\n const account = activeAccount()\n const drafts = runtime\n .drafts(account)\n .list()\n .map((draft) => ({\n id: draft.id,\n subject: draft.subject,\n to: draft.to,\n updatedAt: draft.updatedAt,\n }))\n return {\n drafts,\n text:\n drafts.length === 0\n ? '\uFF08\u6CA1\u6709\u8349\u7A3F\uFF09'\n : drafts\n .map(\n (draft, index) =>\n `${String(index + 1)}. [${draft.id}] ${stamp(draft.updatedAt)} \u00B7 \u2192 ${line(draft.to)} \u00B7 ${line(draft.subject)}`,\n )\n .join('\\n'),\n }\n },\n ),\n tool(\n 'mail_draft',\n '\u65B0\u5EFA\u6216\u66F4\u65B0\u4E00\u5C01\u8349\u7A3F\u3002\u53EA\u5199\u672C\u5730\u8349\u7A3F\uFF0C\u4E0D\u4F1A\u53D1\u9001\uFF1B\u53D1\u9001\u4ECD\u9700\u7528\u6237\u5728\u9762\u677F\u91CC\u786E\u8BA4\u3002',\n plainSchema(\n {\n id: { type: 'string', description: '\u66F4\u65B0\u5DF2\u6709\u8349\u7A3F\u65F6\u4F20\u5165' },\n to: { type: 'string' },\n cc: { type: 'string' },\n bcc: { type: 'string' },\n subject: { type: 'string' },\n html: { type: 'string', description: '\u6B63\u6587 HTML' },\n inReplyTo: { type: 'string' },\n references: { type: 'string' },\n },\n ['subject'],\n ),\n textResult('\u8349\u7A3F', {\n type: 'object',\n properties: { id: { type: 'string' }, subject: { type: 'string' }, updatedAt: { type: 'string' } },\n required: ['id', 'subject', 'updatedAt'],\n }),\n async (args) => {\n const account = activeAccount()\n const draft = runtime.drafts(account).save({\n ...(textOf(args, 'id') !== '' ? { id: textOf(args, 'id') } : {}),\n to: textOf(args, 'to'),\n cc: textOf(args, 'cc'),\n bcc: textOf(args, 'bcc'),\n subject: textOf(args, 'subject'),\n html: textOf(args, 'html'),\n inReplyTo: textOf(args, 'inReplyTo'),\n references: textOf(args, 'references'),\n })\n return { id: draft.id, subject: draft.subject, updatedAt: draft.updatedAt }\n },\n ),\n tool(\n 'mail_backup',\n '\u89E6\u53D1\u4E00\u6B21\u589E\u91CF\u5907\u4EFD\uFF08\u540E\u53F0\u6267\u884C\uFF09\u3002\u5907\u4EFD\u4F1A\u8BFB\u53D6\u670D\u52A1\u5668\u4E0A\u7684\u5168\u90E8\u90AE\u4EF6\uFF0C\u53EF\u80FD\u6301\u7EED\u51E0\u5206\u949F\uFF1B\u53EA\u5728\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\u8C03\u7528\u3002',\n plainSchema({}),\n textResult('\u5907\u4EFD\u72B6\u6001', {\n type: 'object',\n properties: { started: { type: 'boolean' }, message: { type: 'string' } },\n required: ['started', 'message'],\n }),\n async () => {\n const account = activeAccount()\n try {\n runtime.startBackup(account)\n return { started: true, message: '\u5907\u4EFD\u5DF2\u5728\u540E\u53F0\u5F00\u59CB\uFF0C\u8FDB\u5EA6\u53EF\u5728\u9762\u677F\u7684\u5907\u4EFD\u5F39\u7A97\u67E5\u770B' }\n } catch (error) {\n return { started: false, message: error instanceof Error ? error.message : String(error) }\n }\n },\n ),\n ]\n\n return definitions.map((definition) => tools.register(definition))\n}\n\nexport { detailText }\n", "import { createApiHandler } from './api.js'\nimport type { DshContext, DshLlm, DshWorkspaceRegistry } from './dsh.js'\nimport { MailHarborRuntime } from './runtime.js'\nimport { registerMailTools } from './tools.js'\n\n/** Loader row name shown by the DSH plugin manager. */\nexport const name = 'mailharbor'\n/** Host services this plugin needs: the HTTP carrier and the tool registry. */\nexport const inject = ['webServer', 'tools']\nexport { VERSION } from './version.js'\n\nconst ROUTE = '/mail-plugin'\n\nfunction optionalService<T>(ctx: DshContext, name: string): T | undefined {\n try {\n const direct = (ctx as unknown as Record<string, unknown>)[name]\n if (direct !== undefined && direct !== null) return direct as T\n } catch {\n // Guarded service access throws when the service is not injected.\n }\n try {\n return (ctx.get?.(name) as T | undefined) ?? undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * MailHarbor Host half: one HTTP prefix for the panel, plus `mail_*` agent\n * tools when the composition provides the tool registry.\n */\nexport function apply(ctx: DshContext): void {\n let registry: DshWorkspaceRegistry | undefined\n // The registry is optional: wait for it in the background instead of\n // blocking the whole plugin on compositions that do not mount workspaces.\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (scope: DshContext) => {\n try {\n registry = scope.workspaceRegistry\n } catch {\n registry = undefined\n }\n })\n }\n\n const runtime = new MailHarborRuntime({\n workspaceRegistry: () => registry,\n sessions: optionalService(ctx, 'sessions'),\n llm: () => {\n if (typeof ctx.get !== 'function') return undefined\n try {\n return ctx.get.call(ctx, 'llm') as DshLlm | undefined\n } catch {\n return undefined\n }\n },\n })\n const handler = createApiHandler(runtime)\n const disposers: (() => void)[] = []\n\n const start = (): void => {\n disposers.push(ctx.webServer.register({ kind: 'prefix', path: ROUTE, handler }))\n // `tools` is declared in `inject`; Cordis guarantees it is present here.\n if (ctx.tools !== undefined) disposers.push(...registerMailTools(ctx.tools, runtime))\n }\n\n const stop = (): void => {\n for (const dispose of disposers.splice(0)) {\n try {\n dispose()\n } catch {\n // A disposer that already ran must not block the others.\n }\n }\n void runtime.dispose()\n }\n\n if (typeof ctx.effect === 'function') {\n ctx.effect(() => {\n start()\n return stop\n }, 'mailharbor: routes and tools')\n } else {\n start()\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["import type { Address } from '../shared/types.js'\n\n/** Split `a@b, \"Doe, John\" <c@d>` on commas that are not inside quotes or angle brackets. */\nexport function splitAddressHeader(value: string): string[] {\n const text = value.trim()\n if (text === '') return []\n const parts: string[] = []\n let current = ''\n let quoted = false\n let angle = 0\n for (const ch of text) {\n if (ch === '\"') quoted = !quoted\n if (ch === '<' && !quoted) angle += 1\n if (ch === '>' && !quoted && angle > 0) angle -= 1\n if ((ch === ',' || ch === ';') && !quoted && angle === 0) {\n parts.push(current)\n current = ''\n continue\n }\n current += ch\n }\n parts.push(current)\n return parts.map((part) => part.trim()).filter((part) => part !== '')\n}\n\n/** Parse one address token into `{ name, address }`. */\nexport function parseAddress(token: string): Address | null {\n const text = token.trim()\n if (text === '') return null\n const angled = /^(.*?)\\s*<([^>]+)>\\s*$/.exec(text)\n if (angled !== null) {\n const name = (angled[1] ?? '').trim().replace(/^\"|\"$/g, '').trim()\n const address = (angled[2] ?? '').trim()\n return address === '' ? null : { name, address }\n }\n if (/^[^\\s@]+@[^\\s@]+$/.test(text)) return { name: '', address: text }\n return null\n}\n\n/** Parse a whole To/Cc header. Invalid tokens are dropped. */\nexport function parseAddressList(value: string): Address[] {\n return splitAddressHeader(value)\n .map(parseAddress)\n .filter((address): address is Address => address !== null)\n}\n\n/** Format addresses back into a header value. */\nexport function formatAddressList(list: Address[]): string {\n return list\n .map((address) => (address.name !== '' ? `${address.name} <${address.address}>` : address.address))\n .filter((text) => text !== '')\n .join(', ')\n}\n\n/** First address of a list, or an empty address. */\nexport function firstAddress(list: Address[] | undefined): Address {\n const first = list?.[0]\n return first === undefined ? { name: '', address: '' } : { name: first.name, address: first.address }\n}\n\n/** The bare address from `Name <a@b>` or `a@b`. */\nexport function addressOf(value: string): string {\n const angled = /<([^>]+)>/.exec(value)\n return (angled === null ? value : (angled[1] ?? '')).trim()\n}\n\n/** Validate a single address without a full RFC parser. */\nexport function isEmail(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+$/.test(value.trim())\n}\n", "import nodemailer from 'nodemailer'\nimport type { SendResult, SmtpSettings } from '../shared/types.js'\nimport { addressOf, isEmail } from './address.js'\n\nexport interface SmtpCredentials {\n user: string\n pass?: string\n accessToken?: string\n}\n\nexport interface SmtpSecurityOptions {\n /**\n * Accept a self-signed certificate. Deliberately not reachable from the HTTP\n * API: production connections always verify the server certificate.\n */\n allowSelfSigned?: boolean\n}\n\nexport interface OutgoingMail {\n from?: string\n to: string\n cc?: string\n bcc?: string\n subject: string\n text: string\n html?: string\n inReplyTo?: string\n references?: string\n attachments?: { filename: string; contentType: string; content: Buffer }[]\n}\n\nfunction transportOptions(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n security: SmtpSecurityOptions = {},\n) {\n const secure = settings.secure !== false\n return {\n host: settings.host,\n port: Number(settings.port),\n secure,\n // Plain submission ports must upgrade; never send credentials in the clear.\n requireTLS: !secure,\n auth:\n credentials.accessToken !== undefined\n ? { type: 'OAuth2' as const, user: credentials.user, accessToken: credentials.accessToken }\n : { user: credentials.user, pass: credentials.pass ?? '' },\n tls: { rejectUnauthorized: security.allowSelfSigned !== true, servername: settings.host },\n connectionTimeout: 20_000,\n greetingTimeout: 20_000,\n socketTimeout: 120_000,\n pool: false,\n }\n}\n\nfunction createTransport(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n security: SmtpSecurityOptions = {},\n) {\n return nodemailer.createTransport(transportOptions(settings, credentials, security))\n}\n\n/** Validate one recipient header into envelope addresses. */\nexport function recipientsOf(value: string): string[] {\n return value\n .split(/[,;]/)\n .map((part) => addressOf(part))\n .filter((address) => isEmail(address))\n}\n\n/** Check credentials and connectivity without sending a message. */\nexport async function verifySmtp(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n security: SmtpSecurityOptions = {},\n): Promise<{ ok: true; host: string; port: number }> {\n const transport = createTransport(settings, credentials, security)\n try {\n await transport.verify()\n return { ok: true, host: settings.host, port: Number(settings.port) }\n } finally {\n transport.close()\n }\n}\n\n/**\n * Send one message: plain text plus an optional HTML alternative and files.\n * nodemailer builds the MIME tree, so encodings, long headers and 8-bit bodies\n * do not depend on hand-written RFC 5322 code.\n */\nexport async function sendMail(\n settings: SmtpSettings,\n credentials: SmtpCredentials,\n account: string,\n mail: OutgoingMail,\n security: SmtpSecurityOptions = {},\n): Promise<SendResult> {\n const from = addressOf(mail.from ?? account)\n if (!isEmail(from)) throw new Error('\u53D1\u4EF6\u4EBA\u5730\u5740\u65E0\u6548')\n const to = recipientsOf(mail.to)\n const cc = recipientsOf(mail.cc ?? '')\n if (to.length === 0) throw new Error('\u6536\u4EF6\u4EBA\u4E0D\u80FD\u4E3A\u7A7A')\n\n const transport = createTransport(settings, credentials, security)\n try {\n const info = await transport.sendMail({\n from: settings.fromName === '' ? from : { name: settings.fromName, address: from },\n to,\n ...(cc.length > 0 ? { cc } : {}),\n subject: mail.subject,\n text: mail.text,\n ...(mail.html !== undefined && mail.html.trim() !== '' ? { html: mail.html } : {}),\n ...(mail.inReplyTo !== undefined && mail.inReplyTo !== '' ? { inReplyTo: mail.inReplyTo } : {}),\n ...(mail.references !== undefined && mail.references !== '' ? { references: mail.references } : {}),\n ...(mail.attachments !== undefined && mail.attachments.length > 0\n ? {\n attachments: mail.attachments.map((file) => ({\n filename: file.filename,\n content: file.content,\n contentType: file.contentType,\n })),\n }\n : {}),\n })\n return {\n messageId: String(info.messageId ?? ''),\n accepted: (info.accepted ?? []).map((entry) => String(entry)),\n rejected: (info.rejected ?? []).map((entry) => String(entry)),\n }\n } finally {\n transport.close()\n }\n}\n", "import { ImapFlow } from 'imapflow'\nimport { simpleParser } from 'mailparser'\nimport type { Address, AttachmentMeta, ImapSettings } from '../shared/types.js'\nimport { addressOf, formatAddressList, parseAddressList } from './address.js'\n\nexport interface ImapCredentials {\n user: string\n pass?: string\n accessToken?: string\n}\n\nexport interface ImapMessage {\n uid: number\n messageId: string\n subject: string\n from: Address\n to: Address[]\n cc: Address[]\n date: string\n size: number\n flags: string[]\n read: boolean\n}\n\nexport interface ImapMailbox {\n path: string\n name: string\n specialUse: string | null\n messages: number\n}\n\nexport interface ImapFullMessage extends ImapMessage {\n text: string\n html: string | null\n attachments: AttachmentMeta[]\n inReplyTo: string\n references: string[]\n}\n\nexport interface ImapProviderOptions {\n connectionTimeoutMs?: number\n socketTimeoutMs?: number\n /** Called whenever the connection is replaced, for logging. */\n onReconnect?: (reason: string) => void\n}\n\nfunction normalizeId(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/^<|>$/g, '')\n .trim()\n}\n\nfunction isoDate(value: unknown): string {\n const date =\n value instanceof Date\n ? value\n : value === undefined || value === null\n ? undefined\n : new Date(String(value))\n return date instanceof Date && !Number.isNaN(date.getTime()) ? date.toISOString() : ''\n}\n\nfunction flagsOf(value: unknown): string[] {\n if (value === undefined || value === null) return []\n return [...(value as Iterable<unknown>)].map((flag) => String(flag))\n}\n\nfunction isSeen(flags: string[]): boolean {\n return flags.some((flag) => flag.toLowerCase() === '\\\\seen')\n}\n\nfunction attachmentList(parsed: Awaited<ReturnType<typeof simpleParser>>): AttachmentMeta[] {\n return (parsed.attachments ?? []).map((attachment, index) => ({\n id: String(index),\n filename: String(attachment.filename ?? 'attachment'),\n contentType: String(attachment.contentType ?? 'application/octet-stream'),\n size: Number(attachment.size ?? attachment.content?.length ?? 0),\n }))\n}\n\n/**\n * One IMAP account. The connection is reused across operations and every\n * operation is serialized, because an IMAP session is stateful: two interleaved\n * commands on one socket corrupt each other. A failed connection is discarded\n * and rebuilt on the next call, so a dropped socket never wedges the account.\n */\nexport class ImapProvider {\n private readonly settings: ImapSettings\n private readonly credentials: ImapCredentials\n private readonly options: ImapProviderOptions\n private client: ImapFlow | null = null\n private connecting: Promise<ImapFlow> | null = null\n private queue: Promise<unknown> = Promise.resolve()\n private disposed = false\n private mailboxCache: { at: number; items: ImapMailbox[] } | null = null\n private readonly countCache = new Map<string, number>()\n private readonly pageCache = new Map<string, { at: number; total: number; items: ImapMessage[] }>()\n private keepalive: ReturnType<typeof setInterval> | null = null\n private static readonly MAILBOX_TTL_MS = 60_000\n\n constructor(settings: ImapSettings, credentials: ImapCredentials, options: ImapProviderOptions = {}) {\n this.settings = settings\n this.credentials = credentials\n this.options = options\n }\n\n private async connect(): Promise<ImapFlow> {\n if (this.client !== null && this.client.usable) return this.client\n if (this.connecting !== null) return this.connecting\n this.connecting = (async () => {\n const client = new ImapFlow({\n host: this.settings.host,\n port: Number(this.settings.port),\n secure: this.settings.secure !== false,\n auth:\n this.credentials.accessToken !== undefined\n ? { user: this.credentials.user, accessToken: this.credentials.accessToken }\n : { user: this.credentials.user, pass: this.credentials.pass ?? '' },\n logger: false,\n tls: { rejectUnauthorized: true },\n connectionTimeout: this.options.connectionTimeoutMs ?? 15_000,\n greetingTimeout: this.options.connectionTimeoutMs ?? 15_000,\n socketTimeout: this.options.socketTimeoutMs ?? 60_000,\n })\n client.on('error', () => {\n // Errors surface through the operation that triggered them; keep the\n // process-wide handler so an idle socket error is never unhandled.\n })\n client.on('close', () => {\n if (this.client === client) {\n this.client = null\n this.stopKeepalive()\n }\n })\n try {\n await client.connect()\n } catch (error) {\n throw new Error(\n `IMAP \u8FDE\u63A5\u5931\u8D25 (${this.settings.host}:${String(this.settings.port)}): ${messageOf(error)}`,\n )\n }\n this.client = client\n this.startKeepalive(client)\n return client\n })()\n try {\n return await this.connecting\n } finally {\n this.connecting = null\n }\n }\n\n private startKeepalive(client: ImapFlow): void {\n this.stopKeepalive()\n // A NOOP every few minutes keeps the server from dropping an idle session,\n // so opening server mail does not pay a TLS + LOGIN round trip.\n this.keepalive = setInterval(() => {\n if (this.disposed || this.client !== client || !client.usable) return\n void this.run(async (active) => {\n await active.noop()\n }).catch(() => {\n // A failed keepalive is handled by the next operation.\n })\n }, 240_000)\n this.keepalive.unref?.()\n }\n\n private stopKeepalive(): void {\n if (this.keepalive !== null) {\n clearInterval(this.keepalive)\n this.keepalive = null\n }\n }\n\n /** Connect eagerly so the first list does not pay the handshake. */\n async warm(): Promise<void> {\n await this.connect()\n }\n\n private async reset(reason: string): Promise<void> {\n const client = this.client\n this.client = null\n if (client !== null) {\n try {\n await client.logout()\n } catch {\n // Already gone.\n }\n }\n if (!this.disposed) this.options.onReconnect?.(reason)\n }\n\n /** Serialize one operation on this account's connection. */\n private run<T>(operation: (client: ImapFlow) => Promise<T>): Promise<T> {\n const task = this.queue.then(\n async () => {\n const client = await this.connect()\n try {\n return await operation(client)\n } catch (error) {\n // A protocol or socket failure invalidates the session; reconnect next time.\n if (isFatal(error)) await this.reset(messageOf(error))\n throw error\n }\n },\n async () => {\n const client = await this.connect()\n return operation(client)\n },\n )\n this.queue = task.then(\n () => undefined,\n () => undefined,\n )\n return task\n }\n\n async check(): Promise<{ ok: true; mailboxes: { name: string; count: number }[] }> {\n return this.run(async (client) => {\n const boxes = await this.listMailboxes(client)\n return { ok: true as const, mailboxes: boxes.map((box) => ({ name: box.path, count: box.messages })) }\n })\n }\n\n async mailboxes(force = false): Promise<ImapMailbox[]> {\n const cached = this.mailboxCache\n if (!force && cached !== null && Date.now() - cached.at < ImapProvider.MAILBOX_TTL_MS) {\n return cached.items\n }\n const items = await this.run((client) => this.listMailboxes(client))\n this.mailboxCache = { at: Date.now(), items }\n return items\n }\n\n private async listMailboxes(client: ImapFlow): Promise<ImapMailbox[]> {\n const listed = await client.list()\n const boxes = listed.filter((box) => box.flags?.has?.('\\\\Noselect') !== true)\n return boxes.map((box) => ({\n path: box.path,\n name: box.name ?? box.path,\n specialUse: box.specialUse ?? null,\n messages: this.countCache.get(box.path) ?? 0,\n }))\n }\n\n async list(mailbox: string, limit: number, offset = 0): Promise<{ total: number; items: ImapMessage[] }> {\n const cacheKey = `${mailbox}:${String(limit)}:${String(offset)}`\n const cached = this.pageCache.get(cacheKey)\n if (cached !== undefined && Date.now() - cached.at < 15_000) {\n return { total: cached.total, items: cached.items }\n }\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const total = Number((client.mailbox as { exists?: number } | undefined)?.exists ?? 0)\n const cachedBox = this.mailboxCache?.items.find((box) => box.path === mailbox)\n if (cachedBox !== undefined) cachedBox.messages = total\n this.countCache.set(mailbox, total)\n const end = total - offset\n if (total === 0 || end < 1) return { total, items: [] }\n const start = Math.max(1, end - limit + 1)\n const items: ImapMessage[] = []\n for await (const message of client.fetch(`${String(start)}:${String(end)}`, {\n uid: true,\n envelope: true,\n flags: true,\n internalDate: true,\n size: true,\n })) {\n items.push(toMessage(message as unknown as RawImapMessage))\n }\n items.sort((left, right) => right.date.localeCompare(left.date))\n this.pageCache.set(cacheKey, { at: Date.now(), total, items })\n if (this.pageCache.size > 30) {\n const oldest = this.pageCache.keys().next().value\n if (oldest !== undefined) this.pageCache.delete(oldest)\n }\n return { total, items }\n } finally {\n lock.release()\n }\n })\n }\n\n async search(\n mailbox: string,\n options: {\n keywords?: string[]\n from?: string\n subject?: string\n since?: string\n before?: string\n limit?: number\n },\n ): Promise<{ items: ImapMessage[]; total: number }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const criteria: Record<string, unknown> = {}\n if ((options.from ?? '') !== '') criteria.from = options.from\n if ((options.subject ?? '') !== '') criteria.subject = options.subject\n const keywords = (options.keywords ?? []).filter((keyword) => keyword !== '')\n if (keywords.length > 0) criteria.text = keywords.join(' ')\n if ((options.since ?? '') !== '') criteria.since = new Date(`${String(options.since)}T00:00:00Z`)\n if ((options.before ?? '') !== '') criteria.before = new Date(`${String(options.before)}T23:59:59Z`)\n if (Object.keys(criteria).length === 0) return { items: [], total: 0 }\n const found = await client.search(criteria, { uid: true })\n const uids = (Array.isArray(found) ? found : [])\n .map((uid) => Number(uid))\n .filter((uid) => Number.isFinite(uid))\n .sort((left, right) => left - right)\n const take = uids.slice(-Math.min(100, options.limit ?? 60))\n if (take.length === 0) return { items: [], total: uids.length }\n const wanted = new Set(take)\n const items: ImapMessage[] = []\n const first = take[0] ?? 0\n const last = take[take.length - 1] ?? 0\n for await (const message of client.fetch(\n `${String(first)}:${String(last)}`,\n { uid: true, envelope: true, flags: true, internalDate: true, size: true },\n { uid: true },\n )) {\n const item = toMessage(message as unknown as RawImapMessage)\n if (wanted.has(item.uid)) items.push(item)\n }\n items.sort((left, right) => right.date.localeCompare(left.date))\n return { items, total: uids.length }\n } finally {\n lock.release()\n }\n })\n }\n\n async read(mailbox: string, uid: number): Promise<ImapFullMessage> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const message = await client.fetchOne(\n uid,\n { uid: true, source: true, flags: true, envelope: true, internalDate: true, size: true },\n { uid: true },\n )\n if (message === false || message === undefined || message.source === undefined) {\n throw new Error('\u90AE\u4EF6\u4E0D\u5B58\u5728')\n }\n const parsed = await simpleParser(message.source)\n const base = toMessage(message as unknown as RawImapMessage)\n const html =\n typeof parsed.html === 'string'\n ? parsed.html\n : Buffer.isBuffer(parsed.html)\n ? parsed.html.toString('utf8')\n : null\n return {\n ...base,\n text: typeof parsed.text === 'string' ? parsed.text : '',\n html: html === '' ? null : html,\n attachments: attachmentList(parsed),\n inReplyTo: normalizeId(parsed.inReplyTo),\n references: String(parsed.references ?? '')\n .split(/\\s+/)\n .map(normalizeId)\n .filter((item) => item !== ''),\n }\n } finally {\n lock.release()\n }\n })\n }\n\n async attachment(\n mailbox: string,\n uid: number,\n part: number,\n ): Promise<{ filename: string; contentType: string; dataBase64: string }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const message = await client.fetchOne(uid, { uid: true, source: true }, { uid: true })\n if (message === false || message === undefined || message.source === undefined) {\n throw new Error('\u90AE\u4EF6\u4E0D\u5B58\u5728')\n }\n const parsed = await simpleParser(message.source)\n const attachment = parsed.attachments?.[part]\n if (attachment === undefined) throw new Error('\u9644\u4EF6\u4E0D\u5B58\u5728')\n return {\n filename: String(attachment.filename ?? 'attachment'),\n contentType: String(attachment.contentType ?? 'application/octet-stream'),\n dataBase64: attachment.content.toString('base64'),\n }\n } finally {\n lock.release()\n }\n })\n }\n\n async unseen(): Promise<{ count: number; items: { uid: number; messageId: string }[] }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock('INBOX')\n try {\n const found = await client.search({ seen: false }, { uid: true })\n const uids = Array.isArray(found) ? found : []\n const items: { uid: number; messageId: string }[] = []\n if (uids.length > 0) {\n const range = uids.slice(-200).join(',')\n for await (const message of client.fetch(range, { uid: true, envelope: true }, { uid: true })) {\n items.push({\n uid: Number(message.uid),\n messageId: normalizeId(message.envelope?.messageId),\n })\n }\n }\n return { count: uids.length, items }\n } finally {\n lock.release()\n }\n })\n }\n\n async setSeen(mailbox: string, uid: number, seen: boolean): Promise<{ read: boolean; flags: string[] }> {\n this.pageCache.clear()\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n if (seen) await client.messageFlagsAdd(uid, ['\\\\Seen'], { uid: true })\n else await client.messageFlagsRemove(uid, ['\\\\Seen'], { uid: true })\n const message = await client.fetchOne(uid, { uid: true, flags: true }, { uid: true })\n const flags = flagsOf(message === false || message === undefined ? [] : message.flags)\n return { read: isSeen(flags), flags }\n } finally {\n lock.release()\n }\n })\n }\n\n /** Set \\Seen by Message-ID, which is how local-archive rows reach the server. */\n async setSeenByMessageId(\n mailbox: string,\n messageId: string,\n seen: boolean,\n ): Promise<{ found: boolean; uid?: number; read?: boolean }> {\n return this.run(async (client) => {\n const lock = await client.getMailboxLock(mailbox)\n try {\n const id = normalizeId(messageId)\n let found = await client.search({ header: { 'Message-ID': id } }, { uid: true })\n if (!Array.isArray(found) || found.length === 0) {\n found = await client.search({ header: { 'Message-ID': `<${id}>` } }, { uid: true })\n }\n const uids = Array.isArray(found) ? found : []\n const uid = uids[uids.length - 1]\n if (uid === undefined) return { found: false }\n if (seen) await client.messageFlagsAdd(uid, ['\\\\Seen'], { uid: true })\n else await client.messageFlagsRemove(uid, ['\\\\Seen'], { uid: true })\n const message = await client.fetchOne(uid, { uid: true, flags: true }, { uid: true })\n const flags = flagsOf(message === false || message === undefined ? [] : message.flags)\n return { found: true, uid: Number(uid), read: isSeen(flags) }\n } finally {\n lock.release()\n }\n })\n }\n\n async dispose(): Promise<void> {\n this.disposed = true\n this.pageCache.clear()\n this.stopKeepalive()\n await this.reset('dispose')\n }\n}\n\nexport interface RawImapMessage {\n uid: number\n envelope?: {\n messageId?: string\n subject?: string\n from?: { name?: string; address?: string }[]\n to?: { name?: string; address?: string }[]\n cc?: { name?: string; address?: string }[]\n date?: string | Date\n }\n internalDate?: string | Date\n size?: number\n flags?: Iterable<unknown>\n}\n\nfunction toMessage(message: RawImapMessage): ImapMessage {\n const flags = flagsOf(message.flags)\n const addressList = (value: { name?: string; address?: string }[] | undefined): Address[] =>\n (value ?? [])\n .map((entry) => ({ name: entry.name ?? '', address: entry.address ?? '' }))\n .filter((address) => address.address !== '')\n return {\n uid: Number(message.uid),\n messageId: normalizeId(message.envelope?.messageId),\n subject: String(message.envelope?.subject ?? '(\u65E0\u4E3B\u9898)'),\n from: addressList(message.envelope?.from)[0] ?? { name: '', address: '' },\n to: addressList(message.envelope?.to),\n cc: addressList(message.envelope?.cc),\n date: isoDate(message.internalDate ?? message.envelope?.date),\n size: Number(message.size ?? 0),\n flags,\n read: isSeen(flags),\n }\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\n/** Errors that mean the session itself is unusable. */\nfunction isFatal(error: unknown): boolean {\n if (error instanceof Error) {\n const code = (error as { code?: string }).code ?? ''\n if (\n /^(NoConnection|ConnectionClosed|ConnectionNotAvailable|EConnectionClosed|ETIMEDOUT|ECONNRESET|EPIPE)$/.test(\n code,\n )\n ) {\n return true\n }\n return /connection (closed|lost|not available)|socket/i.test(error.message)\n }\n return false\n}\n\n/** Exported for tests and for the service layer that formats addresses. */\nexport { addressOf, formatAddressList, parseAddressList }\n", "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { randomUUID } from 'node:crypto'\nimport type {\n Account,\n Address,\n AppState,\n AttachmentMeta,\n BackupStatus,\n Draft,\n DraftAttachment,\n MessageDetail,\n MessageSummary,\n NewAccountInput,\n Page,\n Project,\n SendResult,\n Task,\n SettingsView,\n} from '../shared/types.js'\nimport { addressOf, formatAddressList, parseAddressList } from './address.js'\nimport {\n buildAccount,\n normalizeAccount,\n saveAccounts,\n saveSettings,\n type MailHarborConfig,\n} from './config.js'\nimport { resolveUserPath } from './paths.js'\nimport { pickDirectory } from './picker.js'\nimport { BackupEngine } from './backup.js'\nimport { HttpError, Router, assertTrusted, readJsonBody, sendJson, type RequestContext } from './http.js'\nimport { normalizeTranslated, summaryBlocks, textToHtml } from './ai.js'\nimport { appendConversation, createProject, type ProjectRecord } from './projects.js'\nimport type { TaskRecord } from './tasks.js'\nimport { VERSION } from './version.js'\nimport type { MailHarborRuntime } from './runtime.js'\nimport type { ImapProvider } from './imap.js'\nimport { verifySmtp } from './smtp.js'\nimport { escapeHtml, htmlToText } from './html.js'\n\nexport const API_PREFIX = '/mail-plugin/api'\n\n/** Build the request handler the Host mounts at `/mail-plugin`. */\nexport function createApiHandler(runtime: MailHarborRuntime) {\n const router = buildRouter(runtime)\n return async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', 'http://localhost')\n if (!url.pathname.startsWith(API_PREFIX)) {\n sendJson(res, 404, { error: 'Not found' })\n return\n }\n const pathname = url.pathname.slice(API_PREFIX.length) || '/'\n const method = (req.method ?? 'GET').toUpperCase()\n try {\n if (method !== 'GET' && method !== 'HEAD') assertTrusted(req)\n const match = router.match(method, pathname)\n if (match === null) throw new HttpError(404, `\u672A\u77E5\u63A5\u53E3: ${method} ${pathname}`)\n runtime.refresh()\n const context: RequestContext = {\n req,\n res,\n url,\n params: match.params,\n body: () =>\n readJsonBody(req, method === 'POST' && pathname === '/send' ? 25 * 1024 * 1024 : 2 * 1024 * 1024),\n }\n const result = await match.handler(context)\n if (result !== undefined) sendJson(res, 200, result)\n } catch (error) {\n if (res.headersSent) {\n res.end()\n return\n }\n sendJson(res, error instanceof HttpError ? error.status : 500, {\n error: error instanceof Error ? error.message : String(error),\n ...(error instanceof HttpError && error.code !== undefined ? { code: error.code } : {}),\n })\n }\n }\n}\n\nfunction buildRouter(runtime: MailHarborRuntime): Router {\n const router = new Router()\n\n router.get('/health', () => ({ ok: true, version: VERSION }))\n\n router.get('/state', () => appState(runtime))\n\n router.get('/folders', async ({ url }) => {\n const source = sourceOf(url)\n const requested = url.searchParams.get('accountId')\n if (source === 'server' && requested === 'all') {\n throw new HttpError(400, '\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD')\n }\n const accounts =\n requested === 'all'\n ? runtime.current.accounts.filter((item) => item.enabled !== false)\n : [accountFrom(runtime, url).account]\n if (source === 'server') {\n const provider = runtime.provider(accounts[0] as Account)\n const boxes = await provider.mailboxes(url.searchParams.get('refresh') === '1')\n return {\n items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages })),\n }\n }\n const counts = new Map<string, number>()\n let drafts = 0\n for (const account of accounts) {\n try {\n for (const box of runtime.database(account).mailboxes()) {\n counts.set(box.name, (counts.get(box.name) ?? 0) + box.count)\n }\n } catch {\n // An account without a backup simply contributes no folders.\n }\n try {\n drafts += runtime.drafts(account).list().length\n } catch {\n // Ignore a broken drafts file.\n }\n }\n const items = [...counts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count)\n items.unshift({ name: '__drafts', count: drafts })\n return { items }\n })\n\n router.get('/messages', async ({ url }) => {\n const source = sourceOf(url)\n const requested = url.searchParams.get('accountId')\n const mailbox = url.searchParams.get('mailbox') ?? 'all'\n const query = url.searchParams.get('query') ?? ''\n const offset = Number(url.searchParams.get('offset') ?? 0)\n const limit = Number(url.searchParams.get('limit') ?? runtime.current.render.listLimit)\n if (requested === 'all') {\n if (source === 'server') throw new HttpError(400, '\u7EDF\u4E00\u6536\u4EF6\u7BB1\u6682\u53EA\u652F\u6301\u672C\u5730\u5907\u4EFD')\n return listAllAccounts(runtime, { mailbox, query, offset, limit })\n }\n const { account } = accountFrom(runtime, url)\n if (source === 'server') {\n const items = await listServer(runtime, account, mailbox, query, limit, offset)\n return { ...items, offset, limit }\n }\n const page = runtime\n .database(account)\n .list({ mailbox: mailbox === '' ? 'all' : mailbox, query, offset, limit })\n return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) }\n })\n\n router.get('/messages/lookup', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = url.searchParams.get('key') ?? ''\n if (key.startsWith('local:')) {\n const id = Number(key.slice(6))\n const detail = runtime.database(account).get(id)\n if (detail === undefined) throw new HttpError(404, '\u90AE\u4EF6\u4E0D\u5B58\u5728')\n return { id: String(id), mailbox: detail.mailbox, subject: detail.subject }\n }\n if (key.startsWith('mid:')) {\n const found = runtime.database(account).findByMessageId(key.slice(4))\n if (found === undefined) throw new HttpError(404, '\u672C\u5730\u5907\u4EFD\u91CC\u627E\u4E0D\u5230\u8FD9\u5C01\u90AE\u4EF6')\n return { id: String(found.id), mailbox: found.mailbox, subject: '' }\n }\n throw new HttpError(400, '\u65E0\u6CD5\u5B9A\u4F4D\u8FD9\u5C01\u90AE\u4EF6')\n })\n\n router.get('/messages/:id', async ({ url, params }) => {\n const { account } = accountFrom(runtime, url)\n const source = sourceOf(url)\n const mailbox = url.searchParams.get('mailbox') ?? 'INBOX'\n return loadDetail(runtime, account, source, params.id ?? '', mailbox)\n })\n\n router.post('/messages/:id/read', async ({ url, params, body }) => {\n const { account } = accountFrom(runtime, url)\n const source = sourceOf(url)\n const mailbox = url.searchParams.get('mailbox') ?? 'INBOX'\n const payload = await body()\n const read = payload.read !== false\n if (source === 'server') {\n const provider = runtime.provider(account)\n const result = await provider.setSeen(mailbox, Number(params.id), read)\n return { ok: true, read: result.read }\n }\n const db = runtime.database(account)\n const detail = db.get(Number(params.id))\n if (detail === undefined) throw new HttpError(404, '\u90AE\u4EF6\u4E0D\u5B58\u5728')\n const store = runtime.readState(account)\n const key = detail.messageId !== '' ? `mid:${detail.messageId}` : `local:${detail.id}`\n store.setUnread(key, !read)\n // Mirror the change to the server when the archived message still exists there.\n if (typeof detail.messageId === 'string' && detail.messageId !== '') {\n try {\n const provider = runtime.provider(account)\n const mailboxName = detail.mailbox === '' ? 'INBOX' : detail.mailbox\n await provider.setSeenByMessageId(mailboxName, detail.messageId, read)\n } catch {\n // Offline or removed on the server: the local record is the source of truth.\n }\n }\n return { ok: true, read }\n })\n\n router.get('/attachment', async ({ url, res }) => {\n const { account } = accountFrom(runtime, url)\n const source = sourceOf(url)\n if (source === 'server') {\n const mailbox = url.searchParams.get('mailbox') ?? 'INBOX'\n const uid = Number(url.searchParams.get('uid') ?? 0)\n const part = Number(url.searchParams.get('part') ?? 0)\n const provider = runtime.provider(account)\n const file = await provider.attachment(mailbox, uid, part)\n writeAttachment(res, file.filename, file.contentType, Buffer.from(file.dataBase64, 'base64'))\n return undefined\n }\n const emailId = Number(url.searchParams.get('emailId') ?? 0)\n const attachmentId = Number(url.searchParams.get('attachmentId') ?? 0)\n const file = runtime.database(account).attachment(emailId, attachmentId)\n if (file === undefined) throw new HttpError(404, '\u9644\u4EF6\u4E0D\u5B58\u5728')\n writeAttachment(res, file.filename, file.contentType, Buffer.from(file.data))\n return undefined\n })\n\n router.get('/drafts', ({ url }) => {\n const { account } = accountFrom(runtime, url)\n return { items: runtime.drafts(account).list() }\n })\n\n router.post('/drafts', async ({ url, body }) => {\n const { account } = accountFrom(runtime, url)\n const payload = await body()\n const draft = runtime.drafts(account).save(normalizeDraftInput(payload, account.id))\n return { draft }\n })\n\n router.delete('/drafts/:id', ({ url, params }) => {\n const { account } = accountFrom(runtime, url)\n const removed = runtime.drafts(account).remove(params.id ?? '')\n if (!removed) throw new HttpError(404, '\u8349\u7A3F\u4E0D\u5B58\u5728')\n return { ok: true }\n })\n\n router.post('/send', async ({ url, body }) => {\n const { account } = accountFrom(runtime, url)\n const payload = await body()\n const result = await sendFromPayload(runtime, account, payload)\n return result\n })\n\n router.get('/unread', async ({ url }) => {\n const requested = url.searchParams.get('accountId')\n const accounts =\n requested === null || requested === '' || requested === 'all'\n ? runtime.current.accounts.filter((item) => item.enabled !== false)\n : [accountById(runtime, requested)]\n let server = 0\n let answered = 0\n await Promise.all(\n accounts.map(async (account) => {\n if (!runtime.hasCredentials(account)) return\n try {\n const result = await runtime.provider(account).unseen()\n server += result.count\n answered += 1\n } catch {\n // An unreachable server falls back to the local counter.\n }\n }),\n )\n if (answered > 0) return { unread: server, source: 'server' }\n const local = accounts.reduce((sum, account) => sum + runtime.readState(account).count(), 0)\n return { unread: local, source: 'local' }\n })\n\n router.get('/accounts', () => ({ accounts: runtime.current.accounts }))\n\n router.post('/accounts', async ({ body }) => {\n const payload = await body()\n const input = payload as unknown as NewAccountInput & { connect?: boolean; remember?: boolean }\n if (typeof input.email !== 'string') throw new HttpError(400, '\u7F3A\u5C11\u90AE\u7BB1\u5730\u5740')\n const password = typeof input.password === 'string' ? input.password : ''\n const config = runtime.current\n const account = buildAccount(input, config.accounts)\n if (password === '') throw new HttpError(400, '\u8BF7\u8F93\u5165\u5BC6\u7801\u6216\u5E94\u7528\u4E13\u7528\u5BC6\u7801')\n\n if (input.connect !== false) {\n const provider = await verifyImap(account, password)\n await provider.dispose()\n try {\n await verifySmtp(account.smtp, { user: account.username, pass: password })\n } catch {\n // IMAP is the gate; SMTP problems are reported by the connection check.\n }\n }\n runtime.credentials.remember(account, password, input.remember !== false)\n const accounts = [...config.accounts, account]\n saveAccounts(accounts, account.id)\n runtime.refresh()\n return { account, state: appState(runtime) }\n })\n\n router.patch('/accounts/:id', async ({ params, body }) => {\n const config = runtime.current\n const id = params.id ?? ''\n const existing = config.accounts.find((account) => account.id === id)\n if (existing === undefined) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n const payload = await body()\n const merged = {\n ...existing,\n label: typeof payload.label === 'string' ? payload.label : existing.label,\n username: typeof payload.username === 'string' ? payload.username : existing.username,\n imap: { ...existing.imap, ...asObject(payload.imap) },\n smtp: { ...existing.smtp, ...asObject(payload.smtp) },\n caldav:\n payload.caldav === null\n ? null\n : payload.caldav === undefined\n ? existing.caldav\n : { ...asObject(payload.caldav) },\n enabled: payload.enabled === undefined ? existing.enabled : payload.enabled !== false,\n }\n const account = normalizeAccount(\n merged as Partial<Account>,\n 0,\n config.accounts.map((item) => item.id),\n )\n // Preserve the id and creation time; the editor cannot reassign them.\n account.id = existing.id\n account.createdAt = existing.createdAt\n const accounts = config.accounts.map((item) => (item.id === existing.id ? account : item))\n saveAccounts(accounts, config.activeAccountId)\n runtime.invalidate(existing.id)\n return { account }\n })\n\n router.delete('/accounts/:id', ({ params }) => {\n const config = runtime.current\n const id = params.id ?? ''\n const accounts = config.accounts.filter((account) => account.id !== id)\n if (accounts.length === config.accounts.length) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n const active = config.activeAccountId === id ? (accounts[0]?.id ?? null) : config.activeAccountId\n saveAccounts(accounts, active)\n runtime.invalidate(id)\n return { ok: true }\n })\n\n router.post('/accounts/:id/activate', ({ params }) => {\n const config = runtime.current\n const id = params.id ?? ''\n if (!config.accounts.some((account) => account.id === id)) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n saveAccounts(config.accounts, id)\n runtime.refresh()\n return { ok: true, activeAccountId: id }\n })\n\n router.post('/accounts/:id/login', async ({ params, body }) => {\n const account = accountById(runtime, params.id)\n const payload = await body()\n const password = typeof payload.password === 'string' ? payload.password : ''\n if (password === '') throw new HttpError(400, '\u8BF7\u8F93\u5165\u5BC6\u7801\u6216\u5E94\u7528\u4E13\u7528\u5BC6\u7801')\n const provider = await verifyImap(account, password)\n await provider.dispose()\n const stored = runtime.credentials.remember(account, password, payload.remember !== false)\n return { ok: true, stored: stored.stored }\n })\n\n router.post('/accounts/:id/logout', ({ params }) => {\n const account = accountById(runtime, params.id)\n runtime.credentials.forget(account)\n runtime.invalidate(account.id)\n return { ok: true }\n })\n\n router.post('/accounts/:id/check', async ({ params }) => {\n const account = accountById(runtime, params.id)\n const provider = runtime.provider(account)\n const imap = await provider.check()\n let smtp: { ok: boolean; error?: string } = { ok: false }\n try {\n const credentials = runtime.credentialsFor(account)\n await verifySmtp(account.smtp, credentials)\n smtp = { ok: true }\n } catch (error) {\n smtp = { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n return { ok: true, imap, smtp }\n })\n\n router.get('/ai/thread', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = (url.searchParams.get('key') ?? '').trim()\n if (key === '') throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key')\n return { thread: runtime.aiThreads(account).get(key) }\n })\n\n router.delete('/ai/thread', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = (url.searchParams.get('key') ?? '').trim()\n if (key === '') throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key')\n return { ok: runtime.aiThreads(account).clear(key) }\n })\n\n router.delete('/ai/cards', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = (url.searchParams.get('key') ?? '').trim()\n const cardId = (url.searchParams.get('cardId') ?? '').trim()\n if (key === '' || cardId === '') throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key \u6216\u5361\u7247 id')\n return { thread: runtime.aiThreads(account).removeCard(key, cardId) }\n })\n\n router.post('/ai/cards', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const key = String(payload.key ?? '').trim()\n if (key === '' || key.length > 200) throw new HttpError(400, '\u7F3A\u5C11\u7EBF\u7A0B key')\n const kind = String(payload.kind ?? '')\n if (!['summary', 'reply', 'translate', 'answer'].includes(kind)) {\n throw new HttpError(400, `\u672A\u77E5\u7684\u5361\u7247\u7C7B\u578B: ${kind}`)\n }\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n const config = runtime.current\n const instruction = typeof payload.instruction === 'string' ? payload.instruction : ''\n const language = payload.language === 'en' ? 'en' : 'zh'\n const target = payload.target === 'en' ? 'en' : 'zh'\n const ai = runtime.ai()\n try {\n let output = ''\n let html: string | undefined\n let meta: Record<string, string> | undefined\n if (kind === 'summary') {\n output = await ai.summarize(detail)\n } else if (kind === 'reply') {\n output = await ai.reply(account, detail, { instruction, language })\n html = textToHtml(output)\n meta = {\n to: detail.from.address,\n subject: /^re:/i.test(detail.subject) ? detail.subject : `Re: ${detail.subject}`,\n inReplyTo: detail.messageId ?? '',\n references: [detail.references.join(' '), detail.messageId ?? ''].join(' ').trim(),\n language,\n }\n } else if (kind === 'translate') {\n const source = detail.html !== null && detail.html !== '' ? detail.html : detail.text\n const translated = normalizeTranslated(await ai.translate(source, target))\n output = translated.text\n html = translated.html\n meta = { target }\n } else {\n const earlier =\n runtime\n .aiThreads(account)\n .get(key)\n ?.cards.filter((card) => card.kind === 'answer')\n .map((card) => card.output) ?? []\n output = await ai.ask(detail, instruction, earlier)\n }\n const card = {\n id: randomUUID(),\n kind: kind as 'summary' | 'reply' | 'translate' | 'answer',\n instruction,\n output,\n ...(html === undefined ? {} : { html }),\n ...(meta === undefined ? {} : { meta }),\n provider: config.ai.provider,\n model: config.ai.model,\n createdAt: new Date().toISOString(),\n }\n const thread = runtime.aiThreads(account).appendCard({\n key,\n accountId: account.id,\n subject: detail.subject,\n messageId: detail.messageId ?? '',\n card,\n })\n return { card, thread }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/search', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const query = String(payload.query ?? '').trim()\n if (query === '') throw new HttpError(400, '\u641C\u7D22\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A')\n const source = sourceOfPayload(payload)\n const mailbox = String(payload.mailbox ?? 'all')\n try {\n const filters = await runtime.ai().parseSearch(query)\n const hasCondition =\n filters.keywords.length > 0 ||\n filters.from !== '' ||\n filters.subject !== '' ||\n filters.since !== '' ||\n filters.before !== ''\n if (!hasCondition) throw new HttpError(400, '\u6CA1\u6709\u7406\u89E3\u51FA\u53EF\u7528\u7684\u641C\u7D22\u6761\u4EF6\uFF0C\u8BF7\u6362\u4E2A\u8BF4\u6CD5')\n if (source === 'server') {\n const provider = runtime.provider(account)\n const result = await provider.search(mailbox === 'all' ? 'INBOX' : mailbox, {\n ...filters,\n limit: 60,\n })\n return {\n items: result.items.map((item) => ({ ...item, accountId: account.id })),\n total: result.total,\n offset: 0,\n limit: 60,\n ai: filters,\n }\n }\n const database = runtime.database(account)\n // Local search is cross-folder: a search should find a mail wherever it is.\n const keywords = [...filters.keywords]\n let page = database.searchAdvanced({ ...filters, subject: '', keywords, mailbox: 'all', limit: 60 })\n if (page.total === 0 && filters.subject !== '') {\n page = database.searchAdvanced({\n ...filters,\n subject: '',\n keywords: [...keywords, filters.subject],\n mailbox: 'all',\n limit: 60,\n })\n }\n return {\n ...page,\n items: page.items.map((item) => ({ ...item, accountId: account.id })),\n ai: filters,\n }\n } catch (error) {\n if (error instanceof HttpError) throw error\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/compose', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const mode = payload.mode === 'polish' ? 'polish' : payload.mode === 'shorten' ? 'shorten' : 'draft'\n const html = typeof payload.html === 'string' ? payload.html : ''\n const instruction = typeof payload.instruction === 'string' ? payload.instruction : ''\n const language = payload.language === 'en' ? 'en' : 'zh'\n try {\n const text = await runtime.ai().compose(account, { mode, instruction, html, language })\n return { text, html: /<[a-z][\\s\\S]*>/i.test(text) ? text : textToHtml(text) }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.get('/ai/status', ({ url }) => {\n const requested = url.searchParams.get('accountId')\n const account =\n requested !== null && requested !== '' && requested !== 'all'\n ? runtime.current.accounts.find((item) => item.id === requested)\n : runtime.active()\n return runtime.ai().status(account ?? null)\n })\n\n router.get('/ai/models', async () => ({ providers: await runtime.ai().models() }))\n\n router.post('/ai/summary', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n try {\n const summary = await runtime.ai().summarize(detail)\n return { summary, blocks: summaryBlocks(summary) }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/reply', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n const language = payload.language === 'en' ? 'en' : 'zh'\n const instruction = typeof payload.instruction === 'string' ? payload.instruction : ''\n try {\n const text = await runtime.ai().reply(account, detail, { instruction, language })\n return { text, html: textToHtml(text) }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/translate', async ({ body }) => {\n const payload = await body()\n const html = typeof payload.html === 'string' ? payload.html : ''\n const target = payload.target === 'en' ? 'en' : 'zh'\n try {\n const translated = normalizeTranslated(await runtime.ai().translate(html, target))\n return { text: translated.text, html: translated.html }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.get('/ai/style', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n return { style: runtime.ai().style(account) }\n })\n\n router.put('/ai/style', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n try {\n const style = runtime.ai().saveStyle(account, String(payload.profile ?? ''))\n return { style }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n router.post('/ai/style/learn', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n try {\n const samples = runtime.database(account).sentMessages(100)\n const style = await runtime.ai().learnStyle(account, samples)\n return { style }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n // ---- tasks ----\n\n router.get('/tasks', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const items = runtime.tasks().list({\n accountId: account.id,\n status: url.searchParams.get('status') ?? '',\n projectId: url.searchParams.get('projectId') ?? '',\n query: url.searchParams.get('query') ?? '',\n })\n return { items: items.map(taskView) }\n })\n\n router.post('/tasks', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const task = runtime.tasks().create({\n accountId: account.id,\n title: String(payload.title ?? ''),\n notes: typeof payload.notes === 'string' ? payload.notes : '',\n due: typeof payload.due === 'string' ? payload.due : '',\n status: typeof payload.status === 'string' ? payload.status : 'NEEDS-ACTION',\n priority: Number(payload.priority ?? 0),\n owner: typeof payload.owner === 'string' ? payload.owner : '',\n projectId: typeof payload.projectId === 'string' ? payload.projectId : null,\n messageKey: typeof payload.messageKey === 'string' ? payload.messageKey : '',\n messageSubject: typeof payload.messageSubject === 'string' ? payload.messageSubject : '',\n })\n return { task: taskView(task) }\n })\n\n router.patch('/tasks/:id', async ({ params, body }) => {\n const payload = await body()\n const task = runtime.tasks().update(String(params.id ?? ''), {\n ...(typeof payload.title === 'string' ? { title: payload.title } : {}),\n ...(typeof payload.notes === 'string' ? { notes: payload.notes } : {}),\n ...(typeof payload.due === 'string' ? { due: payload.due } : {}),\n ...(typeof payload.status === 'string' ? { status: payload.status } : {}),\n ...(typeof payload.priority === 'number' ? { priority: payload.priority } : {}),\n ...(typeof payload.owner === 'string' ? { owner: payload.owner } : {}),\n ...(payload.projectId === null || typeof payload.projectId === 'string'\n ? { projectId: payload.projectId }\n : {}),\n })\n return { task: taskView(task) }\n })\n\n router.delete('/tasks/:id', ({ params }) => {\n const removed = runtime.tasks().remove(String(params.id ?? ''))\n if (!removed) throw new HttpError(404, '\u4EFB\u52A1\u4E0D\u5B58\u5728')\n return { ok: true }\n })\n\n router.post('/tasks/extract', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const detail = await loadDetail(\n runtime,\n account,\n sourceOfPayload(payload),\n String(payload.id ?? ''),\n mailboxOfPayload(payload),\n )\n try {\n const suggestions = await runtime.ai().extractTasks(detail)\n return { suggestions }\n } catch (error) {\n throw aiHttpError(error)\n }\n })\n\n // ---- projects ----\n\n router.get('/projects', () => ({ items: runtime.projects().list().map(projectView) }))\n\n router.get('/projects/for', ({ url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = url.searchParams.get('key') ?? ''\n return {\n project:\n key === ''\n ? null\n : (() => {\n const found = runtime.projects().findByMessage(account.id, key)\n return found === undefined ? null : projectView(found)\n })(),\n }\n })\n\n router.post('/projects/pick-directory', async () => {\n const path = await pickDirectory()\n return { path }\n })\n\n router.post('/projects', async ({ body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const name = String(payload.name ?? '').trim()\n const root = String(payload.root ?? '').trim() || runtime.current.projects.root\n const conversation = typeof payload.conversation === 'string' ? payload.conversation : ''\n const message = asObject(payload.message)\n const key = typeof message.key === 'string' ? message.key : ''\n const result = await createProject({\n store: runtime.projects(),\n input: {\n name,\n root,\n conversation,\n accountId: account.id,\n ...(key === ''\n ? {}\n : {\n message: {\n accountId: account.id,\n key,\n subject: String(message.subject ?? ''),\n date: String(message.date ?? ''),\n },\n }),\n ...(key === '' ? {} : { thread: { accountId: account.id, key } }),\n },\n registry: runtime.workspaceRegistry,\n sessions: runtime.sessions,\n })\n return { project: projectView(result.project), warnings: result.warnings }\n })\n\n router.post('/projects/:id/messages', async ({ params, body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const project = runtime.projects().addMessage(String(params.id ?? ''), {\n accountId: account.id,\n key: String(payload.key ?? ''),\n subject: String(payload.subject ?? ''),\n date: String(payload.date ?? ''),\n })\n if (project === undefined) throw new HttpError(404, '\u9879\u76EE\u4E0D\u5B58\u5728')\n return { project: projectView(project) }\n })\n\n router.delete('/projects/:id/messages', ({ params, url }) => {\n const account = accountFromPayload(runtime, { accountId: url.searchParams.get('accountId') ?? undefined })\n const key = url.searchParams.get('key') ?? ''\n const project = runtime.projects().removeMessage(String(params.id ?? ''), account.id, key)\n if (project === undefined) throw new HttpError(404, '\u9879\u76EE\u4E0D\u5B58\u5728')\n return { project: projectView(project) }\n })\n\n router.post('/projects/:id/transfer', async ({ params, body }) => {\n const payload = await body()\n const account = accountFromPayload(runtime, payload)\n const project = runtime.projects().get(String(params.id ?? ''))\n if (project === undefined) throw new HttpError(404, '\u9879\u76EE\u4E0D\u5B58\u5728')\n const conversation = typeof payload.conversation === 'string' ? payload.conversation : ''\n if (conversation.trim() === '') throw new HttpError(400, '\u6CA1\u6709\u53EF\u8F6C\u79FB\u7684 AI \u8BB0\u5F55')\n appendConversation(project, conversation)\n const key = typeof payload.key === 'string' ? payload.key : ''\n const updated =\n key === ''\n ? runtime.projects().update(project.id, {})\n : runtime.projects().addThread(project.id, { accountId: account.id, key })\n return { project: projectView(updated ?? project) }\n })\n\n router.get('/settings', () => settingsView(runtime.current))\n\n router.patch('/settings', async ({ body }) => {\n const payload = await body()\n const patch: Record<string, unknown> = {}\n if (typeof payload.signature === 'string') patch.signature = payload.signature.slice(0, 5000)\n if (payload.ai !== undefined) patch.ai = asObject(payload.ai)\n if (payload.read !== undefined) patch.read = asObject(payload.read)\n if (payload.render !== undefined) patch.render = asObject(payload.render)\n if (payload.projects !== undefined) {\n const root = String(asObject(payload.projects).root ?? '').trim()\n if (root === '') throw new HttpError(400, '\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A')\n patch.projects = { root: resolveUserPath(root, '\u9879\u76EE\u76EE\u5F55') }\n }\n saveSettings(patch)\n runtime.refresh()\n return { settings: settingsView(runtime.current) }\n })\n\n router.get('/backup/status', ({ url }) => {\n const { account } = accountFrom(runtime, url)\n return runtime.backupStatus(account)\n })\n\n router.post('/backup/start', async ({ url }) => {\n const { account } = accountFrom(runtime, url)\n runtime.startBackup(account)\n return { ok: true, status: runtime.backupStatus(account) }\n })\n\n router.post('/backup/stop', ({ url }) => {\n const { account } = accountFrom(runtime, url)\n const stopped = runtime.stopBackup(account)\n return { ok: stopped, status: runtime.backupStatus(account) }\n })\n\n return router\n}\n\n/** Load one message from either source; shared by the mail and AI routes. */\nasync function loadDetail(\n runtime: MailHarborRuntime,\n account: Account,\n source: 'local' | 'server',\n id: string,\n mailbox: string,\n): Promise<MessageDetail> {\n if (source === 'server') {\n const message = await runtime.provider(account).read(mailbox, Number(id))\n return serverDetail(runtime, account, message, mailbox)\n }\n const detail = runtime.database(account).get(Number(id))\n if (detail === undefined) throw new HttpError(404, '\u90AE\u4EF6\u4E0D\u5B58\u5728')\n return localDetail(runtime, account, detail)\n}\n\n/** Merge the local backup of every enabled account into one newest-first page. */\nfunction listAllAccounts(\n runtime: MailHarborRuntime,\n options: { mailbox: string; query: string; offset: number; limit: number },\n): Page<MessageSummary> {\n const accounts = runtime.current.accounts.filter((item) => item.enabled !== false)\n const collected: MessageSummary[] = []\n let total = 0\n for (const account of accounts) {\n try {\n const page = runtime.database(account).list({\n mailbox: options.mailbox === '' ? 'all' : options.mailbox,\n query: options.query,\n offset: 0,\n limit: options.offset + options.limit,\n })\n total += page.total\n for (const item of page.items) collected.push(localSummary(runtime, account, item))\n } catch {\n // An account without a backup contributes nothing.\n }\n }\n collected.sort((left, right) => right.date.localeCompare(left.date))\n return {\n items: collected.slice(options.offset, options.offset + options.limit),\n total,\n offset: options.offset,\n limit: options.limit,\n }\n}\n\nfunction accountFromPayload(runtime: MailHarborRuntime, payload: Record<string, unknown>): Account {\n const requested = typeof payload.accountId === 'string' ? payload.accountId : ''\n if (requested !== '' && requested !== 'all') {\n const found = runtime.current.accounts.find((item) => item.id === requested)\n if (found !== undefined) return found\n }\n const active = runtime.active()\n if (active !== null) return active\n throw new HttpError(400, '\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n}\n\nfunction sourceOfPayload(payload: Record<string, unknown>): 'local' | 'server' {\n return payload.source === 'server' ? 'server' : 'local'\n}\n\nfunction mailboxOfPayload(payload: Record<string, unknown>): string {\n return typeof payload.mailbox === 'string' && payload.mailbox !== '' ? payload.mailbox : 'INBOX'\n}\n\nfunction aiHttpError(error: unknown): HttpError {\n if (error instanceof HttpError) return error\n return new HttpError(400, error instanceof Error ? error.message : String(error))\n}\n\nfunction accountById(runtime: MailHarborRuntime, id: string | undefined): Account {\n const account = runtime.current.accounts.find((item) => item.id === id)\n if (account === undefined) throw new HttpError(404, '\u8D26\u53F7\u4E0D\u5B58\u5728')\n return account\n}\n\nfunction accountFrom(runtime: MailHarborRuntime, url: URL): { account: Account; config: MailHarborConfig } {\n const requested = url.searchParams.get('accountId')\n const config = runtime.current\n const account =\n requested === null || requested === '' || requested === 'active'\n ? (config.accounts.find((item) => item.id === config.activeAccountId) ?? config.accounts[0])\n : config.accounts.find((item) => item.id === requested)\n if (account === undefined) throw new HttpError(400, '\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n return { account, config }\n}\n\nfunction sourceOf(url: URL): 'local' | 'server' {\n return url.searchParams.get('source') === 'server' ? 'server' : 'local'\n}\n\nasync function verifyImap(account: Account, password: string): Promise<ImapProvider> {\n const { ImapProvider } = await import('./imap.js')\n const provider = new ImapProvider(account.imap, { user: account.username || account.email, pass: password })\n try {\n await provider.check()\n return provider\n } catch (error) {\n await provider.dispose()\n throw new HttpError(400, `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`)\n }\n}\n\nfunction appState(runtime: MailHarborRuntime): AppState {\n const config = runtime.current\n const account = config.accounts.find((item) => item.id === config.activeAccountId) ?? null\n return {\n version: VERSION,\n settings: settingsView(config),\n accounts: config.accounts,\n activeAccountId: config.activeAccountId,\n backup: account === null ? emptyBackup() : runtime.backupStatus(account),\n unread: account === null ? 0 : runtime.readState(account).count(),\n loggedIn: account !== null && runtime.hasCredentials(account),\n }\n}\n\nfunction emptyBackup(): BackupStatus {\n return {\n running: false,\n accountId: null,\n startedAt: null,\n finishedAt: null,\n lastSuccessAt: null,\n total: 0,\n added: 0,\n message: '',\n output: [],\n exitCode: null,\n }\n}\n\nfunction settingsView(config: MailHarborConfig): SettingsView {\n return {\n dataDir: config.dataDir,\n configFile: config.configFile,\n signature: config.signature,\n accounts: config.accounts,\n activeAccountId: config.activeAccountId,\n ai: {\n enabled: config.ai.enabled,\n provider: config.ai.provider,\n model: config.ai.model,\n styleSampleCount: config.ai.styleSampleCount,\n },\n read: { markOnOpen: config.read.markOnOpen, pollSeconds: config.read.pollSeconds },\n render: { listLimit: config.render.listLimit },\n projects: { root: config.projects.root },\n }\n}\n\nfunction taskView(task: TaskRecord): Task {\n return {\n id: task.id,\n accountId: task.accountId,\n projectId: task.projectId,\n title: task.title,\n notes: task.notes,\n due: task.due,\n status: task.status as Task['status'],\n priority: task.priority,\n owner: task.owner,\n messageKey: task.messageKey,\n messageSubject: task.messageSubject,\n dirty: task.dirty,\n createdAt: task.createdAt,\n updatedAt: task.updatedAt,\n completedAt: task.completedAt,\n }\n}\n\nfunction projectView(project: ProjectRecord): Project {\n return {\n id: project.id,\n name: project.name,\n path: project.path,\n workspaceId: project.workspaceId,\n sessionId: project.sessionId,\n accountIds: [...project.accountIds],\n messages: project.messages.map((message) => ({ ...message })),\n threads: project.threads.map((thread) => ({ ...thread })),\n createdAt: project.createdAt,\n updatedAt: project.updatedAt,\n }\n}\n\nfunction localSummary(runtime: MailHarborRuntime, account: Account, row: MessageSummary): MessageSummary {\n const store = runtime.readState(account)\n const key = row.messageId !== undefined && row.messageId !== '' ? `mid:${row.messageId}` : `local:${row.id}`\n return { ...row, accountId: account.id, unread: store.isUnread(key), backedUp: true }\n}\n\nfunction localDetail(runtime: MailHarborRuntime, account: Account, detail: MessageDetail): MessageDetail {\n return localSummary(runtime, account, detail) as MessageDetail\n}\n\nfunction serverSummary(\n account: Account,\n message: {\n uid: number\n messageId: string\n subject: string\n from: Address\n to: Address[]\n date: string\n read: boolean\n size: number\n },\n mailbox: string,\n backedUp: boolean,\n): MessageSummary {\n return {\n id: String(message.uid),\n accountId: account.id,\n uid: message.uid,\n messageId: message.messageId,\n subject: message.subject,\n from: message.from,\n to: message.to,\n date: message.date,\n unread: !message.read,\n flagged: false,\n hasAttachments: false,\n snippet: '',\n size: message.size,\n mailbox,\n backedUp,\n }\n}\n\nfunction serverDetail(\n runtime: MailHarborRuntime,\n account: Account,\n message: {\n uid: number\n messageId: string\n subject: string\n from: Address\n to: Address[]\n cc: Address[]\n date: string\n read: boolean\n size: number\n text: string\n html: string | null\n attachments: AttachmentMeta[]\n inReplyTo: string\n references: string[]\n },\n mailbox: string,\n): MessageDetail {\n let backedUp = false\n try {\n backedUp = message.messageId !== '' && runtime.database(account).hasMessageId(message.messageId)\n } catch {\n backedUp = false\n }\n return {\n id: String(message.uid),\n accountId: account.id,\n uid: message.uid,\n messageId: message.messageId,\n subject: message.subject,\n from: message.from,\n to: message.to,\n cc: message.cc,\n replyTo: [],\n date: message.date,\n unread: !message.read,\n flagged: false,\n hasAttachments: message.attachments.length > 0,\n snippet: '',\n size: message.size,\n mailbox,\n backedUp,\n text: message.text,\n html: message.html,\n attachments: message.attachments,\n inReplyTo: message.inReplyTo,\n references: message.references,\n }\n}\n\nasync function listServer(\n runtime: MailHarborRuntime,\n account: Account,\n mailbox: string,\n query: string,\n limit: number,\n offset = 0,\n): Promise<Omit<Page<MessageSummary>, 'offset' | 'limit'>> {\n const provider = runtime.provider(account)\n const result = await provider.list(mailbox === 'all' ? 'INBOX' : mailbox, Math.min(200, limit * 2), offset)\n let database: ReturnType<MailHarborRuntime['database']> | null = null\n try {\n database = runtime.database(account)\n } catch {\n database = null\n }\n const text = query.trim().toLowerCase()\n const items = result.items\n .filter((message) => {\n if (text === '') return true\n return [message.subject, message.from.name, message.from.address, formatAddressList(message.to)]\n .join(' ')\n .toLowerCase()\n .includes(text)\n })\n .slice(0, limit)\n .map((message) =>\n serverSummary(account, message, mailbox, database?.hasMessageId(message.messageId) ?? false),\n )\n return { items, total: result.total }\n}\n\nfunction writeAttachment(res: ServerResponse, filename: string, contentType: string, data: Buffer): void {\n res.statusCode = 200\n res.setHeader('content-type', contentType)\n res.setHeader('content-length', String(data.length))\n res.setHeader('content-disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`)\n res.setHeader('cache-control', 'no-store')\n res.setHeader('x-content-type-options', 'nosniff')\n res.end(data)\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {}\n}\n\nfunction normalizeDraftInput(payload: Record<string, unknown>, accountId: string): Partial<Draft> {\n const attachments: DraftAttachment[] = Array.isArray(payload.attachments)\n ? payload.attachments\n .map((item) => asObject(item))\n .filter((item) => typeof item.filename === 'string' && typeof item.data === 'string')\n .map((item) => ({\n filename: String(item.filename),\n contentType: typeof item.contentType === 'string' ? item.contentType : 'application/octet-stream',\n data: String(item.data),\n }))\n : []\n return {\n ...(typeof payload.id === 'string' ? { id: payload.id } : {}),\n accountId,\n to: stringOf(payload.to),\n cc: stringOf(payload.cc),\n bcc: stringOf(payload.bcc),\n subject: stringOf(payload.subject),\n html: stringOf(payload.html),\n attachments,\n ...(typeof payload.inReplyTo === 'string' ? { inReplyTo: payload.inReplyTo } : {}),\n ...(typeof payload.references === 'string' ? { references: payload.references } : {}),\n }\n}\n\nfunction stringOf(value: unknown): string {\n return typeof value === 'string' ? value : ''\n}\n\nasync function sendFromPayload(\n runtime: MailHarborRuntime,\n account: Account,\n payload: Record<string, unknown>,\n): Promise<{ ok: true; result: SendResult }> {\n const to = stringOf(payload.to)\n if (to.trim() === '') throw new HttpError(400, '\u6536\u4EF6\u4EBA\u4E0D\u80FD\u4E3A\u7A7A')\n const subject = stringOf(payload.subject)\n const html = stringOf(payload.html)\n const text = stringOf(payload.text) !== '' ? stringOf(payload.text) : htmlToText(html)\n const attachments = Array.isArray(payload.attachments)\n ? payload.attachments\n .map((item) => asObject(item))\n .filter((item) => typeof item.filename === 'string' && typeof item.data === 'string')\n .map((item) => ({\n filename: String(item.filename),\n contentType:\n typeof item.contentType === 'string' && item.contentType !== ''\n ? item.contentType\n : 'application/octet-stream',\n content: Buffer.from(String(item.data), 'base64'),\n }))\n : []\n const totalBytes = attachments.reduce((sum, file) => sum + file.content.length, 0)\n if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, '\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB')\n\n const { sendMail } = await import('./smtp.js')\n const signature = runtime.current.signature\n const bodyText = signature === '' ? text : `${text}\\n\\n--\\n${signature}`\n const bodyHtml =\n signature === '' ? html : `${html}<p>--</p><p>${escapeHtml(signature).replaceAll('\\n', '<br>')}</p>`\n\n const result = await sendMail(\n account.smtp,\n runtime.credentialsFor(account),\n account.username || account.email,\n {\n to,\n cc: stringOf(payload.cc),\n subject,\n text: bodyText,\n html: bodyHtml,\n ...(typeof payload.inReplyTo === 'string' ? { inReplyTo: payload.inReplyTo } : {}),\n ...(typeof payload.references === 'string' ? { references: payload.references } : {}),\n attachments,\n },\n )\n return { ok: true, result }\n}\n\n/** Kept for the tools layer and tests. */\nexport { addressOf, parseAddressList, randomUUID, backups }\n\nfunction backups(account: Account, runtime: MailHarborRuntime): BackupStatus {\n return runtime.backupStatus(account)\n}\n\nexport { htmlToText, escapeHtml, BackupEngine }\n", "import { createHash, randomUUID } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Account, CalDavSettings, ImapSettings, NewAccountInput, SmtpSettings } from '../shared/types.js'\nimport { CONFIG_PATH, DATA_ROOT, accountAttachmentsDir, accountDataDir, accountMailDb } from './paths.js'\n\nexport interface AiSettings {\n enabled: boolean\n provider: string\n model: string\n styleSampleCount: number\n maxChars: number\n}\n\nexport interface ReadSettings {\n markOnOpen: boolean\n pollSeconds: number\n}\n\nexport interface RenderSettings {\n listLimit: number\n}\n\nexport interface ProjectSettings {\n /** Default parent directory for projects created from mail. */\n root: string\n}\n\nexport interface OAuthSettings {\n google: { clientId: string; redirectPort: number; scopes: string[] }\n microsoft: { clientId: string; scopes: string[] }\n}\n\n/** The resolved configuration the rest of the Host reads. */\nexport interface MailHarborConfig {\n dataDir: string\n configFile: string\n accounts: Account[]\n activeAccountId: string | null\n signature: string\n ai: AiSettings\n read: ReadSettings\n render: RenderSettings\n projects: ProjectSettings\n oauth: OAuthSettings\n /** Bumped whenever the file is written; caches key off this value. */\n revision: number\n}\n\nexport interface AccountRuntime {\n account: Account\n root: string\n mailDb: string\n attachments: string\n keychainService: string\n imap: ImapSettings\n smtp: SmtpSettings\n auth: Account['auth']\n}\n\nconst DEFAULTS = {\n signature: '',\n ai: {\n enabled: true,\n provider: 'deepseek-account',\n model: 'deepseek-flash',\n styleSampleCount: 100,\n maxChars: 40000,\n },\n read: { markOnOpen: true, pollSeconds: 60 },\n render: { listLimit: 40 },\n projects: { root: join(homedir(), 'work', 'projects') },\n oauth: {\n google: { clientId: '', redirectPort: 8765, scopes: ['https://mail.google.com/'] },\n microsoft: {\n clientId: '',\n scopes: [\n 'offline_access',\n 'https://outlook.office.com/IMAP.AccessAsUser.All',\n 'https://outlook.office.com/SMTP.Send',\n ],\n },\n },\n} satisfies Omit<MailHarborConfig, 'dataDir' | 'configFile' | 'accounts' | 'activeAccountId' | 'revision'>\n\nconst ACCOUNT_ID = /^[A-Za-z0-9_-]{1,64}$/\nconst EMAIL = /^[^@\\s]+@[^@\\s]+$/\n\nfunction stringOr(value: unknown, fallback: string): string {\n return typeof value === 'string' ? value : fallback\n}\n\nfunction intOr(value: unknown, fallback: number, min: number, max: number): number {\n const parsed = Number(value)\n if (!Number.isInteger(parsed) || parsed < min || parsed > max) return fallback\n return parsed\n}\n\nfunction hostOr(value: unknown, fallback: string): string {\n const text = stringOr(value, fallback).trim()\n return text === '' ? fallback : text\n}\n\n/** Deterministic, filesystem-safe account id derived from the address. */\nexport function deriveAccountId(email: string, existing: Iterable<string> = []): string {\n const taken = new Set(existing)\n const base = `acc_${createHash('sha1').update(email.trim().toLowerCase()).digest('hex').slice(0, 8)}`\n if (!taken.has(base)) return base\n for (let attempt = 0; attempt < 100; attempt += 1) {\n const candidate = `acc_${createHash('sha1').update(`${email}:${attempt}`).digest('hex').slice(0, 8)}`\n if (!taken.has(candidate)) return candidate\n }\n return `acc_${randomUUID().slice(0, 8)}`\n}\n\n/** Validate and fill in one account record. Throws on unusable input. */\nexport function normalizeAccount(\n input: Partial<Account> & { email?: string },\n index = 0,\n existing: string[] = [],\n): Account {\n const email = stringOr(input.email, '').trim()\n if (!EMAIL.test(email)) throw new Error('\u90AE\u7BB1\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E')\n const id =\n typeof input.id === 'string' && ACCOUNT_ID.test(input.id)\n ? input.id\n : deriveAccountId(email, [...existing, `index:${String(index)}`])\n const domain = email.split('@')[1] ?? 'example.com'\n const username = stringOr(input.username, email).trim() || email\n return {\n id,\n label: stringOr(input.label, email).trim() || email,\n email,\n username,\n imap: {\n host: hostOr(input.imap?.host, `imap.${domain}`),\n port: intOr(input.imap?.port, 993, 1, 65535),\n secure: input.imap?.secure !== false,\n },\n smtp: {\n host: hostOr(input.smtp?.host, `smtp.${domain}`),\n port: intOr(input.smtp?.port, 465, 1, 65535),\n secure: input.smtp?.secure !== false,\n fromName: stringOr(input.smtp?.fromName, ''),\n },\n keychainService: stringOr(input.keychainService, `dsh-mailharbor:${id}`),\n dataDir: stringOr(input.dataDir, `accounts/${id}`),\n auth:\n input.auth?.type === 'oauth2'\n ? { type: 'oauth2', provider: input.auth.provider }\n : { type: 'password' },\n caldav: normalizeCaldav(input.caldav),\n enabled: input.enabled !== false,\n createdAt: stringOr(input.createdAt, new Date().toISOString()),\n }\n}\n\nfunction normalizeCaldav(value: CalDavSettings | null | undefined): CalDavSettings | null {\n if (value === null || value === undefined) return null\n const url = stringOr(value.url, '').trim()\n if (url === '') return null\n return {\n url,\n username: stringOr(value.username, '').trim(),\n ...(typeof value.calendarHref === 'string' && value.calendarHref !== ''\n ? { calendarHref: value.calendarHref }\n : {}),\n }\n}\n\n/** Read the config file, tolerating a missing or partly broken file. */\nfunction readFileConfig(): Record<string, unknown> {\n try {\n if (!existsSync(CONFIG_PATH)) return {}\n const parsed: unknown = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'))\n return parsed !== null && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\n/** Resolve the on-disk file into the runtime configuration. */\nexport function loadConfig(): MailHarborConfig {\n const file = readFileConfig()\n const rawAccounts = Array.isArray(file.accounts) ? file.accounts : []\n const accounts: Account[] = []\n for (const [index, raw] of rawAccounts.entries()) {\n if (raw === null || typeof raw !== 'object') continue\n try {\n accounts.push(\n normalizeAccount(\n raw as Partial<Account>,\n index,\n accounts.map((account) => account.id),\n ),\n )\n } catch {\n // A broken record is skipped rather than breaking every other account.\n }\n }\n // Deduplicate by id; the first record wins.\n const seen = new Set<string>()\n const unique = accounts.filter((account) => {\n if (seen.has(account.id)) return false\n seen.add(account.id)\n return true\n })\n const requested = stringOr(file.activeAccountId, '')\n const activeAccountId = unique.some((account) => account.id === requested)\n ? requested\n : (unique.find((account) => account.enabled !== false)?.id ?? null)\n const aiRaw = (file.ai ?? {}) as Record<string, unknown>\n const readRaw = (file.read ?? {}) as Record<string, unknown>\n const renderRaw = (file.render ?? {}) as Record<string, unknown>\n const projectsRaw = (file.projects ?? {}) as Record<string, unknown>\n const oauthRaw = (file.oauth ?? {}) as Record<string, unknown>\n const googleRaw = (oauthRaw.google ?? {}) as Record<string, unknown>\n const microsoftRaw = (oauthRaw.microsoft ?? {}) as Record<string, unknown>\n return {\n dataDir: DATA_ROOT,\n configFile: CONFIG_PATH,\n accounts: unique,\n activeAccountId,\n signature: stringOr(file.signature, DEFAULTS.signature),\n ai: {\n enabled: aiRaw.enabled !== false,\n provider: stringOr(aiRaw.provider, DEFAULTS.ai.provider),\n model: stringOr(aiRaw.model, DEFAULTS.ai.model),\n styleSampleCount: intOr(aiRaw.styleSampleCount, DEFAULTS.ai.styleSampleCount, 10, 200),\n maxChars: intOr(aiRaw.maxChars, DEFAULTS.ai.maxChars, 1000, 200000),\n },\n read: {\n markOnOpen: readRaw.markOnOpen !== false,\n pollSeconds: intOr(readRaw.pollSeconds, DEFAULTS.read.pollSeconds, 15, 3600),\n },\n render: {\n listLimit: intOr(renderRaw.listLimit, DEFAULTS.render.listLimit, 10, 200),\n },\n projects: {\n root: stringOr(projectsRaw.root, DEFAULTS.projects.root),\n },\n oauth: {\n google: {\n clientId: stringOr(googleRaw.clientId, DEFAULTS.oauth.google.clientId),\n redirectPort: intOr(googleRaw.redirectPort, DEFAULTS.oauth.google.redirectPort, 1024, 65535),\n scopes: Array.isArray(googleRaw.scopes)\n ? googleRaw.scopes.filter((item): item is string => typeof item === 'string')\n : DEFAULTS.oauth.google.scopes,\n },\n microsoft: {\n clientId: stringOr(microsoftRaw.clientId, DEFAULTS.oauth.microsoft.clientId),\n scopes: Array.isArray(microsoftRaw.scopes)\n ? microsoftRaw.scopes.filter((item): item is string => typeof item === 'string')\n : DEFAULTS.oauth.microsoft.scopes,\n },\n },\n revision: Date.now(),\n }\n}\n\n/** Merge `patch` into the config file atomically and return the merged file. */\nexport function writeConfigFile(patch: Record<string, unknown>): Record<string, unknown> {\n const current = readFileConfig()\n const next = { ...current, ...patch }\n if (current.accounts !== undefined || patch.accounts !== undefined) {\n next.accounts = patch.accounts ?? current.accounts\n }\n mkdirSync(DATA_ROOT, { recursive: true })\n const temp = `${CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`\n writeFileSync(temp, `${JSON.stringify(next, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temp, CONFIG_PATH)\n return next\n}\n\n/** Persist the mutable settings the client may change. */\nexport function saveSettings(patch: {\n signature?: string\n ai?: Partial<AiSettings>\n read?: Partial<ReadSettings>\n render?: Partial<RenderSettings>\n}): void {\n writeConfigFile(patch as Record<string, unknown>)\n}\n\n/** Persist the full account list and the active account. */\nexport function saveAccounts(accounts: Account[], activeAccountId: string | null): void {\n writeConfigFile({ accounts, activeAccountId })\n}\n\n/** Build a new account record from client input. */\nexport function buildAccount(input: NewAccountInput, existing: Account[]): Account {\n const email = input.email.trim()\n if (!EMAIL.test(email)) throw new Error('\u90AE\u7BB1\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E')\n const id = deriveAccountId(\n email,\n existing.map((account) => account.id),\n )\n return normalizeAccount(\n {\n id,\n email,\n label: input.label,\n username: input.username,\n imap: input.imap as ImapSettings | undefined,\n smtp: input.smtp as SmtpSettings | undefined,\n auth: { type: 'password' },\n caldav: null,\n enabled: true,\n createdAt: new Date().toISOString(),\n },\n 0,\n existing.map((account) => account.id),\n )\n}\n\n/** Per-account runtime paths and connection settings. */\nexport function accountRuntime(account: Account): AccountRuntime {\n const root = accountDataDir(account)\n return {\n account,\n root,\n mailDb: accountMailDb(account),\n attachments: accountAttachmentsDir(account),\n keychainService: account.keychainService || `dsh-mailharbor:${account.id}`,\n imap: account.imap,\n smtp: account.smtp,\n auth: account.auth,\n }\n}\n\n/** Find an account by id, or throw a user-facing error. */\nexport function requireAccount(config: MailHarborConfig, id: string | null | undefined): Account {\n const account =\n config.accounts.find((item) => item.id === id) ??\n config.accounts.find((item) => item.id === config.activeAccountId)\n if (account === undefined) throw new Error('\u8BF7\u5148\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n return account\n}\n\n/** The active account, or null when no account exists yet. */\nexport function activeAccount(config: MailHarborConfig): Account | null {\n return config.accounts.find((account) => account.id === config.activeAccountId) ?? null\n}\n\n/** Directory used for one account's attachments. */\nexport function attachmentsRoot(account: Account): string {\n return join(accountDataDir(account), 'attachments')\n}\n", "import { homedir } from 'node:os'\nimport { isAbsolute, join } from 'node:path'\n\n/**\n * One data root per OS user. It lives beside the DSH home so the plugin behaves\n * the same on macOS, Windows and Linux. `DSH_MAILHARBOR_DATA_DIR` moves it.\n */\nexport const DATA_ROOT = process.env.DSH_MAILHARBOR_DATA_DIR ?? join(homedir(), '.dsh', 'mailharbor')\n\n/** Plugin state (config, drafts, read state, logs). */\nexport const CONFIG_PATH = process.env.DSH_MAILHARBOR_CONFIG ?? join(DATA_ROOT, 'config.json')\n\nexport const ACCOUNTS_DIR = join(DATA_ROOT, 'accounts')\nexport const STATE_DIR = join(DATA_ROOT, 'state')\nexport const REPORTS_DIR = join(DATA_ROOT, 'reports')\nexport const LOGS_DIR = join(DATA_ROOT, 'logs')\n\n/** Resolve `~`, reject relative paths, and normalize separators. */\nexport function resolveUserPath(value: string, label: string): string {\n const text = value.trim()\n if (text === '') throw new Error(`${label}\u4E0D\u80FD\u4E3A\u7A7A`)\n if (text.length > 4096) throw new Error(`${label}\u8FC7\u957F`)\n const expanded = text === '~' ? homedir() : text.startsWith('~/') ? join(homedir(), text.slice(2)) : text\n if (!isAbsolute(expanded)) throw new Error(`${label}\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84`)\n return expanded\n}\n\n/** Account directory names stay portable; IDs are validated by the config layer. */\nexport function accountRoot(id: string): string {\n const safe = id.replace(/[^A-Za-z0-9_-]/g, '_') || 'unknown'\n return join(ACCOUNTS_DIR, safe)\n}\n\n/** Absolute directory for an account record's data. */\nexport function accountDataDir(account: { id: string; dataDir?: string | null }): string {\n const configured = typeof account.dataDir === 'string' ? account.dataDir.trim() : ''\n if (configured === '') return accountRoot(account.id)\n return isAbsolute(configured) ? configured : join(DATA_ROOT, configured)\n}\n\nexport function accountMailDb(account: { id: string; dataDir?: string | null }): string {\n return join(accountDataDir(account), 'mail.db')\n}\n\nexport function accountAttachmentsDir(account: { id: string; dataDir?: string | null }): string {\n return join(accountDataDir(account), 'attachments')\n}\n", "import { execFile } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport { promisify } from 'node:util'\n\nconst run = promisify(execFile)\n\nasync function electronDialog(defaultPath?: string): Promise<string | null | undefined> {\n try {\n const require = createRequire(import.meta.url)\n const electron = require('electron') as {\n dialog?: {\n showOpenDialog(options: Record<string, unknown>): Promise<{ canceled: boolean; filePaths: string[] }>\n }\n }\n if (electron.dialog === undefined) return undefined\n const result = await electron.dialog.showOpenDialog({\n title: '\u9009\u62E9\u9879\u76EE\u76EE\u5F55',\n ...(defaultPath === undefined ? {} : { defaultPath }),\n properties: ['openDirectory', 'createDirectory'],\n })\n return result.canceled ? null : (result.filePaths[0] ?? null)\n } catch {\n return undefined\n }\n}\n\n/**\n * Native directory picker for the desktop app.\n *\n * Tries Electron's dialog first (the Host runs inside the desktop main\n * process), then the platform's own dialog. Returns null when the user\n * cancels and throws when no picker is available.\n */\nexport async function pickDirectory(defaultPath?: string): Promise<string | null> {\n const electron = await electronDialog(defaultPath)\n if (electron !== undefined) return electron\n\n if (process.platform === 'darwin') {\n try {\n const { stdout } = await run(\n 'osascript',\n ['-e', 'POSIX path of (choose folder with prompt \"\u9009\u62E9\u9879\u76EE\u76EE\u5F55\")'],\n { timeout: 180_000 },\n )\n const path = stdout.trim()\n return path === '' ? null : path.replace(/\\/$/, '')\n } catch {\n // Cancel exits non-zero; treat it as \"no selection\".\n return null\n }\n }\n\n if (process.platform === 'win32') {\n const script = [\n 'Add-Type -AssemblyName System.Windows.Forms',\n '$d = New-Object System.Windows.Forms.FolderBrowserDialog',\n '$d.Description = \"\u9009\u62E9\u9879\u76EE\u76EE\u5F55\"',\n 'if ($d.ShowDialog() -eq \"OK\") { Write-Output $d.SelectedPath }',\n ].join('; ')\n try {\n const { stdout } = await run('powershell', ['-NoProfile', '-Command', script], { timeout: 180_000 })\n return stdout.trim() === '' ? null : stdout.trim()\n } catch {\n return null\n }\n }\n\n try {\n const { stdout } = await run('zenity', ['--file-selection', '--directory', '--title=\u9009\u62E9\u9879\u76EE\u76EE\u5F55'], {\n timeout: 180_000,\n })\n return stdout.trim() === '' ? null : stdout.trim()\n } catch {\n throw new Error('\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u6253\u5F00\u76EE\u5F55\u9009\u62E9\u5668\uFF0C\u8BF7\u624B\u52A8\u8F93\u5165\u8DEF\u5F84')\n }\n}\n", "import { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { ImapFlow } from 'imapflow'\nimport { simpleParser } from 'mailparser'\nimport type { BackupStatus } from '../shared/types.js'\nimport { addressOf, formatAddressList, parseAddressList } from './address.js'\nimport type { AccountRuntime } from './config.js'\nimport { DatabaseSync } from 'node:sqlite'\nimport { ensureMailSchema } from './mail-db.js'\nimport type { ImapCredentials } from './imap.js'\n\nexport interface BackupEvent {\n mailbox?: string\n added?: number\n total?: number\n message: string\n}\n\nexport interface BackupResult {\n ok: true\n newCount: number\n failCount: number\n total: number\n mailboxes: { name: string; added: number }[]\n}\n\nexport interface BackupOptions {\n signal?: AbortSignal\n onEvent?: (event: BackupEvent) => void\n batchSize?: number\n}\n\nconst BATCH = 25\n\nfunction sha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex')\n}\n\nfunction normalizeId(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/^<|>$/g, '')\n .trim()\n}\n\n/** Local wall-clock stamp, matching what the previous backup tool wrote. */\nfunction stamp(value: unknown): string {\n const date =\n value instanceof Date\n ? value\n : value === undefined || value === null\n ? undefined\n : new Date(String(value))\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return ''\n const pad = (number: number): string => String(number).padStart(2, '0')\n return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`\n}\n\nfunction abortIfNeeded(signal: AbortSignal | undefined): void {\n if (signal?.aborted === true) {\n const error = new Error('\u5907\u4EFD\u5DF2\u53D6\u6D88')\n ;(error as { code?: string }).code = 'backup-cancelled'\n throw error\n }\n}\n\n/**\n * Incremental IMAP \u2192 SQLite backup for one account.\n *\n * It writes the same schema the previous tool created, so an existing database\n * keeps working. Messages are fetched by UID range (`lastUid+1:*`) instead of\n * enumerating every UID on each run, and each batch commits together with its\n * attachment files and search rows, so an interrupted run resumes cleanly.\n */\nexport class BackupEngine {\n private readonly runtime: AccountRuntime\n private readonly credentials: ImapCredentials\n readonly batchSize: number\n\n constructor(runtime: AccountRuntime, credentials: ImapCredentials, options: { batchSize?: number } = {}) {\n this.runtime = runtime\n this.credentials = credentials\n this.batchSize = options.batchSize ?? BATCH\n }\n\n private attachmentsDir(): string {\n return this.runtime.attachments\n }\n\n private openDb(): DatabaseSync {\n ensureMailSchema(this.runtime.mailDb)\n const db = new DatabaseSync(this.runtime.mailDb)\n db.exec('PRAGMA journal_mode=WAL')\n db.exec('PRAGMA busy_timeout=10000')\n db.exec('PRAGMA synchronous=NORMAL')\n return db\n }\n\n private writeAttachment(data: Buffer): { hash: string; size: number } {\n const hash = sha256(data)\n const dir = join(this.attachmentsDir(), hash.slice(0, 2), hash.slice(2, 4))\n const file = join(dir, hash)\n mkdirSync(dir, { recursive: true })\n if (!existsSync(file)) writeFileSync(file, data, { mode: 0o600 })\n return { hash, size: data.length }\n }\n\n private insertEmail(\n db: DatabaseSync,\n mailbox: string,\n message: RawBackupMessage,\n parsed: Awaited<ReturnType<typeof simpleParser>>,\n logId: number,\n ): number {\n const source = message.source ?? Buffer.alloc(0)\n const hash = sha256(source)\n const from = parsed.from?.value?.[0] ?? { name: '', address: '' }\n const messageId = normalizeId(parsed.messageId ?? message.envelope?.messageId)\n const toText =\n parsedAddressText(parsed.to) || formatAddressList(parseAddressList(addressTextOf(message.envelope?.to)))\n const ccText =\n parsedAddressText(parsed.cc) || formatAddressList(parseAddressList(addressTextOf(message.envelope?.cc)))\n const subject = String(parsed.subject ?? message.envelope?.subject ?? '(\u65E0\u4E3B\u9898)')\n const text = typeof parsed.text === 'string' ? parsed.text : ''\n const html = typeof parsed.html === 'string' ? parsed.html : ''\n const inserted = db\n .prepare(\n `INSERT OR IGNORE INTO emails\n (mailbox, uid, subject, from_name, from_addr, to_addr, cc, date, message_id,\n body_text, body_html, raw_eml, hash, backup_log_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n mailbox,\n Number(message.uid),\n subject,\n String(from.name ?? ''),\n String(from.address ?? ''),\n toText,\n ccText,\n stamp(parsed.date ?? message.internalDate ?? message.envelope?.date),\n messageId === '' ? null : messageId,\n text,\n html,\n source,\n hash,\n logId,\n )\n if (inserted.changes === 0) return 0\n const emailId = Number(inserted.lastInsertRowid)\n const attachments = parsed.attachments ?? []\n const insertAttachment = db.prepare(\n 'INSERT INTO attachments (email_id, filename, content_type, size, hash) VALUES (?, ?, ?, ?, ?)',\n )\n for (const attachment of attachments) {\n const data = attachment.content\n if (data === undefined || data === null) continue\n const stored = this.writeAttachment(Buffer.isBuffer(data) ? data : Buffer.from(data))\n insertAttachment.run(\n emailId,\n String(attachment.filename ?? 'attachment'),\n String(attachment.contentType ?? 'application/octet-stream'),\n stored.size,\n stored.hash,\n )\n }\n try {\n db.prepare(\n `INSERT INTO emails_fts (rowid, subject, from_name, from_addr, to_addr, cc, body_text, att_filename)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n emailId,\n subject,\n String(from.name ?? ''),\n String(from.address ?? ''),\n toText,\n ccText,\n text,\n attachments\n .map((attachment) => attachment.filename)\n .filter(Boolean)\n .join(' '),\n )\n } catch {\n // Search index is best effort; the message itself is stored.\n }\n return 1\n }\n\n async run(options: BackupOptions = {}): Promise<BackupResult> {\n const emit = (event: BackupEvent): void => options.onEvent?.(event)\n const signal = options.signal\n const db = this.openDb()\n const startedAt = new Date().toISOString()\n const logId = Number(\n db.prepare(`INSERT INTO backup_logs (started_at, status) VALUES (?, 'running')`).run(startedAt)\n .lastInsertRowid,\n )\n const client = new ImapFlow({\n host: this.runtime.imap.host,\n port: Number(this.runtime.imap.port),\n secure: this.runtime.imap.secure !== false,\n auth:\n this.credentials.accessToken !== undefined\n ? { user: this.credentials.user, accessToken: this.credentials.accessToken }\n : { user: this.credentials.user, pass: this.credentials.pass ?? '' },\n logger: false,\n tls: { rejectUnauthorized: true },\n connectionTimeout: 20_000,\n greetingTimeout: 20_000,\n socketTimeout: 120_000,\n })\n client.on('error', () => {})\n let newCount = 0\n let failCount = 0\n const mailboxes: { name: string; added: number }[] = []\n emit({ message: `\u5F00\u59CB\u5907\u4EFD ${this.runtime.account.email}` })\n try {\n await client.connect()\n const boxes = await client.list()\n for (const box of boxes) {\n abortIfNeeded(signal)\n if (box.flags?.has?.('\\\\Noselect') === true) continue\n const path = box.path\n emit({ mailbox: path, message: `\u68C0\u67E5 ${path}` })\n const lock = await client.getMailboxLock(path)\n let boxAdded = 0\n try {\n const uidValidity = String(\n (client.mailbox as { uidValidity?: bigint | number } | undefined)?.uidValidity ?? '',\n )\n let state = db\n .prepare('SELECT uid_validity, last_uid FROM backup_state WHERE mailbox = ?')\n .get(path) as { uid_validity?: string; last_uid?: number } | undefined\n if (state === undefined) {\n const dbMax = Number(\n db.prepare('SELECT MAX(uid) AS maxUid FROM emails WHERE mailbox = ?').get(path)?.maxUid ?? 0,\n )\n this.saveState(db, path, uidValidity, dbMax)\n state = { uid_validity: uidValidity, last_uid: dbMax }\n } else if (uidValidity !== '' && String(state.uid_validity ?? '') !== uidValidity) {\n this.saveState(db, path, uidValidity, 0)\n state = { uid_validity: uidValidity, last_uid: 0 }\n emit({ mailbox: path, message: `${path} \u7684 UIDVALIDITY \u53D8\u5316\uFF0C\u91CD\u65B0\u540C\u6B65` })\n }\n const lastUid = Number(state.last_uid ?? 0)\n // Only ask the server for UIDs newer than the last committed one.\n const found = await client.search({ uid: `${String(lastUid + 1)}:*` }, { uid: true })\n const uids = (Array.isArray(found) ? found : [])\n .filter((uid) => Number(uid) > lastUid)\n .sort((left, right) => left - right)\n let committedUid = lastUid\n for (let offset = 0; offset < uids.length; offset += this.batchSize) {\n abortIfNeeded(signal)\n const batch = uids.slice(offset, offset + this.batchSize)\n db.exec('BEGIN')\n try {\n let batchLastUid = committedUid\n for await (const message of client.fetch(\n batch.join(','),\n { uid: true, source: true, flags: true, internalDate: true },\n { uid: true },\n )) {\n abortIfNeeded(signal)\n try {\n const parsed = await simpleParser(message.source ?? Buffer.alloc(0))\n const added = this.insertEmail(\n db,\n path,\n message as unknown as RawBackupMessage,\n parsed,\n logId,\n )\n boxAdded += added\n newCount += added\n batchLastUid = Math.max(batchLastUid, Number(message.uid))\n } catch (error) {\n failCount += 1\n emit({\n mailbox: path,\n message: `\u90AE\u4EF6 ${String(message.uid)} \u89E3\u6790\u5931\u8D25: ${messageOf(error)}`,\n })\n }\n }\n this.saveState(db, path, uidValidity, batchLastUid)\n db.exec('COMMIT')\n committedUid = batchLastUid\n emit({ mailbox: path, added: boxAdded, message: `${path}: \u5DF2\u65B0\u589E ${String(boxAdded)} \u5C01` })\n } catch (error) {\n db.exec('ROLLBACK')\n throw error\n }\n }\n mailboxes.push({ name: path, added: boxAdded })\n emit({ mailbox: path, added: boxAdded, message: `${path}: \u65B0\u589E ${String(boxAdded)} \u5C01` })\n } finally {\n lock.release()\n }\n }\n const total = Number(db.prepare('SELECT COUNT(*) AS n FROM emails').get()?.n ?? 0)\n db.prepare(\n `UPDATE backup_logs SET completed_at = ?, mailboxes = ?, new_count = ?, fail_count = ?,\n total_emails = ?, status = 'completed' WHERE id = ?`,\n ).run(new Date().toISOString(), JSON.stringify(mailboxes), newCount, failCount, total, logId)\n emit({\n total,\n added: newCount,\n message: `\u5907\u4EFD\u5B8C\u6210\uFF1A\u65B0\u589E ${String(newCount)} \u5C01\uFF0C\u5171 ${String(total)} \u5C01`,\n })\n return { ok: true, newCount, failCount, total, mailboxes }\n } catch (error) {\n const total = Number(db.prepare('SELECT COUNT(*) AS n FROM emails').get()?.n ?? 0)\n db.prepare(\n `UPDATE backup_logs SET completed_at = ?, mailboxes = ?, new_count = ?, fail_count = ?,\n total_emails = ?, status = 'failed', error_info = ? WHERE id = ?`,\n ).run(\n new Date().toISOString(),\n JSON.stringify(mailboxes),\n newCount,\n failCount,\n total,\n messageOf(error).slice(0, 1000),\n logId,\n )\n throw error\n } finally {\n try {\n await client.logout()\n } catch {\n // Already closed.\n }\n db.close()\n }\n }\n\n private saveState(db: DatabaseSync, mailbox: string, uidValidity: string, lastUid: number): void {\n db.prepare(\n `INSERT INTO backup_state (mailbox, uid_validity, last_uid, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(mailbox) DO UPDATE SET uid_validity = excluded.uid_validity,\n last_uid = excluded.last_uid, updated_at = excluded.updated_at`,\n ).run(mailbox, uidValidity, Number(lastUid), new Date().toISOString())\n }\n\n /** Latest backup summary for the settings page. */\n static status(dbFile: string): BackupStatus {\n const empty: BackupStatus = {\n running: false,\n accountId: null,\n startedAt: null,\n finishedAt: null,\n lastSuccessAt: null,\n total: 0,\n added: 0,\n message: '',\n output: [],\n exitCode: null,\n }\n if (!existsSync(dbFile)) return empty\n try {\n const db = new DatabaseSync(dbFile, { readOnly: true })\n try {\n const latest = db.prepare('SELECT * FROM backup_logs ORDER BY id DESC LIMIT 1').get()\n const success = db\n .prepare(\"SELECT completed_at FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1\")\n .get()\n const total = Number(db.prepare('SELECT COUNT(*) AS n FROM emails').get()?.n ?? 0)\n return {\n ...empty,\n running: latest?.status === 'running',\n startedAt: latest?.started_at === undefined ? null : String(latest.started_at),\n finishedAt: latest?.completed_at === undefined ? null : String(latest.completed_at),\n lastSuccessAt: success?.completed_at === undefined ? null : String(success.completed_at),\n total,\n added: Number(latest?.new_count ?? 0),\n message: latest?.status === 'failed' ? String(latest?.error_info ?? '\u5907\u4EFD\u5931\u8D25') : '',\n exitCode: latest?.status === 'failed' ? 1 : 0,\n }\n } finally {\n db.close()\n }\n } catch {\n return empty\n }\n }\n}\n\ninterface RawBackupMessage {\n uid: number\n source?: Buffer\n envelope?: {\n messageId?: string\n subject?: string\n from?: { name?: string; address?: string }[]\n to?: { name?: string; address?: string }[]\n cc?: { name?: string; address?: string }[]\n date?: string | Date\n }\n internalDate?: string | Date\n}\n\n/** Format `parsed.to`/`parsed.cc`, which is an address object or a list. */\nfunction parsedAddressText(value: unknown): string {\n if (value === undefined || value === null) return ''\n const items = Array.isArray(value) ? value : [value]\n return formatAddressList(\n items\n .map((item) => ({\n name: String((item as { name?: string }).name ?? ''),\n address: String((item as { address?: string }).address ?? ''),\n }))\n .filter((address) => address.address !== ''),\n )\n}\n\nfunction addressTextOf(list: unknown[] | undefined): string {\n return (list ?? [])\n .map((item) => {\n const entry = item as { name?: string; address?: string }\n return entry.name ? `${entry.name} <${String(entry.address ?? '')}>` : String(entry.address ?? '')\n })\n .filter((text) => text !== '')\n .join(', ')\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nexport { addressOf }\n", "import { existsSync, readFileSync } from 'node:fs'\nimport { DatabaseSync } from 'node:sqlite'\nimport type { AttachmentMeta, MessageDetail, MessageSummary, Page } from '../shared/types.js'\nimport { parseAddressList } from './address.js'\n\n/** The layout the backup engine writes; read here by the panel and tools. */\nconst SCHEMA = `\n CREATE TABLE IF NOT EXISTS emails (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n mailbox TEXT NOT NULL,\n uid INTEGER,\n subject TEXT,\n from_name TEXT,\n from_addr TEXT,\n to_addr TEXT,\n cc TEXT,\n date TEXT,\n message_id TEXT UNIQUE,\n body_text TEXT,\n body_html TEXT,\n raw_eml BLOB,\n hash TEXT,\n backup_log_id INTEGER,\n created_at TEXT DEFAULT (datetime('now'))\n );\n CREATE TABLE IF NOT EXISTS attachments (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n email_id INTEGER NOT NULL,\n filename TEXT NOT NULL,\n content_type TEXT,\n size INTEGER,\n data BLOB,\n hash TEXT\n );\n CREATE TABLE IF NOT EXISTS backup_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n mailboxes TEXT,\n new_count INTEGER DEFAULT 0,\n fail_count INTEGER DEFAULT 0,\n total_emails INTEGER DEFAULT 0,\n cumulative_hash TEXT,\n status TEXT DEFAULT 'running',\n error_info TEXT\n );\n CREATE TABLE IF NOT EXISTS backup_state (\n mailbox TEXT PRIMARY KEY,\n uid_validity TEXT,\n last_uid INTEGER,\n updated_at TEXT\n );\n`\n\nconst INDEXES = `\n CREATE INDEX IF NOT EXISTS idx_emails_mailbox_date ON emails(mailbox, date);\n CREATE INDEX IF NOT EXISTS idx_emails_date ON emails(date);\n CREATE INDEX IF NOT EXISTS idx_attachments_email ON attachments(email_id);\n`\n\nconst FTS = `CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(\n subject, from_name, from_addr, to_addr, cc, body_text, att_filename,\n content='', tokenize='trigram'\n)`\n\n/** Create the mail database layout, including the search index when available. */\nexport function ensureMailSchema(file: string): void {\n const db = new DatabaseSync(file)\n try {\n db.exec('PRAGMA journal_mode=WAL')\n db.exec(SCHEMA)\n db.exec(INDEXES)\n try {\n db.exec(FTS)\n } catch {\n // A build without FTS5 still backs up mail; search falls back to LIKE.\n }\n } finally {\n db.close()\n }\n}\n\nexport interface ListQuery {\n mailbox?: string\n query?: string\n offset?: number\n limit?: number\n since?: string\n order?: 'newest' | 'oldest'\n}\n\nexport interface MailDatabaseOptions {\n /** Hard cap on stored body text returned to the reader. */\n maxBodyChars?: number\n /** Root that holds content-addressed attachment files. */\n attachmentsRoot?: string\n}\n\n/**\n * Read-only view over one account's mail backup.\n *\n * The backup engine owns the file; this class never writes to it and opens it\n * with SQLITE_OPEN_READONLY. A single connection is reused because opening a\n * large database per request is wasteful.\n */\nexport class MailDatabase {\n private readonly file: string\n private readonly options: MailDatabaseOptions\n private db: DatabaseSync | undefined\n private ftsChecked = false\n private ftsAvailable = false\n\n constructor(file: string, options: MailDatabaseOptions = {}) {\n this.file = file\n this.options = options\n }\n\n get path(): string {\n return this.file\n }\n\n isOpen(): boolean {\n return this.db !== undefined\n }\n\n open(): DatabaseSync {\n if (this.db !== undefined) return this.db\n if (!existsSync(this.file)) throw new Error(`\u90AE\u4EF6\u6570\u636E\u5E93\u4E0D\u5B58\u5728: ${this.file}`)\n this.db = new DatabaseSync(this.file, { readOnly: true })\n this.db.exec('PRAGMA busy_timeout = 3000')\n return this.db\n }\n\n close(): void {\n if (this.db === undefined) return\n try {\n this.db.close()\n } catch {\n // Closing an already-closed handle is harmless.\n }\n this.db = undefined\n }\n\n status(): { ok: boolean; database: string; total: number; error?: string } {\n try {\n const row = this.open().prepare('SELECT COUNT(*) AS count FROM emails').get()\n return { ok: true, database: this.file, total: Number(row?.count ?? 0) }\n } catch (error) {\n return { ok: false, database: this.file, total: 0, error: messageOf(error) }\n }\n }\n\n /** Per-mailbox counts, largest first. */\n mailboxes(): { name: string; count: number }[] {\n const rows = this.open()\n .prepare('SELECT mailbox, COUNT(*) AS count FROM emails GROUP BY mailbox ORDER BY count DESC')\n .all()\n return rows.map((row) => ({ name: String(row.mailbox), count: Number(row.count ?? 0) }))\n }\n\n private supportsFts(): boolean {\n if (this.ftsChecked) return this.ftsAvailable\n this.ftsChecked = true\n try {\n const row = this.open()\n .prepare(\"SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'emails_fts'\")\n .get()\n this.ftsAvailable = row !== undefined\n } catch {\n this.ftsAvailable = false\n }\n return this.ftsAvailable\n }\n\n list(query: ListQuery = {}): Page<MessageSummary> {\n const db = this.open()\n const mailbox = String(query.mailbox ?? 'all')\n const offset = Math.max(0, Math.trunc(query.offset ?? 0))\n const limit = Math.min(500, Math.max(1, Math.trunc(query.limit ?? 40)))\n const where: string[] = []\n const params: (string | number)[] = []\n if (mailbox !== '' && mailbox !== 'all') {\n where.push('e.mailbox = ?')\n params.push(mailbox)\n }\n if (typeof query.since === 'string' && query.since !== '') {\n where.push('e.date >= ?')\n params.push(query.since)\n }\n const text = String(query.query ?? '').trim()\n if (text !== '') {\n const tokens = text.split(/\\s+/).filter(Boolean)\n const short = tokens.some((token) => token.length < 3)\n if (!short && this.supportsFts()) {\n const like = `%${text}%`\n where.push(\n '(e.id IN (SELECT rowid FROM emails_fts WHERE emails_fts MATCH ?) OR e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ?)',\n )\n params.push(tokens.map(quoteMatch).join(' AND '), like, like, like, like)\n } else {\n for (const token of tokens) {\n const like = `%${token}%`\n where.push(\n '(e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ? OR e.body_text LIKE ?)',\n )\n params.push(like, like, like, like, like)\n }\n }\n }\n const clause = where.length === 0 ? '' : `WHERE ${where.join(' AND ')}`\n const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,\n e.message_id, e.date, length(e.body_text) AS text_length,\n (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,\n substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`\n const total = Number(\n db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0,\n )\n const direction = query.order === 'oldest' ? 'ASC' : 'DESC'\n const rows = db\n .prepare(\n `${columns} FROM emails e ${clause} ORDER BY e.date ${direction}, e.id ${direction} LIMIT ? OFFSET ?`,\n )\n .all(...params, limit, offset)\n return { items: rows.map((row) => rowToSummary(row as Record<string, unknown>)), total, offset, limit }\n }\n\n /** Structured search used by AI search (keywords + sender + dates). */\n searchAdvanced(options: {\n mailbox?: string\n keywords?: string[]\n from?: string\n subject?: string\n since?: string\n before?: string\n limit?: number\n }): Page<MessageSummary> {\n const db = this.open()\n const where: string[] = []\n const params: (string | number)[] = []\n const mailbox = String(options.mailbox ?? 'all')\n if (mailbox !== '' && mailbox !== 'all') {\n where.push('e.mailbox = ?')\n params.push(mailbox)\n }\n const keywords = (options.keywords ?? []).filter((keyword) => keyword !== '')\n if (keywords.length > 0) {\n const clauses: string[] = []\n for (const keyword of keywords) {\n clauses.push(\n '(e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ? OR e.body_text LIKE ?)',\n )\n const like = `%${keyword}%`\n params.push(like, like, like, like, like)\n }\n where.push(`(${clauses.join(' OR ')})`)\n }\n if ((options.from ?? '') !== '') {\n const like = `%${String(options.from)}%`\n where.push('(e.from_name LIKE ? OR e.from_addr LIKE ?)')\n params.push(like, like)\n }\n if ((options.subject ?? '') !== '') {\n where.push('e.subject LIKE ?')\n params.push(`%${String(options.subject)}%`)\n }\n if ((options.since ?? '') !== '') {\n where.push('substr(e.date, 1, 10) >= ?')\n params.push(String(options.since))\n }\n if ((options.before ?? '') !== '') {\n where.push('substr(e.date, 1, 10) <= ?')\n params.push(String(options.before))\n }\n const limit = Math.min(100, Math.max(1, options.limit ?? 60))\n if (where.length === 0) return { items: [], total: 0, offset: 0, limit }\n const clause = `WHERE ${where.join(' AND ')}`\n const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,\n e.message_id, e.date, length(e.body_text) AS text_length,\n (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,\n substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`\n const total = Number(\n db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0,\n )\n const rows = db\n .prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`)\n .all(...params, limit)\n return { items: rows.map((row) => rowToSummary(row as Record<string, unknown>)), total, offset: 0, limit }\n }\n\n get(localId: number): MessageDetail | undefined {\n const db = this.open()\n const row = db\n .prepare(\n `SELECT id, mailbox, uid, subject, from_name, from_addr, to_addr, cc, date, message_id,\n body_text, body_html\n FROM emails WHERE id = ?`,\n )\n .get(localId)\n if (row === undefined) return undefined\n const attachments = db\n .prepare('SELECT id, filename, content_type, size FROM attachments WHERE email_id = ? ORDER BY id')\n .all(localId)\n .map((item: Record<string, unknown>): AttachmentMeta => ({\n id: String(item.id),\n filename: String(item.filename ?? 'attachment'),\n contentType: String(item.content_type ?? 'application/octet-stream'),\n size: Number(item.size ?? 0),\n }))\n const max = this.options.maxBodyChars ?? 400_000\n const bodyText = String(row.body_text ?? '')\n const bodyHtml = String(row.body_html ?? '')\n return {\n ...rowToSummary(row),\n cc: parseAddressList(String(row.cc ?? '')),\n replyTo: [],\n text: truncate(bodyText, max),\n html: bodyHtml === '' ? null : truncate(bodyHtml, max * 2),\n attachments,\n references: [],\n }\n }\n\n /** Read one attachment's bytes; the payload may live in a file (content-addressed). */\n attachment(\n emailId: number,\n attachmentId: number,\n ): { filename: string; contentType: string; data: Uint8Array } | undefined {\n const db = this.open()\n const row = db\n .prepare('SELECT filename, content_type, data, hash FROM attachments WHERE id = ? AND email_id = ?')\n .get(attachmentId, emailId)\n if (row === undefined) return undefined\n let data: Uint8Array | null = row.data instanceof Uint8Array ? row.data : null\n if (data === null && typeof row.hash === 'string' && row.hash !== '') {\n const root = this.options.attachmentsRoot ?? ''\n if (root !== '') {\n const file = `${root}/${row.hash.slice(0, 2)}/${row.hash.slice(2, 4)}/${row.hash}`\n if (existsSync(file)) data = readFileSyncSafe(file)\n }\n }\n if (data === null) return undefined\n return {\n filename: String(row.filename ?? 'attachment'),\n contentType: String(row.content_type ?? 'application/octet-stream'),\n data,\n }\n }\n\n /** Locate a local row by Message-ID, for links from tasks and projects. */\n findByMessageId(messageId: string): { id: number; mailbox: string } | undefined {\n const id = messageId.trim().replace(/^<|>$/g, '')\n if (id === '') return undefined\n const row = this.open().prepare('SELECT id, mailbox FROM emails WHERE message_id = ? LIMIT 1').get(id)\n if (row === undefined) return undefined\n return { id: Number(row.id), mailbox: String(row.mailbox ?? '') }\n }\n\n /** Whether the backup already holds a message with this Message-ID. */\n hasMessageId(messageId: string): boolean {\n const id = messageId.trim().replace(/^<|>$/g, '')\n if (id === '') return false\n try {\n return (\n this.open().prepare('SELECT 1 AS found FROM emails WHERE message_id = ? LIMIT 1').get(id) !==\n undefined\n )\n } catch {\n return false\n }\n }\n\n /** Recent sent messages, for learning the user's writing style. */\n sentMessages(limit = 100): { subject: string; text: string }[] {\n const rows = this.open()\n .prepare(\n `SELECT subject, body_text FROM emails\n WHERE lower(mailbox) LIKE '%sent%' OR mailbox LIKE '%\u5DF2\u53D1\u9001%'\n ORDER BY date DESC, id DESC LIMIT ?`,\n )\n .all(limit)\n return rows.map((row) => ({\n subject: String(row.subject ?? ''),\n text: String(row.body_text ?? ''),\n }))\n }\n\n /** Latest backup runs, for the settings page. */\n backupSummary(): {\n ok: boolean\n lastCompleted: Record<string, unknown> | null\n latest: Record<string, unknown> | null\n total: number\n error?: string\n } {\n try {\n const db = this.open()\n const lastCompleted =\n db\n .prepare(\n `SELECT id, started_at, completed_at, new_count, total_emails, status\n FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1`,\n )\n .get() ?? null\n const latest =\n db\n .prepare(\n 'SELECT id, started_at, completed_at, new_count, total_emails, status FROM backup_logs ORDER BY id DESC LIMIT 1',\n )\n .get() ?? null\n return {\n ok: true,\n lastCompleted: toPlain(lastCompleted),\n latest: toPlain(latest),\n total: Number(db.prepare('SELECT COUNT(*) AS count FROM emails').get()?.count ?? 0),\n }\n } catch (error) {\n return { ok: false, lastCompleted: null, latest: null, total: 0, error: messageOf(error) }\n }\n }\n}\n\nfunction toPlain(row: unknown): Record<string, unknown> | null {\n if (row === undefined || row === null || typeof row !== 'object') return null\n return { ...(row as Record<string, unknown>) }\n}\n\nfunction readFileSyncSafe(file: string): Uint8Array | null {\n try {\n return readFileSync(file)\n } catch {\n return null\n }\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nfunction truncate(value: string, max: number): string {\n return value.length <= max ? value : `${value.slice(0, max)}\\n\\n\u2026\uFF08\u5DF2\u622A\u65AD\uFF09`\n}\n\nfunction quoteMatch(text: string): string {\n return `\"${text.replaceAll('\"', '\"\"')}\"`\n}\n\nfunction rowToSummary(row: Record<string, unknown>): MessageSummary {\n const fromName = String(row.from_name ?? '')\n const fromAddr = String(row.from_addr ?? '')\n return {\n id: String(row.id),\n accountId: '',\n uid: row.uid === null || row.uid === undefined ? undefined : Number(row.uid),\n messageId: String(row.message_id ?? ''),\n subject: String(row.subject ?? '(\u65E0\u4E3B\u9898)'),\n from: { name: fromName, address: fromAddr },\n to: parseAddressList(String(row.to_addr ?? '')),\n date: String(row.date ?? ''),\n unread: false,\n flagged: false,\n hasAttachments: Number(row.attachment_count ?? 0) > 0,\n snippet: String(row.snippet ?? '')\n .replace(/\\s+/g, ' ')\n .trim(),\n mailbox: String(row.mailbox ?? ''),\n }\n}\n", "import type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport class HttpError extends Error {\n readonly status: number\n readonly code: string | undefined\n\n constructor(status: number, message: string, code?: string) {\n super(message)\n this.status = status\n this.code = code\n }\n}\n\nexport function sendJson(res: ServerResponse, status: number, payload: unknown): void {\n const body = Buffer.from(JSON.stringify(payload), 'utf8')\n res.statusCode = status\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('content-length', String(body.length))\n res.setHeader('cache-control', 'no-store')\n res.setHeader('x-content-type-options', 'nosniff')\n res.end(body)\n}\n\nexport function sendError(res: ServerResponse, error: unknown): void {\n const status = error instanceof HttpError ? error.status : 500\n const message = error instanceof Error ? error.message : String(error)\n const code = error instanceof HttpError ? error.code : undefined\n sendJson(res, status, { error: message, ...(code === undefined ? {} : { code }) })\n}\n\n/** Read a JSON request body with a hard size cap. */\nexport async function readJsonBody(\n req: IncomingMessage,\n limit = 2 * 1024 * 1024,\n): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string)\n size += buffer.length\n if (size > limit) throw new HttpError(413, '\u8BF7\u6C42\u4F53\u8FC7\u5927')\n chunks.push(buffer)\n }\n if (size === 0) return {}\n try {\n const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new HttpError(400, '\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61')\n }\n return parsed as Record<string, unknown>\n } catch (error) {\n if (error instanceof HttpError) throw error\n throw new HttpError(400, '\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON')\n }\n}\n\n/**\n * Reject cross-site writes. The Host only listens on loopback, so a request\n * whose Origin is a web page on another site is a CSRF attempt; the desktop\n * shell uses the `dsh-app:` origin and is accepted explicitly.\n */\nexport function isTrustedRequest(req: IncomingMessage): boolean {\n const origin = req.headers.origin\n if (origin === undefined || origin === '') return true\n if (origin.startsWith('dsh-app://')) return true\n try {\n const parsed = new URL(origin)\n const host = req.headers.host ?? ''\n return parsed.host === host\n } catch {\n return false\n }\n}\n\nexport function assertTrusted(req: IncomingMessage): void {\n if (!isTrustedRequest(req)) throw new HttpError(403, '\u6765\u6E90\u4E0D\u88AB\u5141\u8BB8', 'forbidden')\n}\n\nexport interface RouteMatch {\n handler: RouteHandler\n params: Record<string, string>\n path: string\n}\n\nexport interface RequestContext {\n req: IncomingMessage\n res: ServerResponse\n url: URL\n params: Record<string, string>\n body: () => Promise<Record<string, unknown>>\n}\n\nexport type RouteHandler = (context: RequestContext) => Promise<unknown> | unknown\n\ninterface Route {\n method: string\n segments: string[]\n handler: RouteHandler\n}\n\n/** Tiny method + path router with `:param` segments. */\nexport class Router {\n private readonly routes: Route[] = []\n\n add(method: string, path: string, handler: RouteHandler): void {\n this.routes.push({\n method: method.toUpperCase(),\n segments: path.split('/').filter((segment) => segment !== ''),\n handler,\n })\n }\n\n get(path: string, handler: RouteHandler): void {\n this.add('GET', path, handler)\n }\n\n post(path: string, handler: RouteHandler): void {\n this.add('POST', path, handler)\n }\n\n patch(path: string, handler: RouteHandler): void {\n this.add('PATCH', path, handler)\n }\n\n put(path: string, handler: RouteHandler): void {\n this.add('PUT', path, handler)\n }\n\n delete(path: string, handler: RouteHandler): void {\n this.add('DELETE', path, handler)\n }\n\n match(method: string, pathname: string): RouteMatch | null {\n const parts = pathname.split('/').filter((segment) => segment !== '')\n for (const route of this.routes) {\n if (route.method !== method.toUpperCase()) continue\n if (route.segments.length !== parts.length) continue\n const params: Record<string, string> = {}\n let matched = true\n for (const [index, segment] of route.segments.entries()) {\n const actual = parts[index] ?? ''\n if (segment.startsWith(':')) {\n params[segment.slice(1)] = decodeURIComponent(actual)\n } else if (segment !== actual) {\n matched = false\n break\n }\n }\n if (matched) return { handler: route.handler, params, path: pathname }\n }\n return null\n }\n}\n", "import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { Account, MessageDetail } from '../shared/types.js'\nimport type { MailHarborConfig } from './config.js'\nimport type { DshLlm } from './dsh.js'\n\nexport interface StyleProfile {\n /** Free-form Chinese description of the user's writing habits. */\n profile: string\n updatedAt: string\n sampleCount: number\n}\n\nexport interface AiStatus {\n available: boolean\n enabled: boolean\n provider: string\n model: string\n styleUpdatedAt: string\n styleSampleCount: number\n error?: string\n}\n\nexport interface AiDeps {\n config: () => MailHarborConfig\n llm: () => DshLlm | undefined\n /** Directory that holds the account's mail data (style profile lives here). */\n accountRoot: (account: Account) => string\n}\n\nconst TIMEOUT_MS = 90_000\nconst MAX_PROMPT_CHARS = 40_000\n\nconst SUMMARY_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u7406\uFF0C\u670D\u52A1\u4E8E\u4E2D\u6587\u7528\u6237\u3002\u8BF7\u7528\u7B80\u4F53\u4E2D\u6587\u603B\u7ED3\u90AE\u4EF6\uFF0C\u4E25\u683C\u6309\u4E0B\u9762\u56DB\u6BB5\u8F93\u51FA\uFF0C\u4F7F\u7528\u7ED9\u5B9A\u7684\u5C0F\u6807\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9\uFF1A\n\n\u7ED3\u8BBA\n\uFF081-2 \u53E5\u8BDD\u8BF4\u660E\u8FD9\u5C01\u90AE\u4EF6\u662F\u4EC0\u4E48\u4E8B\uFF09\n\n\u9700\u8981\u4F60\u505A\u4EC0\u4E48\n\uFF08\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u9700\u64CD\u4F5C\u300D\uFF1B\u6709\u5C31\u5217 1-3 \u6761\uFF0C\u9879\u76EE\u7B26\u53F7\uFF09\n\n\u5173\u952E\u4FE1\u606F\n\uFF08\u65F6\u95F4\u3001\u91D1\u989D\u3001\u4EBA\u540D\u3001\u5355\u53F7\u3001\u5730\u5740\u7B49\uFF0C\u9879\u76EE\u7B26\u53F7\uFF1B\u6CA1\u6709\u5C31\u5199\u300C\u65E0\u300D\uFF09\n\n\u5EFA\u8BAE\u56DE\u590D\u8981\u70B9\n\uFF081-3 \u6761\u4E2D\u6587\u8981\u70B9\uFF09\n\n\u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\uFF0C\u4E0D\u8981\u7F16\u9020\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B\u3002`\n\nconst REPLY_SYSTEM = (\n language: string,\n style: string,\n): string => `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u539F\u59CB\u90AE\u4EF6\u548C\u7528\u6237\u7684\u8981\u6C42\uFF0C\u5199\u4E00\u5C01\u53EF\u4EE5\u76F4\u63A5\u53D1\u9001\u7684\u56DE\u590D\u6B63\u6587\u3002\n\n\u8981\u6C42\uFF1A\n- \u8BED\u8A00\uFF1A${language === 'en' ? '\u82F1\u6587' : '\u7B80\u4F53\u4E2D\u6587'}\n- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48\n- \u8BED\u6C14\u4E13\u4E1A\u3001\u7B80\u6D01\u3001\u793C\u8C8C\uFF1B\u4E2D\u6587\u7528\u300C\u60A8\u300D\uFF0C\u82F1\u6587\u907F\u514D\u8FC7\u5EA6\u6B63\u5F0F\n- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB\uFF08\u79F0\u547C\u3001\u6B63\u6587\u3001\u7ED3\u5C3E\uFF09\uFF0C\u4E0D\u8981\u5F15\u53F7\u5305\u88F9\n${style === '' ? '' : `\\n\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A\\n${style}`}`\n\nconst TRANSLATE_SYSTEM = (\n target: string,\n): string => `\u4F60\u662F\u90AE\u4EF6\u7FFB\u8BD1\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7FFB\u8BD1\u6210${target === 'en' ? '\u82F1\u6587' : '\u7B80\u4F53\u4E2D\u6587'}\u3002\n\n\u89C4\u5219\uFF1A\n- \u53EA\u8F93\u51FA\u8BD1\u6587\uFF0C\u4E0D\u6DFB\u52A0\u89E3\u91CA\n- \u4FDD\u7559\u6BB5\u843D\u7ED3\u6784\n- \u5982\u679C\u6B63\u6587\u662F HTML\uFF0C\u4FDD\u7559\u539F\u6709\u6807\u7B7E\u3001\u94FE\u63A5\u548C\u56FE\u7247\uFF0C\u53EA\u7FFB\u8BD1\u53EF\u89C1\u6587\u5B57\n- \u4E13\u6709\u540D\u8BCD\u3001\u90AE\u7BB1\u5730\u5740\u3001\u94FE\u63A5\u3001\u5355\u53F7\u4FDD\u6301\u4E0D\u53D8`\n\nconst ASK_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u52A9\u624B\u3002\u57FA\u4E8E\u7528\u6237\u63D0\u4F9B\u7684\u90AE\u4EF6\u548C\u5DF2\u6709\u7684 AI \u8BB0\u5F55\u56DE\u7B54\u95EE\u9898\u3002\n\n\u8981\u6C42\uFF1A\n- \u7528\u7B80\u4F53\u4E2D\u6587\u56DE\u7B54\uFF0C\u7B80\u6D01\u76F4\u63A5\n- \u53EA\u4F9D\u636E\u90AE\u4EF6\u5185\u5BB9\u548C\u5DF2\u6709\u8BB0\u5F55\uFF0C\u4E0D\u786E\u5B9A\u5C31\u8BF4\u660E\u300C\u90AE\u4EF6\u91CC\u6CA1\u6709\u63D0\u5230\u300D\n- \u4E0D\u8981\u590D\u8FF0\u95EE\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u601D\u8003\u8FC7\u7A0B`\n\nconst COMPOSE_SYSTEM = (mode: 'draft' | 'polish' | 'shorten', language: string, style: string): string => {\n const shared = `- \u8BED\u8A00\uFF1A${language === 'en' ? '\u82F1\u6587' : '\u7B80\u4F53\u4E2D\u6587'}\\n- \u4E0D\u8981\u5199\u4E3B\u9898\u884C\uFF0C\u4E0D\u8981\u8F93\u51FA Markdown \u6807\u8BB0\uFF0C\u4E0D\u8981\u89E3\u91CA\u4F60\u5728\u505A\u4EC0\u4E48\\n- \u53EA\u8F93\u51FA\u6B63\u6587\u672C\u8EAB`\n const styleLine = style === '' ? '' : `\\n\u5199\u4F5C\u98CE\u683C\uFF08\u6A21\u4EFF\u4F46\u4E0D\u751F\u786C\uFF09\uFF1A\\n${style}`\n if (mode === 'polish') {\n return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6DA6\u8272\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\uFF0C\u4FDD\u6301\u539F\u610F\u548C\u4E8B\u5B9E\uFF0C\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u81EA\u7136\u3002\\n\\n\u8981\u6C42\uFF1A\\n${shared}${styleLine}`\n }\n if (mode === 'shorten') {\n return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u628A\u7528\u6237\u7ED9\u51FA\u7684\u90AE\u4EF6\u6B63\u6587\u7F29\u77ED\u5230\u7EA6\u4E00\u534A\uFF0C\u4FDD\u7559\u5173\u952E\u4FE1\u606F\u548C\u4E8B\u5B9E\u3002\\n\\n\u8981\u6C42\uFF1A\\n${shared}${styleLine}`\n }\n return `\u4F60\u662F\u90AE\u4EF6\u5199\u4F5C\u52A9\u7406\u3002\u6839\u636E\u7528\u6237\u7ED9\u51FA\u7684\u8981\u70B9\u5199\u4E00\u5C01\u90AE\u4EF6\u6B63\u6587\u3002\\n\\n\u8981\u6C42\uFF1A\\n${shared}${styleLine}`\n}\n\nconst SEARCH_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u641C\u7D22\u52A9\u624B\u3002\u628A\u7528\u6237\u7684\u4E00\u53E5\u8BDD\u641C\u7D22\u9700\u6C42\u7FFB\u8BD1\u6210\u7ED3\u6784\u5316\u6761\u4EF6\u3002\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u89E3\u91CA\u6216\u4EE3\u7801\u5757\uFF1A\n{\"keywords\": [\"\u5173\u952E\u8BCD\"], \"from\": \"\u53D1\u4EF6\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32\", \"subject\": \"\u4E3B\u9898\u5173\u952E\u8BCD\u6216\u7A7A\u5B57\u7B26\u4E32\", \"since\": \"YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32\", \"before\": \"YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32\", \"explanation\": \"\u4E00\u53E5\u4E2D\u6587\u8BF4\u660E\u4F60\u7406\u89E3\u6210\u4E86\u4EC0\u4E48\"}\n\n\u89C4\u5219\uFF1A\n- \u4ECA\u5929\u662F {{today}}\uFF0C\u76F8\u5BF9\u65F6\u95F4\uFF08\u4E0A\u5468\u3001\u4E0A\u4E2A\u6708\u3001\u6700\u8FD1\u4E09\u5929\uFF09\u6362\u7B97\u6210 since/before\n- keywords \u53EA\u653E\u6B63\u6587/\u4E3B\u9898\u91CC\u53EF\u80FD\u51FA\u73B0\u7684\u5B9E\u8BCD\uFF0C2-4 \u4E2A\u5373\u53EF\n- subject \u53EA\u5728\u7528\u6237\u660E\u786E\u8BF4\u201C\u4E3B\u9898\u662F/\u6807\u9898\u662F\u201D\u65F6\u586B\u5199\uFF0C\u5426\u5219\u7559\u7A7A\uFF1B\u4E0D\u8981\u628A\u666E\u901A\u5173\u952E\u8BCD\u585E\u8FDB subject\n- \u4E0D\u786E\u5B9A\u7684\u5B57\u6BB5\u7559\u7A7A\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u7F16\u9020\n- \u6CA1\u6709\u53EF\u7528\u6761\u4EF6\u65F6 keywords \u4E3A\u7A7A\u6570\u7EC4`\n\nconst TASK_SYSTEM = `\u4F60\u662F\u4EFB\u52A1\u63D0\u53D6\u52A9\u624B\u3002\u4ECE\u90AE\u4EF6\u4E2D\u63D0\u53D6\u9700\u8981\u7528\u6237\u5904\u7406\u7684\u5177\u4F53\u4E8B\u9879\u3002\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001Markdown \u6216\u4EE3\u7801\u5757\u6807\u8BB0\u3002\u6BCF\u4E2A\u5143\u7D20\uFF1A\n{\"title\": \"\u4EFB\u52A1\u6807\u9898\uFF08\u52A8\u8BCD\u5F00\u5934\uFF0C\u5C3D\u91CF\u5177\u4F53\uFF09\", \"due\": \"YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32\", \"priority\": 0 \u5230 9 \u7684\u6574\u6570, \"owner\": \"\u8D1F\u8D23\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32\", \"notes\": \"\u4E00\u53E5\u6765\u6E90\u4F9D\u636E\"}\n\n\u89C4\u5219\uFF1A\n- \u53EA\u63D0\u53D6\u90AE\u4EF6\u91CC\u660E\u786E\u8981\u6C42\u3001\u6216\u660E\u663E\u9700\u8981\u8DDF\u8FDB\u7684\u4E8B\u9879\n- \u622A\u6B62\u65F6\u95F4\u80FD\u786E\u5B9A\u624D\u586B\uFF0C\u5426\u5219\u7559\u7A7A\n- \u6CA1\u6709\u53EF\u6267\u884C\u4E8B\u9879\u65F6\u8F93\u51FA []\n- \u4E0D\u8981\u7F16\u9020\u90AE\u4EF6\u91CC\u6CA1\u6709\u7684\u4FE1\u606F`\n\nconst STYLE_SYSTEM = `\u4F60\u662F\u5199\u4F5C\u98CE\u683C\u5206\u6790\u5E08\u3002\u9605\u8BFB\u7528\u6237\u6700\u8FD1\u53D1\u51FA\u7684\u90AE\u4EF6\uFF0C\u7528\u7B80\u4F53\u4E2D\u6587\u5199\u4E00\u6BB5 150-300 \u5B57\u7684\u5199\u4F5C\u98CE\u683C\u6863\u6848\uFF0C\u8986\u76D6\uFF1A\n\u5E38\u7528\u79F0\u547C\u3001\u5F00\u5934\u65B9\u5F0F\u3001\u7ED3\u5C3E\u4E0E\u7F72\u540D\u3001\u8BED\u6C14\uFF08\u6B63\u5F0F/\u4EB2\u5207/\u76F4\u63A5\uFF09\u3001\u53E5\u5B50\u957F\u77ED\u3001\u5E38\u7528\u8868\u8FBE\u6216\u53E3\u5934\u7985\u3002\n\u53EA\u8F93\u51FA\u6863\u6848\u6B63\u6587\uFF0C\u4E0D\u8981\u8F93\u51FA\u6807\u9898\u3001\u5217\u8868\u7B26\u53F7\u6216\u5206\u6790\u8FC7\u7A0B\u3002`\n\nfunction stylePath(root: string): string {\n return join(root, 'style.json')\n}\n\nfunction readStyle(root: string): StyleProfile | null {\n try {\n const parsed: unknown = JSON.parse(readFileSync(stylePath(root), 'utf8'))\n if (parsed === null || typeof parsed !== 'object') return null\n const value = parsed as Partial<StyleProfile>\n if (typeof value.profile !== 'string' || value.profile.trim() === '') return null\n return {\n profile: value.profile,\n updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : '',\n sampleCount: typeof value.sampleCount === 'number' ? value.sampleCount : 0,\n }\n } catch {\n return null\n }\n}\n\nfunction writeStyle(root: string, profile: StyleProfile): void {\n const file = stylePath(root)\n mkdirSync(dirname(file), { recursive: true })\n const temp = `${file}.${String(process.pid)}.tmp`\n writeFileSync(temp, `${JSON.stringify(profile, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temp, file)\n}\n\nfunction clip(value: string, max = MAX_PROMPT_CHARS): string {\n return value.length <= max ? value : `${value.slice(0, max - 2000)}\\n\u2026\uFF08\u4E2D\u95F4\u7701\u7565\uFF09\u2026\\n${value.slice(-2000)}`\n}\n\n/** Plain-text body of a mail message, falling back from HTML. */\nfunction bodyText(message: MessageDetail): string {\n if (message.text.trim() !== '') return message.text\n return (message.html ?? '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6])>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .trim()\n}\n\nfunction contextOf(message: MessageDetail): string {\n const lines = [\n `\u4E3B\u9898: ${message.subject}`,\n `\u53D1\u4EF6\u4EBA: ${message.from.name !== '' ? `${message.from.name} <${message.from.address}>` : message.from.address}`,\n `\u6536\u4EF6\u4EBA: ${message.to.map((item) => (item.name !== '' ? `${item.name} <${item.address}>` : item.address)).join(', ')}`,\n ...(message.cc.length === 0 ? [] : [`\u6284\u9001: ${message.cc.map((item) => item.address).join(', ')}`]),\n `\u65F6\u95F4: ${message.date}`,\n '',\n clip(bodyText(message)),\n ]\n return lines.join('\\n')\n}\n\n/**\n * Reading and writing assistance backed by the Host's model service.\n *\n * The service never sends mail: it only produces text for the panel, and every\n * send still needs the user's click. Nothing is sent to the model when AI is\n * disabled or the composition has no model service.\n */\nexport class AiService {\n private readonly deps: AiDeps\n\n constructor(deps: AiDeps) {\n this.deps = deps\n }\n\n private llm(): DshLlm | undefined {\n try {\n return this.deps.llm()\n } catch {\n return undefined\n }\n }\n\n status(account: Account | null): AiStatus {\n const config = this.deps.config()\n const llm = this.llm()\n const style = account === null ? null : this.style(account)\n return {\n available: llm !== undefined,\n enabled: config.ai.enabled,\n provider: config.ai.provider,\n model: config.ai.model,\n styleUpdatedAt: style?.updatedAt ?? '',\n styleSampleCount: style?.sampleCount ?? 0,\n ...(llm === undefined ? { error: '\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1' } : {}),\n }\n }\n\n style(account: Account): StyleProfile | null {\n return readStyle(this.deps.accountRoot(account))\n }\n\n saveStyle(account: Account, profile: string): StyleProfile {\n const text = profile.trim()\n if (text === '') throw new Error('\u5199\u4F5C\u98CE\u683C\u4E0D\u80FD\u4E3A\u7A7A')\n if (text.length > 8000) throw new Error('\u5199\u4F5C\u98CE\u683C\u8FC7\u957F\uFF08\u6700\u591A 8000 \u5B57\uFF09')\n const previous = this.style(account)\n const next: StyleProfile = {\n profile: text,\n updatedAt: new Date().toISOString(),\n sampleCount: previous?.sampleCount ?? 0,\n }\n writeStyle(this.deps.accountRoot(account), next)\n return next\n }\n\n private requireReady(): { provider: string; model: string } {\n const config = this.deps.config()\n if (!config.ai.enabled) throw new Error('AI \u529F\u80FD\u5DF2\u5173\u95ED\uFF0C\u8BF7\u5230\u8BBE\u7F6E\u91CC\u5F00\u542F')\n const llm = this.llm()\n if (llm === undefined) throw new Error('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1')\n if (config.ai.provider === '' || config.ai.model === '') throw new Error('\u8BF7\u5148\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 AI \u6A21\u578B')\n return { provider: config.ai.provider, model: config.ai.model }\n }\n\n private async complete(system: string, user: string, signal?: AbortSignal): Promise<string> {\n const { provider, model } = this.requireReady()\n const llm = this.llm()\n if (llm === undefined) throw new Error('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u670D\u52A1')\n const controller = new AbortController()\n const timer = setTimeout(() => {\n controller.abort()\n }, TIMEOUT_MS)\n const onAbort = (): void => {\n controller.abort()\n }\n signal?.addEventListener('abort', onAbort, { once: true })\n let text = ''\n try {\n const stream = llm.stream({\n provider,\n model,\n messages: [\n { role: 'system', content: [{ type: 'text', text: system }] },\n { role: 'user', content: [{ type: 'text', text: user }] },\n ],\n signal: controller.signal,\n })\n for await (const chunk of stream) {\n if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {\n text += chunk.text\n } else if (chunk.type === 'finish') {\n const reason = chunk.reason\n if (reason?.kind === 'error') {\n throw new Error(reason.failure?.message ?? reason.failure?.code ?? '\u6A21\u578B\u8C03\u7528\u5931\u8D25')\n }\n if (reason?.kind === 'aborted') throw new Error('\u6A21\u578B\u8C03\u7528\u5DF2\u53D6\u6D88')\n }\n }\n } finally {\n clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n }\n const trimmed = text.trim()\n if (trimmed === '') throw new Error('\u6A21\u578B\u6CA1\u6709\u8FD4\u56DE\u5185\u5BB9')\n return trimmed\n }\n\n async summarize(message: MessageDetail, signal?: AbortSignal): Promise<string> {\n return this.complete(SUMMARY_SYSTEM, contextOf(message), signal)\n }\n\n async reply(\n account: Account,\n message: MessageDetail,\n options: { instruction?: string; language?: 'zh' | 'en' } = {},\n signal?: AbortSignal,\n ): Promise<string> {\n const style = this.style(account)?.profile ?? ''\n const instruction = (options.instruction ?? '').trim()\n const user = [\n '\u539F\u59CB\u90AE\u4EF6\uFF1A',\n contextOf(message),\n '',\n instruction === '' ? '\u7528\u6237\u6CA1\u6709\u989D\u5916\u8981\u6C42\uFF0C\u8BF7\u5199\u4E00\u5C01\u5F97\u4F53\u7684\u56DE\u590D\u3002' : `\u7528\u6237\u7684\u8981\u6C42\uFF1A${instruction}`,\n ].join('\\n')\n return this.complete(REPLY_SYSTEM(options.language ?? 'zh', style), user, signal)\n }\n\n async translate(html: string, target: 'zh' | 'en', signal?: AbortSignal): Promise<string> {\n const text = html.trim()\n if (text === '') throw new Error('\u6B63\u6587\u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u7FFB\u8BD1')\n return this.complete(TRANSLATE_SYSTEM(target), clip(text, 20_000), signal)\n }\n\n /** Free-form question about a mail, with up to three earlier AI outputs as context. */\n async ask(\n message: MessageDetail,\n question: string,\n history: string[],\n signal?: AbortSignal,\n ): Promise<string> {\n const prompt = (question ?? '').trim()\n if (prompt === '') throw new Error('\u95EE\u9898\u4E0D\u80FD\u4E3A\u7A7A')\n const earlier = history\n .filter((item) => item.trim() !== '')\n .slice(-3)\n .map((item, index) => `\u3010\u5DF2\u6709\u8BB0\u5F55 ${String(index + 1)}\u3011\\n${clip(item, 4000)}`)\n .join('\\n\\n')\n const user = [\n '\u90AE\u4EF6\uFF1A',\n contextOf(message),\n ...(earlier === '' ? [] : ['', '\u5DF2\u6709 AI \u8BB0\u5F55\uFF1A', earlier]),\n '',\n `\u7528\u6237\u7684\u95EE\u9898\uFF1A${prompt}`,\n ].join('\\n')\n return this.complete(ASK_SYSTEM, user, signal)\n }\n\n /** Translate a natural-language search request into structured filters. */\n async parseSearch(query: string, signal?: AbortSignal): Promise<AiSearchFilters> {\n const today = new Date().toISOString().slice(0, 10)\n const system = SEARCH_SYSTEM.replace('{{today}}', today)\n const text = await this.complete(system, `\u641C\u7D22\u9700\u6C42\uFF1A${query.trim()}`, signal)\n return parseSearchJson(text)\n }\n\n /** Extract actionable tasks from one mail. */\n async extractTasks(message: MessageDetail, signal?: AbortSignal): Promise<TaskSuggestion[]> {\n const text = await this.complete(TASK_SYSTEM, contextOf(message), signal)\n return parseTaskJson(text)\n }\n\n /** Compose-body assistance: draft from points, polish, or shorten. */\n async compose(\n account: Account,\n options: {\n mode: 'draft' | 'polish' | 'shorten'\n instruction?: string\n html: string\n language?: 'zh' | 'en'\n },\n signal?: AbortSignal,\n ): Promise<string> {\n const style = this.style(account)?.profile ?? ''\n const language = options.language ?? 'zh'\n const body = options.html.trim()\n if (options.mode !== 'draft' && body === '') throw new Error('\u6B63\u6587\u4E3A\u7A7A')\n const source =\n options.mode === 'draft'\n ? `\u7528\u6237\u8981\u70B9\uFF1A${(options.instruction ?? '').trim() === '' ? '\u5199\u4E00\u5C01\u7B80\u77ED\u7684\u90AE\u4EF6' : (options.instruction ?? '').trim()}`\n : `\u9700\u8981\u5904\u7406\u7684\u6B63\u6587\uFF1A\\n${clip(htmlToPlain(body), 12000)}`\n return this.complete(COMPOSE_SYSTEM(options.mode, language, style), source, signal)\n }\n\n async learnStyle(\n account: Account,\n samples: { subject: string; text: string }[],\n signal?: AbortSignal,\n ): Promise<StyleProfile> {\n if (samples.length === 0) throw new Error('\u6CA1\u6709\u53EF\u7528\u7684\u5DF2\u53D1\u9001\u90AE\u4EF6\uFF0C\u5148\u5907\u4EFD\u4E00\u6B21\u90AE\u7BB1')\n const digest = samples\n .slice(0, 100)\n .map((sample, index) => `\u3010${String(index + 1)}\u3011\u4E3B\u9898\uFF1A${sample.subject}\\n${clip(sample.text, 800)}`)\n .join('\\n\\n')\n const profile = await this.complete(STYLE_SYSTEM, clip(digest, 35_000), signal)\n const next: StyleProfile = {\n profile,\n updatedAt: new Date().toISOString(),\n sampleCount: samples.length,\n }\n writeStyle(this.deps.accountRoot(account), next)\n return next\n }\n\n async models(): Promise<{ provider: string; name: string; models: { id: string; name: string }[] }[]> {\n const llm = this.llm()\n if (llm === undefined) return []\n const providers = llm.listProviders()\n const catalog: { provider: string; name: string; models: { id: string; name: string }[] }[] = []\n for (const provider of providers) {\n try {\n const models = await llm.listModels(provider.id)\n catalog.push({\n provider: provider.id,\n name: provider.name ?? provider.id,\n models: models.map((model) => ({ id: model.id, name: model.name ?? model.id })),\n })\n } catch {\n catalog.push({ provider: provider.id, name: provider.name ?? provider.id, models: [] })\n }\n }\n return catalog\n }\n}\n\n/**\n * The model may answer a translation with a complete HTML document. Keep the\n * body for insertion, and derive plain text for display so the panel never\n * shows raw tags.\n */\nexport function normalizeTranslated(value: string): { text: string; html: string } {\n const raw = value.trim()\n if (raw === '') return { text: '', html: '' }\n if (!/<[a-z][\\s\\S]*>/i.test(raw)) return { text: raw, html: textToHtml(raw) }\n let body = raw\n const match = /<body[^>]*>([\\s\\S]*?)<\\/body>/i.exec(raw)\n if (match !== null) body = match[1] ?? ''\n body = body\n .replace(/<!doctype[^>]*>/gi, '')\n .replace(/<head[\\s\\S]*?<\\/head>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .trim()\n const text = htmlToPlain(body)\n if (body === '' || text === '') return { text: raw, html: textToHtml(raw) }\n return { text, html: body }\n}\n\n/** Strip HTML tags for prompts that must receive plain text. */\nexport function htmlToPlain(html: string): string {\n return html\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6])>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nexport interface AiSearchFilters {\n keywords: string[]\n from: string\n subject: string\n since: string\n before: string\n explanation: string\n}\n\n/** Parse the model's search JSON, tolerating code fences and prose. */\nexport function parseSearchJson(text: string): AiSearchFilters {\n const start = text.indexOf('{')\n const end = text.lastIndexOf('}')\n const empty: AiSearchFilters = {\n keywords: [],\n from: '',\n subject: '',\n since: '',\n before: '',\n explanation: '',\n }\n if (start < 0 || end <= start) return empty\n let parsed: unknown\n try {\n parsed = JSON.parse(text.slice(start, end + 1))\n } catch {\n return empty\n }\n if (parsed === null || typeof parsed !== 'object') return empty\n const value = parsed as Record<string, unknown>\n const keywords = Array.isArray(value.keywords)\n ? value.keywords\n .map((item) => String(item).trim())\n .filter((item) => item !== '')\n .slice(0, 4)\n : []\n const date = (input: unknown): string =>\n /^\\d{4}-\\d{2}-\\d{2}$/.test(String(input ?? '')) ? String(input) : ''\n return {\n keywords,\n from: String(value.from ?? '')\n .trim()\n .slice(0, 120),\n subject: String(value.subject ?? '')\n .trim()\n .slice(0, 120),\n since: date(value.since),\n before: date(value.before),\n explanation: String(value.explanation ?? '')\n .trim()\n .slice(0, 200),\n }\n}\n\nexport interface TaskSuggestion {\n title: string\n due: string\n priority: number\n owner: string\n notes: string\n}\n\n/** Parse the model's JSON array, tolerating code fences and prose around it. */\nexport function parseTaskJson(text: string): TaskSuggestion[] {\n const start = text.indexOf('[')\n const end = text.lastIndexOf(']')\n if (start < 0 || end <= start) return []\n let parsed: unknown\n try {\n parsed = JSON.parse(text.slice(start, end + 1))\n } catch {\n return []\n }\n if (!Array.isArray(parsed)) return []\n const result: TaskSuggestion[] = []\n for (const item of parsed) {\n if (item === null || typeof item !== 'object') continue\n const value = item as Record<string, unknown>\n const title = String(value.title ?? '').trim()\n if (title === '') continue\n result.push({\n title: title.slice(0, 300),\n due: /^\\d{4}-\\d{2}-\\d{2}/.test(String(value.due ?? '')) ? String(value.due).slice(0, 10) : '',\n priority: Math.max(0, Math.min(9, Number(value.priority ?? 0) || 0)),\n owner: String(value.owner ?? '').slice(0, 120),\n notes: String(value.notes ?? '').slice(0, 1000),\n })\n }\n return result.slice(0, 30)\n}\n\n/** Markdown-ish summary \u2192 simple blocks for the panel. */\nexport function summaryBlocks(text: string): { title: string; body: string }[] {\n const blocks: { title: string; body: string }[] = []\n let current: { title: string; lines: string[] } = { title: '', lines: [] }\n const push = (): void => {\n const body = current.lines.join('\\n').trim()\n if (current.title !== '' || body !== '') blocks.push({ title: current.title, body })\n current = { title: '', lines: [] }\n }\n for (const line of text.split('\\n')) {\n const heading =\n /^#{1,4}\\s+(.*)$/.exec(line) ?? /^(\u7ED3\u8BBA|\u9700\u8981\u4F60\u505A\u4EC0\u4E48|\u5173\u952E\u4FE1\u606F|\u5EFA\u8BAE\u56DE\u590D\u8981\u70B9)\\s*$/.exec(line)\n if (heading !== null) {\n push()\n current.title = heading[1] ?? ''\n } else {\n current.lines.push(line)\n }\n }\n push()\n return blocks\n}\n\nexport function textToHtml(text: string): string {\n return text\n .split(/\\n{2,}/)\n .map(\n (paragraph) =>\n `<p>${paragraph.trim().replaceAll('&', '&').replaceAll('<', '<').replaceAll('\\n', '<br>')}</p>`,\n )\n .join('')\n}\n\nexport { existsSync }\n", "import { randomUUID } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { DshSessions, DshWorkspaceRegistry } from './dsh.js'\n\nexport interface ProjectMessage {\n accountId: string\n key: string\n subject: string\n date: string\n}\n\nexport interface ProjectThread {\n accountId: string\n key: string\n}\n\nexport interface ProjectRecord {\n id: string\n name: string\n path: string\n workspaceId: string\n sessionId: string\n accountIds: string[]\n messages: ProjectMessage[]\n threads: ProjectThread[]\n createdAt: string\n updatedAt: string\n}\n\ninterface ProjectFile {\n version: 1\n projects: ProjectRecord[]\n}\n\n/** JSON store for mail-linked projects (few records, no need for SQLite). */\nexport class ProjectStore {\n private readonly file: string\n private cache: ProjectFile | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n private load(): ProjectFile {\n if (this.cache !== null) return this.cache\n let parsed: unknown = null\n try {\n if (existsSync(this.file)) parsed = JSON.parse(readFileSync(this.file, 'utf8'))\n } catch {\n parsed = null\n }\n const projects =\n parsed !== null && typeof parsed === 'object' && Array.isArray((parsed as ProjectFile).projects)\n ? (parsed as ProjectFile).projects\n : []\n this.cache = { version: 1, projects: projects.filter(isProject) }\n return this.cache\n }\n\n private flush(): void {\n const data = this.load()\n mkdirSync(join(this.file, '..'), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.tmp`\n writeFileSync(temp, `${JSON.stringify(data, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temp, this.file)\n }\n\n list(): ProjectRecord[] {\n return [...this.load().projects].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n }\n\n get(id: string): ProjectRecord | undefined {\n return this.load().projects.find((project) => project.id === id)\n }\n\n findByMessage(accountId: string, key: string): ProjectRecord | undefined {\n return this.load().projects.find((project) =>\n project.messages.some((message) => message.accountId === accountId && message.key === key),\n )\n }\n\n create(record: ProjectRecord): ProjectRecord {\n const data = this.load()\n data.projects.push(record)\n this.flush()\n return record\n }\n\n update(id: string, patch: Partial<ProjectRecord>): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n Object.assign(project, patch, { updatedAt: new Date().toISOString() })\n this.flush()\n return project\n }\n\n addMessage(id: string, message: ProjectMessage): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n const exists = project.messages.some(\n (item) => item.accountId === message.accountId && item.key === message.key,\n )\n if (!exists) project.messages.push(message)\n if (!project.accountIds.includes(message.accountId)) project.accountIds.push(message.accountId)\n project.updatedAt = new Date().toISOString()\n this.flush()\n return project\n }\n\n removeMessage(id: string, accountId: string, key: string): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n project.messages = project.messages.filter((item) => !(item.accountId === accountId && item.key === key))\n project.updatedAt = new Date().toISOString()\n this.flush()\n return project\n }\n\n addThread(id: string, thread: ProjectThread): ProjectRecord | undefined {\n const project = this.get(id)\n if (project === undefined) return undefined\n if (!project.threads.some((item) => item.accountId === thread.accountId && item.key === thread.key)) {\n project.threads.push(thread)\n }\n project.updatedAt = new Date().toISOString()\n this.flush()\n return project\n }\n}\n\nfunction isProject(value: unknown): value is ProjectRecord {\n return (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as ProjectRecord).id === 'string' &&\n typeof (value as ProjectRecord).path === 'string'\n )\n}\n\nexport function slugify(name: string): string {\n const text = name\n .trim()\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n return text === '' ? 'project' : text.slice(0, 60)\n}\n\nexport interface CreateProjectInput {\n name: string\n root: string\n conversation: string\n accountId: string\n message?: ProjectMessage\n thread?: ProjectThread\n}\n\nexport interface CreateProjectResult {\n project: ProjectRecord\n warnings: string[]\n}\n\n/**\n * Create a project folder with a brief and the transferred AI conversation,\n * register it as a DSH Workspace, and open a session that starts from it.\n */\nexport async function createProject(options: {\n store: ProjectStore\n input: CreateProjectInput\n registry?: DshWorkspaceRegistry\n sessions?: DshSessions\n}): Promise<CreateProjectResult> {\n const { store, input } = options\n const name = input.name.trim()\n if (name === '') throw new Error('\u9879\u76EE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A')\n if (name.length > 120) throw new Error('\u9879\u76EE\u540D\u79F0\u8FC7\u957F')\n const root = input.root.trim()\n if (root === '') throw new Error('\u9879\u76EE\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A')\n\n const warnings: string[] = []\n const id = `prj_${randomUUID().slice(0, 8)}`\n const base = slugify(name)\n let path = join(root, base)\n let attempt = 2\n while (existsSync(path)) {\n path = join(root, `${base}-${String(attempt)}`)\n attempt += 1\n }\n mkdirSync(path, { recursive: true })\n\n const created = new Date().toISOString()\n const brief = renderBrief(name, input)\n writeFileSync(join(path, 'BRIEF.md'), brief, { mode: 0o600 })\n writeFileSync(join(path, 'CONVERSATION.md'), renderConversation(input.conversation, created), {\n mode: 0o600,\n })\n\n let workspaceId = ''\n if (options.registry !== undefined) {\n try {\n const workspace = await options.registry.create(path, name)\n workspaceId = String(workspace?.id ?? '')\n } catch (error) {\n warnings.push(`\u5DE5\u4F5C\u7A7A\u95F4\u6CE8\u518C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)\n }\n } else {\n warnings.push('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 workspaceRegistry\uFF0C\u9879\u76EE\u76EE\u5F55\u5DF2\u521B\u5EFA\u4F46\u6CA1\u6709\u6CE8\u518C\u5230\u4FA7\u680F')\n }\n\n let sessionId = ''\n if (options.sessions !== undefined) {\n try {\n sessionId = randomUUID()\n const session = options.sessions.create(sessionId, { meta: { cwd: path } })\n session.append(\n 'user/message',\n {\n role: 'user',\n content: [{ type: 'text', text: `\u6211\u521B\u5EFA\u4E86\u4E00\u4E2A\u90AE\u4EF6\u9879\u76EE\u300C${name}\u300D\u3002\\n\\n${brief}` }],\n source: { kind: 'user' },\n },\n { surfaceOp: 'append' },\n )\n } catch (error) {\n warnings.push(`\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)\n sessionId = ''\n }\n } else {\n warnings.push('\u5F53\u524D DSH \u7EC4\u5408\u6CA1\u6709 sessions \u670D\u52A1\uFF0C\u672A\u521B\u5EFA\u9879\u76EE\u4F1A\u8BDD')\n }\n\n const project: ProjectRecord = {\n id,\n name,\n path,\n workspaceId,\n sessionId,\n accountIds: [input.accountId],\n messages: input.message === undefined ? [] : [input.message],\n threads: input.thread === undefined ? [] : [input.thread],\n createdAt: created,\n updatedAt: created,\n }\n writeFileSync(\n join(path, 'project.json'),\n `${JSON.stringify({ ...project, conversation: undefined }, null, 2)}\\n`,\n {\n mode: 0o600,\n },\n )\n store.create(project)\n return { project, warnings }\n}\n\n/** Append new AI cards to an existing project's conversation file. */\nexport function appendConversation(project: ProjectRecord, conversation: string): string {\n const stamp = new Date().toISOString()\n const file = join(project.path, 'CONVERSATION.md')\n const block = `\\n\\n## \u8FFD\u52A0\u8BB0\u5F55 \u00B7 ${stamp}\\n\\n${conversation.trim()}\\n`\n writeFileSync(file, block, { flag: 'a', mode: 0o600 })\n return file\n}\n\nfunction renderBrief(name: string, input: CreateProjectInput): string {\n const lines = [`# ${name}`, '', `- \u521B\u5EFA\u65F6\u95F4\uFF1A${new Date().toISOString()}`, `- \u6765\u6E90\u8D26\u53F7\uFF1A${input.accountId}`]\n if (input.message !== undefined) {\n lines.push(`- \u6765\u6E90\u90AE\u4EF6\uFF1A${input.message.subject}\uFF08${input.message.date}\uFF09`)\n }\n lines.push(\n '',\n '## \u80CC\u666F',\n '',\n '\u7531 MailHarbor \u4ECE\u90AE\u4EF6\u751F\u6210\u3002\u4EE5\u4E0B\u662F\u5F53\u65F6 AI \u5DE5\u4F5C\u53F0\u7684\u8BB0\u5F55\u3002',\n '',\n '## AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55',\n '',\n input.conversation.trim(),\n '',\n )\n return lines.join('\\n')\n}\n\nfunction renderConversation(conversation: string, created: string): string {\n return `# AI \u5DE5\u4F5C\u53F0\u8BB0\u5F55\\n\\n- \u8F6C\u79FB\u5230\u9879\u76EE\uFF1A${created}\\n\\n${conversation.trim()}\\n`\n}\n", "/** Single source of the Host-side version (`npm run version` keeps it in sync). */\nexport const VERSION = '0.7.1'\n", "/** Convert the composer's HTML into a plain-text alternative for the message. */\nexport function htmlToText(html: string): string {\n return html\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|li|tr|h[1-6])>/gi, '\\n')\n .replace(/<li[^>]*>/gi, '\u2022 ')\n .replace(/<[^>]+>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nexport function escapeHtml(value: string): string {\n return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n}\n", "import { statSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Account, BackupStatus, ImapSettings } from '../shared/types.js'\nimport { CredentialManager } from './accounts.js'\nimport { AiService } from './ai.js'\nimport { AiThreadStore } from './ai-store.js'\nimport { ProjectStore } from './projects.js'\nimport { TaskStore } from './tasks.js'\nimport type { DshLlm, DshSessions, DshWorkspaceRegistry } from './dsh.js'\nimport { BackupEngine } from './backup.js'\nimport { DraftStore, draftPath } from './drafts.js'\nimport {\n type AccountRuntime,\n type MailHarborConfig,\n accountRuntime,\n activeAccount as pickActive,\n loadConfig,\n requireAccount,\n} from './config.js'\nimport { accountDataDir } from './paths.js'\nimport { ImapProvider, type ImapCredentials } from './imap.js'\nimport { MailDatabase } from './mail-db.js'\nimport { ReadStateStore } from './read-state.js'\n\ninterface SignatureEntry<T> {\n signature: string\n value: T\n}\n\nexport interface BackupJob {\n status: BackupStatus\n controller: AbortController\n promise: Promise<void>\n}\n\nconst MAX_OUTPUT_LINES = 200\n\n/**\n * Process-wide state for the plugin: one config, one credential manager, and\n * lazily created per-account providers and stores. Host code calls\n * {@link refresh} at the start of every request so external config edits are\n * picked up without a restart.\n */\nexport class MailHarborRuntime {\n private config: MailHarborConfig\n private configStamp = 0\n readonly credentials: CredentialManager\n private readonly providers = new Map<string, SignatureEntry<ImapProvider>>()\n private readonly databases = new Map<string, SignatureEntry<MailDatabase>>()\n private readonly draftStores = new Map<string, SignatureEntry<DraftStore>>()\n private readonly readStores = new Map<string, SignatureEntry<ReadStateStore>>()\n private readonly aiStores = new Map<string, SignatureEntry<AiThreadStore>>()\n private readonly backups = new Map<string, BackupJob>()\n private readonly llmProvider: () => DshLlm | undefined\n private readonly workspaceRegistryProvider: () => DshWorkspaceRegistry | undefined\n readonly sessions: DshSessions | undefined\n private aiService: AiService | null = null\n private projectStore: ProjectStore | null = null\n private taskStore: TaskStore | null = null\n private disposed = false\n\n constructor(\n options: {\n credentials?: CredentialManager\n llm?: () => DshLlm | undefined\n workspaceRegistry?: () => DshWorkspaceRegistry | undefined\n sessions?: DshSessions\n } = {},\n ) {\n this.config = loadConfig()\n this.credentials = options.credentials ?? new CredentialManager()\n this.llmProvider = options.llm ?? (() => undefined)\n this.workspaceRegistryProvider = options.workspaceRegistry ?? (() => undefined)\n this.sessions = options.sessions\n }\n\n get workspaceRegistry(): DshWorkspaceRegistry | undefined {\n return this.workspaceRegistryProvider()\n }\n\n projects(): ProjectStore {\n if (this.projectStore === null) {\n this.projectStore = new ProjectStore(join(this.config.dataDir, 'projects.json'))\n }\n return this.projectStore\n }\n\n tasks(): TaskStore {\n if (this.taskStore === null) {\n this.taskStore = new TaskStore(join(this.config.dataDir, 'state', 'state.db'))\n }\n return this.taskStore\n }\n\n /** The Host model service, when the composition provides one. */\n llm(): DshLlm | undefined {\n try {\n return this.llmProvider()\n } catch {\n return undefined\n }\n }\n\n ai(): AiService {\n if (this.aiService === null) {\n this.aiService = new AiService({\n config: () => this.config,\n llm: () => this.llm(),\n accountRoot: (account) => accountDataDir(account),\n })\n }\n return this.aiService\n }\n\n get current(): MailHarborConfig {\n return this.config\n }\n\n /** Re-read the config file when it changed on disk. */\n refresh(): MailHarborConfig {\n if (this.disposed) return this.config\n let stamp = 0\n try {\n const stats = statSync(this.config.configFile)\n stamp = stats.mtimeMs\n } catch {\n stamp = 0\n }\n if (stamp !== this.configStamp) {\n this.configStamp = stamp\n this.config = loadConfig()\n this.prune()\n }\n return this.config\n }\n\n private prune(): void {\n const live = new Set(this.config.accounts.map((account) => account.id))\n for (const id of [...this.providers.keys()]) {\n if (!live.has(id)) {\n void this.providers.get(id)?.value.dispose()\n this.providers.delete(id)\n }\n }\n for (const id of [...this.databases.keys()]) {\n if (!live.has(id)) {\n this.databases.get(id)?.value.close()\n this.databases.delete(id)\n }\n }\n for (const id of [...this.draftStores.keys()]) if (!live.has(id)) this.draftStores.delete(id)\n for (const id of [...this.readStores.keys()]) if (!live.has(id)) this.readStores.delete(id)\n for (const id of [...this.aiStores.keys()]) if (!live.has(id)) this.aiStores.delete(id)\n }\n\n /** Drop cached connections and stores for one account after a settings change. */\n invalidate(accountId: string): void {\n void this.providers.get(accountId)?.value.dispose()\n this.providers.delete(accountId)\n this.databases.get(accountId)?.value.close()\n this.databases.delete(accountId)\n this.draftStores.delete(accountId)\n this.readStores.delete(accountId)\n this.aiStores.delete(accountId)\n this.refresh()\n }\n\n active(): Account | null {\n return pickActive(this.config)\n }\n\n account(id?: string | null): Account {\n return requireAccount(this.config, id)\n }\n\n paths(account: Account): AccountRuntime {\n return accountRuntime(account)\n }\n\n /** Credentials for an account, with a clear message when the user must log in. */\n credentialsFor(account: Account): ImapCredentials {\n if (account.auth.type === 'oauth2') {\n throw new Error('OAuth2 \u8D26\u53F7\u6682\u672A\u5728\u6B64\u7248\u672C\u542F\u7528\uFF0C\u8BF7\u4F7F\u7528\u5E94\u7528\u4E13\u7528\u5BC6\u7801\u767B\u5F55')\n }\n const resolved = this.credentials.resolve(account)\n if (resolved === undefined) throw new Error('\u8BF7\u5148\u767B\u5F55\u90AE\u7BB1\u8D26\u53F7')\n return { user: account.username || account.email, pass: resolved.password }\n }\n\n hasCredentials(account: Account): boolean {\n return this.credentials.resolve(account) !== undefined\n }\n\n imapSettings(account: Account): ImapSettings {\n return account.imap\n }\n\n provider(account: Account): ImapProvider {\n const signature = `${account.imap.host}:${String(account.imap.port)}:${String(account.imap.secure)}:${account.username}:${account.auth.type}`\n const existing = this.providers.get(account.id)\n if (existing !== undefined && existing.signature === signature) return existing.value\n if (existing !== undefined) void existing.value.dispose()\n const created = new ImapProvider(account.imap, this.credentialsFor(account), {\n onReconnect: () => {\n // Connection drops are expected on long-lived accounts; the next\n // operation reconnects transparently.\n },\n })\n this.providers.set(account.id, { signature, value: created })\n return created\n }\n\n database(account: Account): MailDatabase {\n const root = this.paths(account)\n const existing = this.databases.get(account.id)\n if (existing !== undefined && existing.signature === root.mailDb) return existing.value\n if (existing !== undefined) existing.value.close()\n const created = new MailDatabase(root.mailDb, {\n maxBodyChars: 400_000,\n attachmentsRoot: root.attachments,\n })\n this.databases.set(account.id, { signature: root.mailDb, value: created })\n return created\n }\n\n drafts(account: Account): DraftStore {\n const file = draftPath(this.paths(account).root)\n const existing = this.draftStores.get(account.id)\n if (existing !== undefined && existing.signature === file) return existing.value\n const created = new DraftStore(file, account.id)\n this.draftStores.set(account.id, { signature: file, value: created })\n return created\n }\n\n aiThreads(account: Account): AiThreadStore {\n const file = join(this.paths(account).root, 'ai-threads.json')\n const existing = this.aiStores.get(account.id)\n if (existing !== undefined && existing.signature === file) return existing.value\n const created = new AiThreadStore(file)\n this.aiStores.set(account.id, { signature: file, value: created })\n return created\n }\n\n readState(account: Account): ReadStateStore {\n const file = join(this.paths(account).root, 'read-state.json')\n const existing = this.readStores.get(account.id)\n if (existing !== undefined && existing.signature === file) return existing.value\n const created = new ReadStateStore(file)\n this.readStores.set(account.id, { signature: file, value: created })\n return created\n }\n\n backupStatus(account: Account): BackupStatus {\n const job = this.backups.get(account.id)\n if (job !== undefined) return { ...job.status, output: [...job.status.output] }\n const persisted = BackupEngine.status(this.paths(account).mailDb)\n return { ...persisted, accountId: account.id }\n }\n\n /** Start an incremental backup; returns the job so the caller can stream output. */\n startBackup(\n account: Account,\n onEvent?: (event: { message: string; mailbox?: string; added?: number }) => void,\n ): BackupJob {\n const running = this.backups.get(account.id)\n if (running !== undefined && running.status.running) throw new Error('\u5907\u4EFD\u5DF2\u5728\u8FDB\u884C\u4E2D')\n const controller = new AbortController()\n const status: BackupStatus = {\n running: true,\n accountId: account.id,\n startedAt: new Date().toISOString(),\n finishedAt: null,\n lastSuccessAt: null,\n total: 0,\n added: 0,\n message: '\u51C6\u5907\u5907\u4EFD',\n output: [],\n exitCode: null,\n }\n const job: BackupJob = { status, controller, promise: Promise.resolve() }\n const engine = new BackupEngine(accountRuntime(account), this.credentialsFor(account))\n job.promise = (async () => {\n try {\n const result = await engine.run({\n signal: controller.signal,\n onEvent: (event) => {\n status.message = event.message\n if (event.mailbox !== undefined) status.message = event.message\n if (event.added !== undefined) status.added = event.added\n if (event.total !== undefined) status.total = event.total\n status.output.push(event.message)\n if (status.output.length > MAX_OUTPUT_LINES)\n status.output.splice(0, status.output.length - MAX_OUTPUT_LINES)\n onEvent?.(event)\n },\n })\n status.total = result.total\n status.added = result.newCount\n status.exitCode = 0\n status.message = `\u5907\u4EFD\u5B8C\u6210\uFF1A\u65B0\u589E ${String(result.newCount)} \u5C01\uFF0C\u5171 ${String(result.total)} \u5C01`\n } catch (error) {\n status.exitCode = 1\n status.message = error instanceof Error ? error.message : String(error)\n status.output.push(status.message)\n } finally {\n status.running = false\n status.finishedAt = new Date().toISOString()\n if (status.exitCode === 0) status.lastSuccessAt = status.finishedAt\n this.databases.get(account.id)?.value.close()\n this.databases.delete(account.id)\n }\n })()\n this.backups.set(account.id, job)\n return job\n }\n\n stopBackup(account: Account): boolean {\n const job = this.backups.get(account.id)\n if (job === undefined || !job.status.running) return false\n job.controller.abort()\n return true\n }\n\n async dispose(): Promise<void> {\n this.disposed = true\n for (const job of this.backups.values()) job.controller.abort()\n for (const provider of this.providers.values()) await provider.value.dispose()\n this.providers.clear()\n for (const db of this.databases.values()) db.value.close()\n this.databases.clear()\n this.taskStore?.close()\n }\n}\n", "import { spawnSync } from 'node:child_process'\n\nexport type SecretProvider = 'macos' | 'libsecret' | 'windows' | 'none'\n\nexport interface CommandResult {\n ok: boolean\n stdout: string\n stderr: string\n}\n\nexport interface SecretStoreOptions {\n platform?: NodeJS.Platform\n run?: (command: string, args: string[], options?: { input?: string; timeout?: number }) => CommandResult\n hasCommand?: (command: string, args: string[]) => boolean\n}\n\nfunction defaultRun(\n command: string,\n args: string[],\n options: { input?: string; timeout?: number } = {},\n): CommandResult {\n const result = spawnSync(command, args, {\n encoding: 'utf8',\n timeout: options.timeout ?? 10_000,\n input: options.input,\n stdio: options.input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],\n })\n return { ok: result.status === 0, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }\n}\n\nfunction defaultHasCommand(command: string, args: string[]): boolean {\n const result = spawnSync(command, args, { timeout: 3000, stdio: 'ignore' })\n return result.error === undefined && result.status === 0\n}\n\nfunction psString(value: string): string {\n return `'${value.replaceAll(\"'\", \"''\")}'`\n}\n\n/**\n * Cross-platform secret persistence.\n *\n * macOS uses the Keychain (`security`), Linux uses libsecret (`secret-tool`),\n * Windows uses the Credential Locker through PowerShell. When no provider is\n * available the store reports unavailable; callers keep the password in this\n * process's session and never write it to the config.\n */\nexport class SecretStore {\n readonly provider: SecretProvider\n private readonly run: NonNullable<SecretStoreOptions['run']>\n private readonly hasCommand: NonNullable<SecretStoreOptions['hasCommand']>\n\n constructor(options: SecretStoreOptions = {}) {\n const platform = options.platform ?? process.platform\n this.run = options.run ?? defaultRun\n this.hasCommand = options.hasCommand ?? defaultHasCommand\n if (platform === 'darwin') {\n this.provider = 'macos'\n } else if (platform === 'win32') {\n this.provider = this.hasCommand('powershell.exe', [\n '-NoProfile',\n '-NonInteractive',\n '-Command',\n 'exit 0',\n ])\n ? 'windows'\n : 'none'\n } else if (platform === 'linux') {\n this.provider = this.hasCommand('secret-tool', ['--version']) ? 'libsecret' : 'none'\n } else {\n this.provider = 'none'\n }\n }\n\n available(): boolean {\n return this.provider !== 'none'\n }\n\n read(service: string, account: string): string | undefined {\n if (service === '' || account === '') return undefined\n if (this.provider === 'macos') {\n const result = this.run('security', ['find-generic-password', '-s', service, '-a', account, '-w'])\n if (!result.ok) return undefined\n return trimSecret(result.stdout)\n }\n if (this.provider === 'libsecret') {\n const result = this.run('secret-tool', ['lookup', 'service', service, 'account', account])\n if (!result.ok) return undefined\n return trimSecret(result.stdout)\n }\n if (this.provider === 'windows') {\n const script = [\n \"$ErrorActionPreference = 'Stop'\",\n '[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null',\n '$vault = New-Object Windows.Security.Credentials.PasswordVault',\n `$resource = ${psString(service)}`,\n `$user = ${psString(account)}`,\n '$cred = $vault.FindAllByResource($resource) | Where-Object { $_.UserName -eq $user } | Select-Object -First 1',\n 'if ($null -eq $cred) { exit 1 }',\n '$cred.RetrievePassword()',\n '[Console]::Out.Write($cred.Password)',\n ].join('\\n')\n const result = this.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {\n input: script,\n })\n if (!result.ok) return undefined\n return trimSecret(result.stdout)\n }\n return undefined\n }\n\n write(service: string, account: string, secret: string): boolean {\n if (service === '' || account === '' || secret === '') return false\n if (this.provider === 'macos') {\n return this.run('security', ['add-generic-password', '-U', '-s', service, '-a', account, '-w', secret])\n .ok\n }\n if (this.provider === 'libsecret') {\n return this.run('secret-tool', ['store', '--label', service, 'service', service, 'account', account], {\n input: secret,\n }).ok\n }\n if (this.provider === 'windows') {\n const encoded = Buffer.from(secret, 'utf8').toString('base64')\n const script = [\n \"$ErrorActionPreference = 'Stop'\",\n '[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null',\n '$vault = New-Object Windows.Security.Credentials.PasswordVault',\n `$resource = ${psString(service)}`,\n `$user = ${psString(account)}`,\n `$password = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,\n '$vault.FindAllByResource($resource) | Where-Object { $_.UserName -eq $user } | ForEach-Object { $vault.Remove($_) }',\n '$vault.Add((New-Object Windows.Security.Credentials.PasswordCredential($resource, $user, $password)))',\n ].join('\\n')\n return this.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {\n input: script,\n }).ok\n }\n return false\n }\n\n delete(service: string, account: string): boolean {\n if (service === '' || account === '') return false\n if (this.provider === 'macos') {\n return this.run('security', ['delete-generic-password', '-s', service, '-a', account]).ok\n }\n if (this.provider === 'libsecret') {\n return this.run('secret-tool', ['clear', 'service', service, 'account', account]).ok\n }\n if (this.provider === 'windows') {\n const script = [\n \"$ErrorActionPreference = 'Stop'\",\n '[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null',\n '$vault = New-Object Windows.Security.Credentials.PasswordVault',\n `$resource = ${psString(service)}`,\n `$user = ${psString(account)}`,\n '$vault.FindAllByResource($resource) | Where-Object { $_.UserName -eq $user } | ForEach-Object { $vault.Remove($_) }',\n ].join('\\n')\n return this.run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {\n input: script,\n }).ok\n }\n return false\n }\n}\n\nfunction trimSecret(value: string): string | undefined {\n const text = value.replace(/\\r?\\n$/, '')\n return text === '' ? undefined : text\n}\n", "import type { Account } from '../shared/types.js'\nimport { SecretStore } from './secrets.js'\n\nexport type PasswordSource = 'session' | 'env' | 'keychain'\n\nexport interface ResolvedPassword {\n password: string\n source: PasswordSource\n}\n\nexport interface CredentialStatus {\n hasPassword: boolean\n source: PasswordSource | null\n provider: string\n}\n\nfunction envKeyFor(account: Account): string {\n const suffix = account.id.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()\n return `DSH_MAILHARBOR_PASSWORD_${suffix}`\n}\n\n/**\n * Account passwords live in three places, in priority order: this process's\n * session (a fresh login), the environment (CI and scripted installs), and the\n * OS credential store. They are never written to `config.json`.\n */\nexport class CredentialManager {\n private readonly session = new Map<string, string>()\n private readonly secrets: SecretStore\n\n constructor(secrets: SecretStore = new SecretStore()) {\n this.secrets = secrets\n }\n\n get provider(): string {\n return this.secrets.provider\n }\n\n resolve(account: Account): ResolvedPassword | undefined {\n const session = this.session.get(account.id)\n if (session !== undefined && session !== '') return { password: session, source: 'session' }\n const fromEnv = process.env[envKeyFor(account)]\n if (typeof fromEnv === 'string' && fromEnv !== '') return { password: fromEnv, source: 'env' }\n const stored = this.secrets.read(account.keychainService, account.email)\n if (stored !== undefined) return { password: stored, source: 'keychain' }\n return undefined\n }\n\n /** Keep the password for this run and, when asked, for later runs. */\n remember(account: Account, password: string, persist: boolean): { stored: boolean } {\n if (password === '') throw new Error('\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A')\n this.session.set(account.id, password)\n if (!persist) return { stored: false }\n return { stored: this.secrets.write(account.keychainService, account.email, password) }\n }\n\n forget(account: Account): void {\n this.session.delete(account.id)\n this.secrets.delete(account.keychainService, account.email)\n }\n\n /** Read an arbitrary secret (for example a CalDAV app password). */\n readSecret(service: string, account: string): string | undefined {\n return this.secrets.read(service, account)\n }\n\n writeSecret(service: string, account: string, secret: string): boolean {\n return this.secrets.write(service, account, secret)\n }\n\n deleteSecret(service: string, account: string): boolean {\n return this.secrets.delete(service, account)\n }\n\n status(account: Account): CredentialStatus {\n const resolved = this.resolve(account)\n return {\n hasPassword: resolved !== undefined,\n source: resolved?.source ?? null,\n provider: this.secrets.provider,\n }\n }\n}\n", "import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport type { AiCard, AiThread } from '../shared/types.js'\n\nconst MAX_THREADS = 50\nconst MAX_CARDS_PER_THREAD = 50\nconst MAX_FILE_BYTES = 2 * 1024 * 1024\n\ninterface ThreadFile {\n version: 1\n threads: Record<string, AiThread>\n}\n\n/**\n * Per-account store for AI cards.\n *\n * Cards persist so closing the panel, switching mail or restarting DSH never\n * loses an AI result. The file is capped: oldest threads are dropped first,\n * then the oldest cards inside a thread.\n */\nexport class AiThreadStore {\n private readonly file: string\n private cache: ThreadFile | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n private load(): ThreadFile {\n if (this.cache !== null) return this.cache\n let parsed: unknown = null\n try {\n if (existsSync(this.file)) parsed = JSON.parse(readFileSync(this.file, 'utf8'))\n } catch {\n parsed = null\n }\n const threads =\n parsed !== null && typeof parsed === 'object' && (parsed as ThreadFile).threads !== undefined\n ? (parsed as ThreadFile).threads\n : {}\n this.cache = { version: 1, threads: sanitize(threads) }\n return this.cache\n }\n\n private flush(): void {\n const data = this.load()\n prune(data)\n const text = `${JSON.stringify(data, null, 2)}\\n`\n mkdirSync(dirname(this.file), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.tmp`\n writeFileSync(temp, text, { mode: 0o600 })\n renameSync(temp, this.file)\n }\n\n get(key: string): AiThread | null {\n const thread = this.load().threads[key]\n return thread === undefined ? null : { ...thread, cards: [...thread.cards] }\n }\n\n appendCard(input: {\n key: string\n accountId: string\n subject: string\n messageId?: string\n card: AiCard\n }): AiThread {\n const data = this.load()\n const now = new Date().toISOString()\n const existing = data.threads[input.key]\n const thread: AiThread = existing ?? {\n key: input.key,\n accountId: input.accountId,\n subject: input.subject,\n messageId: input.messageId ?? '',\n cards: [],\n updatedAt: now,\n }\n thread.accountId = input.accountId\n thread.subject = input.subject\n if (input.messageId !== undefined && input.messageId !== '') thread.messageId = input.messageId\n thread.cards.push(input.card)\n thread.updatedAt = now\n data.threads[input.key] = thread\n this.flush()\n return { ...thread, cards: [...thread.cards] }\n }\n\n removeCard(key: string, cardId: string): AiThread | null {\n const data = this.load()\n const thread = data.threads[key]\n if (thread === undefined) return null\n thread.cards = thread.cards.filter((card) => card.id !== cardId)\n thread.updatedAt = new Date().toISOString()\n if (thread.cards.length === 0) {\n delete data.threads[key]\n this.flush()\n return null\n }\n this.flush()\n return { ...thread, cards: [...thread.cards] }\n }\n\n clear(key: string): boolean {\n const data = this.load()\n if (data.threads[key] === undefined) return false\n delete data.threads[key]\n this.flush()\n return true\n }\n\n /** Number of threads, for the settings page. */\n size(): number {\n return Object.keys(this.load().threads).length\n }\n}\n\nfunction sanitize(input: unknown): Record<string, AiThread> {\n if (input === null || typeof input !== 'object') return {}\n const source = input as Record<string, unknown>\n const result: Record<string, AiThread> = {}\n for (const [key, raw] of Object.entries(source)) {\n if (raw === null || typeof raw !== 'object') continue\n const thread = raw as Partial<AiThread>\n if (typeof thread.accountId !== 'string' || !Array.isArray(thread.cards)) continue\n const cards: AiCard[] = thread.cards\n .filter(\n (card): card is AiCard => card !== null && typeof card === 'object' && typeof card.id === 'string',\n )\n .map((card) => ({\n id: card.id,\n kind: card.kind,\n instruction: String(card.instruction ?? ''),\n output: String(card.output ?? ''),\n ...(typeof card.html === 'string' ? { html: card.html } : {}),\n ...(card.meta !== undefined ? { meta: card.meta } : {}),\n provider: String(card.provider ?? ''),\n model: String(card.model ?? ''),\n createdAt: String(card.createdAt ?? ''),\n }))\n result[key] = {\n key,\n accountId: thread.accountId,\n subject: String(thread.subject ?? ''),\n messageId: String(thread.messageId ?? ''),\n cards,\n updatedAt: String(thread.updatedAt ?? ''),\n }\n }\n return result\n}\n\nfunction prune(data: ThreadFile): void {\n const entries = Object.entries(data.threads).sort((left, right) =>\n right[1].updatedAt.localeCompare(left[1].updatedAt),\n )\n const kept = new Map(entries.slice(0, MAX_THREADS))\n for (const thread of kept.values()) {\n if (thread.cards.length > MAX_CARDS_PER_THREAD) {\n thread.cards = thread.cards.slice(-MAX_CARDS_PER_THREAD)\n }\n }\n data.threads = Object.fromEntries(kept)\n let size = JSON.stringify(data).length\n while (size > MAX_FILE_BYTES && kept.size > 1) {\n const oldest = [...kept.entries()].sort((left, right) =>\n left[1].updatedAt.localeCompare(right[1].updatedAt),\n )[0]\n if (oldest === undefined) break\n kept.delete(oldest[0])\n data.threads = Object.fromEntries(kept)\n size = JSON.stringify(data).length\n }\n}\n", "import { randomUUID } from 'node:crypto'\nimport { mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { DatabaseSync } from 'node:sqlite'\n\nconst STATUSES = new Set(['NEEDS-ACTION', 'IN-PROCESS', 'COMPLETED', 'CANCELLED'])\n\nfunction now(): string {\n return new Date().toISOString()\n}\n\nexport interface TaskRecord {\n id: string\n accountId: string\n projectId: string | null\n calendarHref: string | null\n href: string | null\n etag: string | null\n uid: string\n title: string\n notes: string\n due: string\n status: string\n priority: number\n owner: string\n messageKey: string\n messageSubject: string\n completedAt: string | null\n dirty: boolean\n deleted: boolean\n createdAt: string\n updatedAt: string\n}\n\nfunction rowToTask(row: Record<string, unknown> | undefined): TaskRecord | undefined {\n if (row === undefined) return undefined\n return {\n id: String(row.id),\n accountId: String(row.account_id),\n projectId: row.project_id === null || row.project_id === undefined ? null : String(row.project_id),\n calendarHref:\n row.calendar_href === null || row.calendar_href === undefined ? null : String(row.calendar_href),\n href: row.href === null || row.href === undefined ? null : String(row.href),\n etag: row.etag === null || row.etag === undefined ? null : String(row.etag),\n uid: String(row.uid),\n title: String(row.title),\n notes: String(row.notes ?? ''),\n due: String(row.due ?? ''),\n status: String(row.status ?? 'NEEDS-ACTION'),\n priority: Number(row.priority ?? 0),\n owner: String(row.owner ?? ''),\n messageKey: String(row.message_key ?? ''),\n messageSubject: String(row.message_subject ?? ''),\n completedAt:\n row.completed_at === null || row.completed_at === undefined ? null : String(row.completed_at),\n dirty: Number(row.dirty) === 1,\n deleted: Number(row.deleted) === 1,\n createdAt: String(row.created_at ?? ''),\n updatedAt: String(row.updated_at ?? ''),\n }\n}\n\nfunction statusOf(value: unknown): string {\n const text = String(value ?? 'NEEDS-ACTION').toUpperCase()\n return STATUSES.has(text) ? text : 'NEEDS-ACTION'\n}\n\n/**\n * Local VTODO store in `<DataRoot>/state/state.db`.\n *\n * Local edits set dirty=1; CalDAV sync pushes dirty rows and applies remote\n * rows when there is no pending local edit. Deletions leave a tombstone until\n * the remote DELETE succeeds.\n */\nexport class TaskStore {\n private readonly file: string\n private db: DatabaseSync | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n close(): void {\n if (this.db !== null) {\n this.db.close()\n this.db = null\n }\n }\n\n private open(): DatabaseSync {\n if (this.db !== null) return this.db\n mkdirSync(dirname(this.file), { recursive: true })\n const db = new DatabaseSync(this.file)\n db.exec('PRAGMA journal_mode=WAL')\n db.exec('PRAGMA busy_timeout=10000')\n db.exec(`\n CREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n account_id TEXT NOT NULL,\n project_id TEXT,\n calendar_href TEXT,\n href TEXT,\n etag TEXT,\n uid TEXT NOT NULL,\n title TEXT NOT NULL,\n notes TEXT,\n due TEXT,\n status TEXT NOT NULL DEFAULT 'NEEDS-ACTION',\n priority INTEGER NOT NULL DEFAULT 0,\n owner TEXT,\n message_key TEXT,\n message_subject TEXT,\n completed_at TEXT,\n dirty INTEGER NOT NULL DEFAULT 1,\n deleted INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS tasks_account_due ON tasks (account_id, deleted, status, due);\n CREATE INDEX IF NOT EXISTS tasks_project ON tasks (project_id);\n CREATE TABLE IF NOT EXISTS calendar_state (\n account_id TEXT PRIMARY KEY,\n calendar_href TEXT,\n display_name TEXT,\n ctag TEXT,\n last_sync TEXT,\n error TEXT\n );\n `)\n this.db = db\n return db\n }\n\n list(options: {\n accountId: string\n status?: string\n projectId?: string\n query?: string\n limit?: number\n }): TaskRecord[] {\n const db = this.open()\n const where = ['account_id = ?', 'deleted = 0']\n const params: (string | number)[] = [options.accountId]\n if (options.status !== undefined && options.status !== '') {\n where.push('status = ?')\n params.push(statusOf(options.status))\n }\n if (options.projectId !== undefined && options.projectId !== '') {\n where.push('project_id = ?')\n params.push(options.projectId)\n }\n const text = String(options.query ?? '').trim()\n if (text !== '') {\n where.push('(title LIKE ? OR notes LIKE ? OR owner LIKE ?)')\n const like = `%${text}%`\n params.push(like, like, like)\n }\n const rows = db\n .prepare(\n `SELECT * FROM tasks WHERE ${where.join(' AND ')}\n ORDER BY CASE WHEN completed_at IS NOT NULL THEN 1 ELSE 0 END,\n CASE WHEN due IS NULL OR due = '' THEN 1 ELSE 0 END, due ASC, created_at DESC\n LIMIT ?`,\n )\n .all(...params, Math.max(1, Math.min(1000, options.limit ?? 500)))\n return rows\n .map((row) => rowToTask(row as Record<string, unknown>))\n .filter((task): task is TaskRecord => task !== undefined)\n }\n\n get(id: string): TaskRecord | undefined {\n return rowToTask(\n this.open().prepare('SELECT * FROM tasks WHERE id = ?').get(id) as Record<string, unknown> | undefined,\n )\n }\n\n create(input: {\n accountId: string\n title: string\n notes?: string\n due?: string\n status?: string\n priority?: number\n owner?: string\n projectId?: string | null\n messageKey?: string\n messageSubject?: string\n }): TaskRecord {\n const title = String(input.title ?? '').trim()\n if (title === '') throw new Error('\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A')\n if (title.length > 300) throw new Error('\u4EFB\u52A1\u6807\u9898\u8FC7\u957F')\n const status = statusOf(input.status)\n const id = `task_${randomUUID()}`\n const stamp = now()\n this.open()\n .prepare(\n `INSERT INTO tasks (id, account_id, project_id, uid, title, notes, due, status, priority, owner,\n message_key, message_subject, completed_at, dirty, deleted, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)`,\n )\n .run(\n id,\n input.accountId,\n input.projectId ?? null,\n `${randomUUID()}@dsh-mailharbor`,\n title,\n String(input.notes ?? ''),\n String(input.due ?? ''),\n status,\n Math.max(0, Math.min(9, Number(input.priority ?? 0))),\n String(input.owner ?? ''),\n String(input.messageKey ?? ''),\n String(input.messageSubject ?? ''),\n status === 'COMPLETED' ? stamp : null,\n stamp,\n stamp,\n )\n const task = this.get(id)\n if (task === undefined) throw new Error('\u4EFB\u52A1\u521B\u5EFA\u5931\u8D25')\n return task\n }\n\n update(id: string, fields: Partial<TaskRecord> & { projectId?: string | null }): TaskRecord {\n const current = this.get(id)\n if (current === undefined) throw new Error('\u4EFB\u52A1\u4E0D\u5B58\u5728')\n const next = {\n title: fields.title === undefined ? current.title : String(fields.title).trim(),\n notes: fields.notes === undefined ? current.notes : String(fields.notes ?? ''),\n due: fields.due === undefined ? current.due : String(fields.due ?? ''),\n status: fields.status === undefined ? current.status : statusOf(fields.status),\n priority: fields.priority === undefined ? current.priority : Number(fields.priority),\n owner: fields.owner === undefined ? current.owner : String(fields.owner ?? ''),\n projectId: fields.projectId === undefined ? current.projectId : fields.projectId,\n messageKey: fields.messageKey === undefined ? current.messageKey : String(fields.messageKey),\n messageSubject:\n fields.messageSubject === undefined ? current.messageSubject : String(fields.messageSubject),\n }\n if (next.title === '') throw new Error('\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A')\n const stamp = now()\n const completedAt = next.status === 'COMPLETED' ? (current.completedAt ?? stamp) : null\n this.open()\n .prepare(\n `UPDATE tasks SET title = ?, notes = ?, due = ?, status = ?, priority = ?, owner = ?, project_id = ?,\n message_key = ?, message_subject = ?, completed_at = ?, dirty = 1, updated_at = ? WHERE id = ?`,\n )\n .run(\n next.title,\n next.notes,\n next.due,\n next.status,\n Math.max(0, Math.min(9, Number(next.priority) || 0)),\n next.owner,\n next.projectId,\n next.messageKey,\n next.messageSubject,\n completedAt,\n stamp,\n id,\n )\n const task = this.get(id)\n if (task === undefined) throw new Error('\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25')\n return task\n }\n\n remove(id: string): boolean {\n const current = this.get(id)\n if (current === undefined) return false\n if (current.href !== null && current.href !== '') {\n this.open()\n .prepare('UPDATE tasks SET deleted = 1, dirty = 1, updated_at = ? WHERE id = ?')\n .run(now(), id)\n } else {\n this.open().prepare('DELETE FROM tasks WHERE id = ?').run(id)\n }\n return true\n }\n\n /** Apply a VTODO fetched from CalDAV; local pending edits win over remote. */\n applyRemote(input: {\n accountId: string\n calendarHref: string\n href: string\n etag: string | null\n todo: {\n uid: string\n title: string\n notes: string\n due: string\n status: string\n priority: number\n completedAt: string | null\n }\n }): TaskRecord | undefined {\n const db = this.open()\n const existing =\n (db\n .prepare('SELECT * FROM tasks WHERE account_id = ? AND href = ?')\n .get(input.accountId, input.href) as Record<string, unknown> | undefined) ??\n (db\n .prepare('SELECT * FROM tasks WHERE account_id = ? AND uid = ?')\n .get(input.accountId, input.todo.uid) as Record<string, unknown> | undefined)\n const status = statusOf(input.todo.status)\n const completedAt = status === 'COMPLETED' ? (input.todo.completedAt ?? now()) : null\n if (existing !== undefined) {\n if (Number(existing.dirty) === 1) return rowToTask(existing)\n db.prepare(\n `UPDATE tasks SET calendar_href = ?, href = ?, etag = ?, title = ?, notes = ?, due = ?, status = ?, priority = ?,\n completed_at = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?`,\n ).run(\n input.calendarHref,\n input.href,\n input.etag,\n String(input.todo.title || existing.title),\n String(input.todo.notes ?? ''),\n String(input.todo.due ?? ''),\n status,\n Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),\n completedAt,\n now(),\n String(existing.id),\n )\n return this.get(String(existing.id))\n }\n const id = `task_${randomUUID()}`\n const stamp = now()\n db.prepare(\n `INSERT INTO tasks (id, account_id, calendar_href, href, etag, uid, title, notes, due, status, priority,\n completed_at, dirty, deleted, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)`,\n ).run(\n id,\n input.accountId,\n input.calendarHref,\n input.href,\n input.etag,\n input.todo.uid || `${randomUUID()}@dsh-mailharbor`,\n String(input.todo.title || '(\u65E0\u6807\u9898)'),\n String(input.todo.notes ?? ''),\n String(input.todo.due ?? ''),\n status,\n Math.max(0, Math.min(9, Number(input.todo.priority) || 0)),\n completedAt,\n stamp,\n stamp,\n )\n return this.get(id)\n }\n\n pending(accountId: string): TaskRecord[] {\n return this.open()\n .prepare('SELECT * FROM tasks WHERE account_id = ? AND dirty = 1')\n .all(accountId)\n .map((row) => rowToTask(row as Record<string, unknown>))\n .filter((task): task is TaskRecord => task !== undefined)\n }\n\n markPushed(id: string, result: { href: string; etag: string | null; uid: string }): TaskRecord | undefined {\n this.open()\n .prepare(\n 'UPDATE tasks SET href = ?, etag = ?, uid = ?, dirty = 0, deleted = 0, updated_at = ? WHERE id = ?',\n )\n .run(result.href, result.etag, result.uid, now(), id)\n return this.get(id)\n }\n\n purge(id: string): void {\n this.open().prepare('DELETE FROM tasks WHERE id = ?').run(id)\n }\n\n /** Remove local copies whose remote VTODO disappeared (not dirty, not tombstoned). */\n pruneMissingRemote(accountId: string, calendarHref: string, hrefs: string[]): number {\n const db = this.open()\n const present = new Set(hrefs)\n const rows = db\n .prepare(\n `SELECT id, href FROM tasks\n WHERE account_id = ? AND dirty = 0 AND deleted = 0 AND href IS NOT NULL\n AND (calendar_href = ? OR calendar_href IS NULL OR calendar_href = '')`,\n )\n .all(accountId, calendarHref)\n let removed = 0\n for (const row of rows) {\n if (present.has(String(row.href))) continue\n db.prepare('DELETE FROM tasks WHERE id = ?').run(String(row.id))\n removed += 1\n }\n return removed\n }\n}\n", "import { randomUUID } from 'node:crypto'\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { Draft, DraftAttachment } from '../shared/types.js'\n\nfunction text(value: unknown): string {\n return value === undefined || value === null ? '' : String(value)\n}\n\nfunction isAttachment(value: unknown): value is DraftAttachment {\n return (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as DraftAttachment).filename === 'string' &&\n typeof (value as DraftAttachment).data === 'string'\n )\n}\n\nfunction isDraft(value: unknown): value is Draft {\n return value !== null && typeof value === 'object' && typeof (value as Draft).id === 'string'\n}\n\nexport function draftPath(root: string): string {\n return join(root, 'drafts.json')\n}\n\n/**\n * JSON-file draft store per account. Drafts never touch the backup database,\n * and an atomic rename means a crash cannot leave a half-written file.\n */\nexport class DraftStore {\n private file: string\n private items: Draft[] = []\n private readonly accountId: string\n\n constructor(file: string, accountId: string) {\n this.file = file\n this.accountId = accountId\n this.load()\n }\n\n private load(): void {\n try {\n const parsed: unknown = JSON.parse(readFileSync(this.file, 'utf8'))\n this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : []\n } catch {\n this.items = []\n }\n }\n\n private flush(): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`\n writeFileSync(temp, `${JSON.stringify(this.items, null, 2)}\\n`, { encoding: 'utf8', mode: 0o600 })\n renameSync(temp, this.file)\n }\n\n list(): Draft[] {\n return [...this.items]\n .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n .map((draft) => ({ ...draft }))\n }\n\n get(id: string): Draft | undefined {\n const found = this.items.find((draft) => draft.id === id)\n return found === undefined ? undefined : { ...found }\n }\n\n save(input: Partial<Draft> & { id?: string }): Draft {\n const now = new Date().toISOString()\n const existing = input.id === undefined ? undefined : this.items.find((draft) => draft.id === input.id)\n const fields = {\n to: text(input.to),\n cc: text(input.cc),\n bcc: text(input.bcc),\n subject: text(input.subject),\n html: text(input.html),\n attachments: Array.isArray(input.attachments)\n ? input.attachments.filter(isAttachment).map((file) => ({\n filename: text(file.filename).slice(0, 200),\n contentType: text(file.contentType).slice(0, 160),\n data: text(file.data),\n }))\n : [],\n inReplyTo: text(input.inReplyTo),\n references: text(input.references),\n }\n if (existing !== undefined) {\n Object.assign(existing, fields, { updatedAt: now })\n this.flush()\n return { ...existing }\n }\n const draft: Draft = {\n id: randomUUID(),\n accountId: this.accountId,\n ...fields,\n updatedAt: now,\n }\n this.items.push(draft)\n this.flush()\n return { ...draft }\n }\n\n remove(id: string): boolean {\n const index = this.items.findIndex((draft) => draft.id === id)\n if (index < 0) return false\n this.items.splice(index, 1)\n this.flush()\n return true\n }\n\n location(): string {\n return this.file\n }\n}\n", "import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/**\n * Local read state for archived mail.\n *\n * The backup database stores no IMAP flags, and most archived messages no\n * longer exist on the server. This store keeps only the keys the user marked\n * unread; every other message is treated as read. Server mail uses its own\n * `\\Seen` flag and is mirrored here by message id when the two overlap.\n */\nexport class ReadStateStore {\n private readonly file: string\n private keys: Set<string> | null = null\n\n constructor(file: string) {\n this.file = file\n }\n\n private load(): Set<string> {\n if (this.keys !== null) return this.keys\n try {\n const parsed: unknown = JSON.parse(readFileSync(this.file, 'utf8'))\n const list = Array.isArray(parsed)\n ? parsed\n : Array.isArray((parsed as { unread?: unknown })?.unread)\n ? (parsed as { unread: unknown[] }).unread\n : []\n this.keys = new Set(list.filter((key): key is string => typeof key === 'string' && key !== ''))\n } catch {\n this.keys = new Set()\n }\n return this.keys\n }\n\n private flush(): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const temp = `${this.file}.${String(process.pid)}.tmp`\n writeFileSync(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}\\n`, {\n encoding: 'utf8',\n mode: 0o600,\n })\n renameSync(temp, this.file)\n }\n\n isUnread(key: string): boolean {\n return key !== '' && this.load().has(key)\n }\n\n setUnread(key: string, unread: boolean): boolean {\n const normalized = key.trim()\n if (normalized === '') return false\n const keys = this.load()\n const has = keys.has(normalized)\n if (unread && !has) {\n keys.add(normalized)\n this.flush()\n return true\n }\n if (!unread && has) {\n keys.delete(normalized)\n this.flush()\n return true\n }\n return false\n }\n\n sync(entries: { key: string; unread: boolean }[]): void {\n let changed = false\n const keys = this.load()\n for (const entry of entries) {\n const key = String(entry.key ?? '').trim()\n if (key === '') continue\n if (entry.unread && !keys.has(key)) {\n keys.add(key)\n changed = true\n } else if (!entry.unread && keys.has(key)) {\n keys.delete(key)\n changed = true\n }\n }\n if (changed) this.flush()\n }\n\n count(): number {\n return this.load().size\n }\n\n list(): string[] {\n return [...this.load()]\n }\n}\n", "import type { MessageDetail, MessageSummary } from '../shared/types.js'\nimport { formatAddressList } from './address.js'\nimport type { DshToolDefinition, DshTools } from './dsh.js'\nimport type { MailHarborRuntime } from './runtime.js'\nimport type { Account } from '../shared/types.js'\n\nconst MAX_BODY = 20_000\nconst MAX_LIST = 50\n\nfunction textOf(args: unknown, key: string): string {\n if (args === null || typeof args !== 'object') return ''\n const value = (args as Record<string, unknown>)[key]\n return typeof value === 'string' ? value : ''\n}\n\nfunction numberOf(args: unknown, key: string): number | undefined {\n if (args === null || typeof args !== 'object') return undefined\n const value = (args as Record<string, unknown>)[key]\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined\n}\n\nfunction clip(text: string, max = MAX_BODY): string {\n return text.length <= max ? text : `${text.slice(0, max)}\\n\u2026\uFF08\u5DF2\u622A\u65AD\uFF0C\u5171 ${text.length} \u5B57\u7B26\uFF09`\n}\n\nfunction line(value: string): string {\n return value.replace(/\\s+/g, ' ').trim()\n}\n\nfunction stamp(value: string): string {\n return value.slice(0, 19).replace('T', ' ')\n}\n\nfunction addressText(list: { name: string; address: string }[]): string {\n return formatAddressList(list)\n}\n\nfunction summaryLine(item: MessageSummary, index: number): string {\n const flags = [\n item.backedUp === false ? '\u672A\u5907\u4EFD' : item.backedUp === true ? '\u5DF2\u5907\u4EFD' : '',\n item.hasAttachments ? '\u6709\u9644\u4EF6' : '',\n item.unread ? '\u672A\u8BFB' : '',\n ].filter(Boolean)\n return `${String(index + 1)}. [id=${item.id}] ${stamp(item.date)} \u00B7 ${line(item.from.name || item.from.address)} \u00B7 ${line(item.subject)}${flags.length === 0 ? '' : ` \u00B7 ${flags.join(' \u00B7 ')}`}\\n ${item.snippet}`\n}\n\nfunction detailText(message: MessageDetail): string {\n const head = [\n `${message.backedUp === true ? '\u5DF2\u5907\u4EFD' : '\u670D\u52A1\u5668\u5B9E\u65F6'}`,\n `\u4E3B\u9898: ${line(message.subject)}`,\n `\u53D1\u4EF6\u4EBA: ${line(message.from.name)} <${message.from.address}>`,\n `\u6536\u4EF6\u4EBA: ${line(addressText(message.to))}`,\n ...(message.cc.length === 0 ? [] : [`\u6284\u9001: ${line(addressText(message.cc))}`]),\n `\u65F6\u95F4: ${stamp(message.date)}`,\n ...(message.attachments.length === 0\n ? []\n : [\n `\u9644\u4EF6: ${message.attachments.map((file) => `${file.filename}(${String(file.size)} B)`).join('\u3001')}`,\n ]),\n '',\n clip(message.text !== '' ? message.text : '(\u65E0\u6B63\u6587)'),\n ]\n return head.join('\\n')\n}\n\nfunction plainSchema(properties: Record<string, unknown>, required: string[] = []): Record<string, unknown> {\n return { type: 'object', properties, required, additionalProperties: false }\n}\n\nfunction textResult(\n _label: string,\n schema: Record<string, unknown> = { type: 'string' },\n): DshToolDefinition['output'] {\n return {\n schema,\n render: (_args, value) => [\n { type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) },\n ],\n }\n}\n\nfunction tool(\n name: string,\n description: string,\n parameters: Record<string, unknown>,\n output: DshToolDefinition['output'],\n execute: (args: unknown) => Promise<unknown>,\n): DshToolDefinition {\n return {\n name,\n description,\n parameters,\n output,\n timeoutMs: 120_000,\n isConcurrencySafe: () => true,\n presentCall: (args) => ({ card: 'generic', title: name, kind: 'read', rawInput: args }),\n execute: async (args: unknown) => execute(args),\n }\n}\n\n/**\n * Register the `mail_*` tools. The model can read mail, search the local\n * backup, and manage drafts; sending stays a user action in the panel, so no\n * tool can put a message on the wire without the user pressing send.\n */\nexport function registerMailTools(tools: DshTools, runtime: MailHarborRuntime): (() => void)[] {\n const activeAccount = (): Account => {\n const account = runtime.active()\n if (account === null) throw new Error('\u8BF7\u5148\u5728 MailHarbor \u9762\u677F\u91CC\u6DFB\u52A0\u4E00\u4E2A\u90AE\u7BB1\u8D26\u53F7')\n return account\n }\n\n const definitions: DshToolDefinition[] = [\n tool(\n 'mail_status',\n '\u67E5\u770B\u90AE\u7BB1\u72B6\u6001\uFF1A\u8D26\u53F7\u3001\u662F\u5426\u5DF2\u767B\u5F55\u3001\u672C\u5730\u5907\u4EFD\u90AE\u4EF6\u6570\u3001\u4E0A\u6B21\u5907\u4EFD\u65F6\u95F4\u3002\u53EA\u8BFB\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u5185\u5BB9\u3002',\n plainSchema({}),\n textResult('\u72B6\u6001', {\n type: 'object',\n properties: {\n account: { type: 'string' },\n loggedIn: { type: 'boolean' },\n localTotal: { type: 'number' },\n lastBackup: { type: 'string' },\n backupRunning: { type: 'boolean' },\n },\n required: ['account', 'loggedIn', 'localTotal'],\n }),\n async () => {\n const account = activeAccount()\n const status = runtime.database(account).status()\n const backup = runtime.backupStatus(account)\n return {\n account: account.email,\n loggedIn: runtime.hasCredentials(account),\n localTotal: status.total,\n lastBackup: backup.lastSuccessAt ?? '',\n backupRunning: backup.running,\n }\n },\n ),\n tool(\n 'mail_list',\n '\u5217\u51FA\u672C\u5730\u5907\u4EFD\u6216\u670D\u52A1\u5668\u4E0A\u7684\u90AE\u4EF6\u6458\u8981\u3002source=local \u8BFB\u672C\u5730\u5907\u4EFD\uFF08\u5FEB\u3001\u53EF\u641C\u7D22\uFF09\uFF0Csource=server \u8BFB\u670D\u52A1\u5668\u5B9E\u65F6\u3002',\n plainSchema({\n source: { type: 'string', enum: ['local', 'server'], description: '\u9ED8\u8BA4 local' },\n mailbox: { type: 'string', description: '\u90AE\u7BB1\u6587\u4EF6\u5939\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 all\uFF08\u672C\u5730\uFF09\u6216 INBOX\uFF08\u670D\u52A1\u5668\uFF09' },\n query: { type: 'string', description: '\u5173\u952E\u8BCD\uFF0C\u4EC5\u672C\u5730\u641C\u7D22' },\n limit: { type: 'number', description: '\u6700\u591A\u8FD4\u56DE\u591A\u5C11\u5C01\uFF0C\u9ED8\u8BA4 20\uFF0C\u6700\u5927 50' },\n }),\n textResult('\u90AE\u4EF6\u5217\u8868', {\n type: 'object',\n properties: {\n source: { type: 'string' },\n total: { type: 'number' },\n items: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n subject: { type: 'string' },\n from: { type: 'string' },\n date: { type: 'string' },\n unread: { type: 'boolean' },\n backedUp: { type: 'boolean' },\n },\n required: ['id', 'subject', 'from', 'date', 'unread', 'backedUp'],\n },\n },\n text: { type: 'string' },\n },\n required: ['source', 'total', 'items', 'text'],\n }),\n async (args) => {\n const account = activeAccount()\n const source = textOf(args, 'source') === 'server' ? 'server' : 'local'\n const mailbox =\n textOf(args, 'mailbox') !== '' ? textOf(args, 'mailbox') : source === 'server' ? 'INBOX' : 'all'\n const limit = Math.min(MAX_LIST, Math.max(1, numberOf(args, 'limit') ?? 20))\n const query = textOf(args, 'query')\n let items: MessageSummary[] = []\n let total = 0\n if (source === 'server') {\n const provider = runtime.provider(account)\n const result = await provider.list(mailbox, limit)\n total = result.total\n const db = runtime.database(account)\n items = result.items.map((message) => ({\n id: String(message.uid),\n accountId: account.id,\n uid: message.uid,\n messageId: message.messageId,\n subject: message.subject,\n from: message.from,\n to: message.to,\n date: message.date,\n unread: !message.read,\n flagged: false,\n hasAttachments: false,\n snippet: '',\n mailbox,\n backedUp: message.messageId !== '' && db.hasMessageId(message.messageId),\n }))\n } else {\n const page = runtime.database(account).list({\n mailbox,\n query,\n limit,\n offset: 0,\n })\n total = page.total\n const readState = runtime.readState(account)\n items = page.items.map((item) => ({\n ...item,\n accountId: account.id,\n unread:\n item.messageId !== undefined && item.messageId !== ''\n ? readState.isUnread(`mid:${item.messageId}`)\n : readState.isUnread(`local:${item.id}`),\n backedUp: true,\n }))\n }\n const head = `${source === 'server' ? '\u670D\u52A1\u5668\u5B9E\u65F6' : '\u672C\u5730\u5907\u4EFD'} \u00B7 ${mailbox} \u00B7 ${String(items.length)}/${String(total)} \u5C01`\n return {\n source,\n total,\n items: items.map((item) => ({\n id: item.id,\n subject: item.subject,\n from: item.from.name || item.from.address,\n date: stamp(item.date),\n unread: item.unread,\n backedUp: item.backedUp ?? true,\n })),\n text:\n items.length === 0 ? `${head}\\n\uFF08\u6CA1\u6709\u90AE\u4EF6\uFF09` : `${head}\\n\\n${items.map(summaryLine).join('\\n')}`,\n }\n },\n ),\n tool(\n 'mail_read',\n '\u8BFB\u53D6\u4E00\u5C01\u90AE\u4EF6\u7684\u5168\u6587\u3002\u672C\u5730\u5907\u4EFD\u7528 id\uFF1B\u670D\u52A1\u5668\u90AE\u4EF6\u7528 uid \u5E76\u4F20 source=server\u3001mailbox\u3002',\n plainSchema(\n {\n id: { type: 'string', description: '\u672C\u5730\u90AE\u4EF6 id \u6216\u670D\u52A1\u5668 uid' },\n source: { type: 'string', enum: ['local', 'server'], description: '\u9ED8\u8BA4 local' },\n mailbox: { type: 'string', description: '\u670D\u52A1\u5668\u6587\u4EF6\u5939\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 INBOX' },\n },\n ['id'],\n ),\n textResult('\u90AE\u4EF6\u5168\u6587', {\n type: 'object',\n properties: {\n subject: { type: 'string' },\n from: { type: 'string' },\n date: { type: 'string' },\n text: { type: 'string' },\n attachments: { type: 'array', items: { type: 'object' } },\n },\n required: ['subject', 'from', 'date', 'text', 'attachments'],\n }),\n async (args) => {\n const account = activeAccount()\n const id = textOf(args, 'id')\n if (id === '') throw new Error('\u7F3A\u5C11\u90AE\u4EF6 id')\n const source = textOf(args, 'source') === 'server' ? 'server' : 'local'\n if (source === 'server') {\n const mailbox = textOf(args, 'mailbox') !== '' ? textOf(args, 'mailbox') : 'INBOX'\n const message = await runtime.provider(account).read(mailbox, Number(id))\n const detail = {\n ...message,\n id: String(message.uid),\n accountId: account.id,\n cc: message.cc,\n replyTo: [],\n unread: !message.read,\n flagged: false,\n hasAttachments: message.attachments.length > 0,\n snippet: '',\n mailbox,\n backedUp: message.messageId !== '' && runtime.database(account).hasMessageId(message.messageId),\n } as MessageDetail\n return {\n subject: detail.subject,\n from: `${detail.from.name} <${detail.from.address}>`.trim(),\n date: stamp(detail.date),\n text: clip(detail.text),\n attachments: detail.attachments.map((file) => ({\n filename: file.filename,\n contentType: file.contentType,\n size: file.size,\n })),\n }\n }\n const detail = runtime.database(account).get(Number(id))\n if (detail === undefined) throw new Error('\u90AE\u4EF6\u4E0D\u5B58\u5728')\n return {\n subject: detail.subject,\n from: `${detail.from.name} <${detail.from.address}>`.trim(),\n date: stamp(detail.date),\n text: clip(detail.text),\n attachments: detail.attachments.map((file) => ({\n filename: file.filename,\n contentType: file.contentType,\n size: file.size,\n })),\n }\n },\n ),\n tool(\n 'mail_search',\n '\u5728\u672C\u5730\u5907\u4EFD\u4E2D\u5168\u6587\u641C\u7D22\uFF08\u4E3B\u9898\u3001\u53D1\u4EF6\u4EBA\u3001\u6536\u4EF6\u4EBA\u3001\u6B63\u6587\u3001\u9644\u4EF6\u540D\uFF09\u3002\u53EA\u8BFB\u3002',\n plainSchema(\n {\n query: { type: 'string', description: '\u641C\u7D22\u5173\u952E\u8BCD' },\n limit: { type: 'number', description: '\u6700\u591A\u8FD4\u56DE\u591A\u5C11\u5C01\uFF0C\u9ED8\u8BA4 20' },\n },\n ['query'],\n ),\n textResult('\u641C\u7D22\u7ED3\u679C'),\n async (args) => {\n const account = activeAccount()\n const query = textOf(args, 'query')\n if (query === '') throw new Error('\u7F3A\u5C11\u641C\u7D22\u5173\u952E\u8BCD')\n const limit = Math.min(MAX_LIST, Math.max(1, numberOf(args, 'limit') ?? 20))\n const page = runtime.database(account).list({ mailbox: 'all', query, limit, offset: 0 })\n const readState = runtime.readState(account)\n return page.items.length === 0\n ? `\u6CA1\u6709\u5339\u914D\u300C${query}\u300D\u7684\u90AE\u4EF6`\n : `\u641C\u7D22\u300C${query}\u300D\uFF1A${String(page.total)} \u5C01\u4E2D\u524D ${String(page.items.length)} \u5C01\\n\\n${page.items\n .map((item, index) =>\n summaryLine(\n {\n ...item,\n accountId: account.id,\n unread: readState.isUnread(`local:${item.id}`),\n backedUp: true,\n },\n index,\n ),\n )\n .join('\\n')}`\n },\n ),\n tool(\n 'mail_drafts',\n '\u5217\u51FA\u5F53\u524D\u8D26\u53F7\u7684\u8349\u7A3F\u3002\u53EA\u8BFB\uFF0C\u4E0D\u4F1A\u53D1\u9001\u3002',\n plainSchema({}),\n textResult('\u8349\u7A3F\u5217\u8868', {\n type: 'object',\n properties: { drafts: { type: 'array', items: { type: 'object' } }, text: { type: 'string' } },\n required: ['drafts', 'text'],\n }),\n async () => {\n const account = activeAccount()\n const drafts = runtime\n .drafts(account)\n .list()\n .map((draft) => ({\n id: draft.id,\n subject: draft.subject,\n to: draft.to,\n updatedAt: draft.updatedAt,\n }))\n return {\n drafts,\n text:\n drafts.length === 0\n ? '\uFF08\u6CA1\u6709\u8349\u7A3F\uFF09'\n : drafts\n .map(\n (draft, index) =>\n `${String(index + 1)}. [${draft.id}] ${stamp(draft.updatedAt)} \u00B7 \u2192 ${line(draft.to)} \u00B7 ${line(draft.subject)}`,\n )\n .join('\\n'),\n }\n },\n ),\n tool(\n 'mail_draft',\n '\u65B0\u5EFA\u6216\u66F4\u65B0\u4E00\u5C01\u8349\u7A3F\u3002\u53EA\u5199\u672C\u5730\u8349\u7A3F\uFF0C\u4E0D\u4F1A\u53D1\u9001\uFF1B\u53D1\u9001\u4ECD\u9700\u7528\u6237\u5728\u9762\u677F\u91CC\u786E\u8BA4\u3002',\n plainSchema(\n {\n id: { type: 'string', description: '\u66F4\u65B0\u5DF2\u6709\u8349\u7A3F\u65F6\u4F20\u5165' },\n to: { type: 'string' },\n cc: { type: 'string' },\n bcc: { type: 'string' },\n subject: { type: 'string' },\n html: { type: 'string', description: '\u6B63\u6587 HTML' },\n inReplyTo: { type: 'string' },\n references: { type: 'string' },\n },\n ['subject'],\n ),\n textResult('\u8349\u7A3F', {\n type: 'object',\n properties: { id: { type: 'string' }, subject: { type: 'string' }, updatedAt: { type: 'string' } },\n required: ['id', 'subject', 'updatedAt'],\n }),\n async (args) => {\n const account = activeAccount()\n const draft = runtime.drafts(account).save({\n ...(textOf(args, 'id') !== '' ? { id: textOf(args, 'id') } : {}),\n to: textOf(args, 'to'),\n cc: textOf(args, 'cc'),\n bcc: textOf(args, 'bcc'),\n subject: textOf(args, 'subject'),\n html: textOf(args, 'html'),\n inReplyTo: textOf(args, 'inReplyTo'),\n references: textOf(args, 'references'),\n })\n return { id: draft.id, subject: draft.subject, updatedAt: draft.updatedAt }\n },\n ),\n tool(\n 'mail_backup',\n '\u89E6\u53D1\u4E00\u6B21\u589E\u91CF\u5907\u4EFD\uFF08\u540E\u53F0\u6267\u884C\uFF09\u3002\u5907\u4EFD\u4F1A\u8BFB\u53D6\u670D\u52A1\u5668\u4E0A\u7684\u5168\u90E8\u90AE\u4EF6\uFF0C\u53EF\u80FD\u6301\u7EED\u51E0\u5206\u949F\uFF1B\u53EA\u5728\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\u8C03\u7528\u3002',\n plainSchema({}),\n textResult('\u5907\u4EFD\u72B6\u6001', {\n type: 'object',\n properties: { started: { type: 'boolean' }, message: { type: 'string' } },\n required: ['started', 'message'],\n }),\n async () => {\n const account = activeAccount()\n try {\n runtime.startBackup(account)\n return { started: true, message: '\u5907\u4EFD\u5DF2\u5728\u540E\u53F0\u5F00\u59CB\uFF0C\u8FDB\u5EA6\u53EF\u5728\u9762\u677F\u7684\u5907\u4EFD\u5F39\u7A97\u67E5\u770B' }\n } catch (error) {\n return { started: false, message: error instanceof Error ? error.message : String(error) }\n }\n },\n ),\n ]\n\n return definitions.map((definition) => tools.register(definition))\n}\n\nexport { detailText }\n", "import { createApiHandler } from './api.js'\nimport type { DshContext, DshLlm, DshWorkspaceRegistry } from './dsh.js'\nimport { MailHarborRuntime } from './runtime.js'\nimport { registerMailTools } from './tools.js'\n\n/** Loader row name shown by the DSH plugin manager. */\nexport const name = 'mailharbor'\n/** Host services this plugin needs: the HTTP carrier and the tool registry. */\nexport const inject = ['webServer', 'tools']\nexport { VERSION } from './version.js'\n\nconst ROUTE = '/mail-plugin'\n\nfunction optionalService<T>(ctx: DshContext, name: string): T | undefined {\n try {\n const direct = (ctx as unknown as Record<string, unknown>)[name]\n if (direct !== undefined && direct !== null) return direct as T\n } catch {\n // Guarded service access throws when the service is not injected.\n }\n try {\n return (ctx.get?.(name) as T | undefined) ?? undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * MailHarbor Host half: one HTTP prefix for the panel, plus `mail_*` agent\n * tools when the composition provides the tool registry.\n */\nexport function apply(ctx: DshContext): void {\n let registry: DshWorkspaceRegistry | undefined\n // The registry is optional: wait for it in the background instead of\n // blocking the whole plugin on compositions that do not mount workspaces.\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (scope: DshContext) => {\n try {\n registry = scope.workspaceRegistry\n } catch {\n registry = undefined\n }\n })\n }\n\n const runtime = new MailHarborRuntime({\n workspaceRegistry: () => registry,\n sessions: optionalService(ctx, 'sessions'),\n llm: () => {\n if (typeof ctx.get !== 'function') return undefined\n try {\n return ctx.get.call(ctx, 'llm') as DshLlm | undefined\n } catch {\n return undefined\n }\n },\n })\n const handler = createApiHandler(runtime)\n const disposers: (() => void)[] = []\n\n const start = (): void => {\n disposers.push(ctx.webServer.register({ kind: 'prefix', path: ROUTE, handler }))\n // `tools` is declared in `inject`; Cordis guarantees it is present here.\n if (ctx.tools !== undefined) disposers.push(...registerMailTools(ctx.tools, runtime))\n }\n\n const stop = (): void => {\n for (const dispose of disposers.splice(0)) {\n try {\n dispose()\n } catch {\n // A disposer that already ran must not block the others.\n }\n }\n void runtime.dispose()\n }\n\n if (typeof ctx.effect === 'function') {\n ctx.effect(() => {\n start()\n return stop\n }, 'mailharbor: routes and tools')\n } else {\n start()\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["text", "name", "ImapFlow", "simpleParser", "normalizeId", "messageOf", "randomUUID", "homedir", "join", "text", "join", "homedir", "text", "require", "createHash", "existsSync", "mkdirSync", "writeFileSync", "join", "DatabaseSync", "existsSync", "readFileSync", "text", "bodyText", "createHash", "DatabaseSync", "join", "mkdirSync", "existsSync", "writeFileSync", "text", "messageOf", "existsSync", "mkdirSync", "readFileSync", "renameSync", "writeFileSync", "join", "text", "line", "randomUUID", "existsSync", "mkdirSync", "readFileSync", "renameSync", "writeFileSync", "join", "name", "text", "stamp", "name", "file", "account", "card", "randomUUID", "text", "ImapProvider", "sendMail", "bodyText", "join", "text", "existsSync", "mkdirSync", "readFileSync", "renameSync", "writeFileSync", "dirname", "text", "now", "randomUUID", "mkdirSync", "dirname", "DatabaseSync", "text", "stamp", "randomUUID", "mkdirSync", "readFileSync", "renameSync", "writeFileSync", "dirname", "join", "now", "draft", "mkdirSync", "readFileSync", "renameSync", "writeFileSync", "dirname", "join", "stamp", "clip", "text", "stamp", "stamp", "name", "activeAccount", "stamp", "detail", "clip", "name"]
|
|
7
7
|
}
|