From b2263a779c674160bed74398d95400df34f79a85 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 8 Aug 2026 15:59:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(mail):=20=E9=82=AE=E7=AE=B1=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E9=87=8D=E6=9E=84=E4=B8=BA=E4=BC=81=E4=B8=9A=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E4=B8=89=E6=A0=8F=E9=A3=8E=E6=A0=BC=E5=B9=B6=E9=80=82?= =?UTF-8?q?=E9=85=8D=E6=B7=B1=E8=89=B2=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 三栏布局:左侧文件夹树(未读徽标)+ 邮件列表(多选/批量已读/删除)+ 阅读窗格(回复/转发/删除) - 新增全部邮件视图、删除移入垃圾箱、批量已读、回复/转发引用头(inReplyTo/references) - 退出邮箱吊销后端会话 token - 硬编码颜色替换为 Element Plus CSS 变量,深色下选中态改用半透明主色 --- src/utils/jmap.ts | 39 +- src/views/mail/Mailbox.vue | 800 +++++++++++++++++++++++++++++-------- 2 files changed, 673 insertions(+), 166 deletions(-) diff --git a/src/utils/jmap.ts b/src/utils/jmap.ts index d8e8a1a..700c5b8 100644 --- a/src/utils/jmap.ts +++ b/src/utils/jmap.ts @@ -91,11 +91,13 @@ export class JmapClient { return payload.list || [] } - /** 查询指定邮箱内的邮件 id(默认收件箱,按接收时间倒序) */ - async queryEmailIds(mailboxId: string, limit = 50): Promise { + /** 查询邮箱内邮件 id(mailboxId 为空时查全部;按接收时间倒序) */ + async queryEmailIds(mailboxId: string | null = null, limit = 50): Promise { + const filter: Record = {} + if (mailboxId) filter.inMailbox = mailboxId const payload = await this.call<{ ids: string[] }>('Email/query', { accountId: this.accountId, - filter: { inMailbox: mailboxId }, + filter, sort: [{ property: 'receivedAt', isAscending: false }], limit, }) @@ -132,6 +134,32 @@ export class JmapClient { }) } + /** 批量标记已读/未读 */ + async setSeenBatch(emailIds: string[], seen: boolean): Promise { + if (!emailIds.length) return + const update: Record = {} + for (const id of emailIds) { + update[id] = { keywords: seen ? { $seen: true } : { $seen: false } } + } + await this.call('Email/set', { accountId: this.accountId, update }) + } + + /** 批量删除:传入 fromMailboxId 且有垃圾箱时移入垃圾箱;否则直接销毁 */ + async deleteEmailBatch(emailIds: string[], fromMailboxId?: string): Promise { + if (!emailIds.length) return + const mailboxes = await this.getMailboxes() + const trash = mailboxes.find((m) => m.role === 'trash') + if (!fromMailboxId || !trash) { + await this.call('Email/set', { accountId: this.accountId, destroy: emailIds }) + return + } + const update: Record = {} + for (const id of emailIds) { + update[id] = { mailboxIds: { [trash.id]: true, [fromMailboxId]: false } } + } + await this.call('Email/set', { accountId: this.accountId, update }) + } + /** 获取发信身份(Identity) */ private async getIdentityId(): Promise { const payload = await this.call<{ list: Array<{ id: string }> }>('Identity/get', { accountId: this.accountId, ids: null }) @@ -148,6 +176,8 @@ export class JmapClient { subject: string textBody?: string htmlBody?: string + inReplyTo?: string[] + references?: string[] }): Promise { const mailboxes = await this.getMailboxes() const drafts = mailboxes.find((m) => m.role === 'drafts') || mailboxes[0] @@ -160,6 +190,9 @@ export class JmapClient { to: opts.to.map((t) => ({ name: t.name || '', email: t.email })), subject: opts.subject, } + // 回复/转发时携带引用头,形成会话线程 + if (opts.inReplyTo?.length) emailData.inReplyTo = opts.inReplyTo + if (opts.references?.length) emailData.references = opts.references // 正文必须用标准 JMAP bodyValues + partId 引用方式: // Stalwart v0.11 不允许在 textBody/htmlBody 数组里内联 value(报 invalidProperties "Cannot set property.") if (opts.htmlBody) { diff --git a/src/views/mail/Mailbox.vue b/src/views/mail/Mailbox.vue index fbde942..da3c981 100644 --- a/src/views/mail/Mailbox.vue +++ b/src/views/mail/Mailbox.vue @@ -1,20 +1,25 @@ @@ -291,74 +481,119 @@ async function openMailbox(): Promise { - + - - - - - - - - + + @@ -422,7 +640,7 @@ async function openMailbox(): Promise { - +