feat(mail): 邮箱登录换取会话 token,JMAP 调用不再传输明文密码

- 登录调 /API/Mail/Login 换取 token(密码仅一次传输)

- JmapClient 改持 token;登出吊销 token
This commit is contained in:
TermiNexus committed 2026-08-08 15:12:17 +08:00
1 parent 600ca27cbb
commit 86cd82024f
3 files changed
+22 -10

No files matched your search

+14 -2
View File
@@ -40,6 +40,12 @@ export interface JmapProxyResult {
} }
} }
/** 邮箱登录结果 */
export interface MailLoginResult {
Token: string
MailboxName: string
}
/** 邮箱 API 客户端 */ /** 邮箱 API 客户端 */
export const mailApi = { export const mailApi = {
/** 获取 JMAP 连接配置 */ /** 获取 JMAP 连接配置 */
@@ -55,8 +61,14 @@ export const mailApi = {
/** 修改邮箱密码 */ /** 修改邮箱密码 */
changePassword: (NewPassword: string): Promise<void> => post<void>('/API/Mail/ChangePassword', { NewPassword }), changePassword: (NewPassword: string): Promise<void> => post<void>('/API/Mail/ChangePassword', { NewPassword }),
/** JMAP 收发代理(后端统一转发,避免浏览器跨域) */ /** 邮箱登录:校验密码并换取会话 token(密码仅此一次传输,后端存内存) */
jmap: (data: { Password: string; MethodCalls: Array<[string, Record<string, unknown>, string]>; AccountId?: string }): Promise<JmapProxyResult> => login: (Password: string): Promise<MailLoginResult> => post<MailLoginResult>('/API/Mail/Login', { Password }),
/** 邮箱登出:吊销会话 token */
logout: (Token: string): Promise<void> => post<void>('/API/Mail/Logout', { Token }),
/** JMAP 收发代理(携带会话 token,不再传输明文密码) */
jmap: (data: { Token: string; MethodCalls: Array<[string, Record<string, unknown>, string]>; AccountId?: string }): Promise<JmapProxyResult> =>
post<JmapProxyResult>('/API/Mail/Jmap', data), post<JmapProxyResult>('/API/Mail/Jmap', data),
/** 管理员:全部邮箱列表 */ /** 管理员:全部邮箱列表 */
+5 -5
View File
@@ -49,12 +49,12 @@ export interface JmapCallResult {
export class JmapClient { export class JmapClient {
readonly mailboxName: string readonly mailboxName: string
readonly password: string readonly token: string
private accountId = '' private accountId = ''
constructor(mailboxName: string, password: string) { constructor(mailboxName: string, token: string) {
this.mailboxName = mailboxName this.mailboxName = mailboxName
this.password = password this.token = token
} }
/** 登录验证:通过一次 Mailbox/get 触发后端发现账户 ID */ /** 登录验证:通过一次 Mailbox/get 触发后端发现账户 ID */
@@ -62,10 +62,10 @@ export class JmapClient {
await this.request([['Mailbox/get', {}, 'c0']]) await this.request([['Mailbox/get', {}, 'c0']])
} }
/** 批量调用 JMAP 方法(经后端代理) */ /** 批量调用 JMAP 方法(经后端代理,携带会话 token,密码不落前端) */
async request(methodCalls: Array<[string, Record<string, unknown>, string]>): Promise<JmapCallResult> { async request(methodCalls: Array<[string, Record<string, unknown>, string]>): Promise<JmapCallResult> {
const result = await mailApi.jmap({ const result = await mailApi.jmap({
Password: this.password, Token: this.token,
MethodCalls: methodCalls, MethodCalls: methodCalls,
AccountId: this.accountId || undefined, AccountId: this.accountId || undefined,
}) })
+3 -3
View File
@@ -30,7 +30,6 @@ const applyForm = ref({ MailboxName: '', Password: '', ConfirmPassword: '' })
// JMAP 会话 // JMAP 会话
const client = ref<JmapClient | null>(null) const client = ref<JmapClient | null>(null)
const jmapPassword = ref('')
const loginVisible = ref(false) const loginVisible = ref(false)
const loginForm = ref({ Password: '' }) const loginForm = ref({ Password: '' })
const connecting = ref(false) const connecting = ref(false)
@@ -107,10 +106,11 @@ async function submitLogin(): Promise<void> {
} }
connecting.value = true connecting.value = true
try { try {
const c = new JmapClient(myAddress.value, loginForm.value.Password) // 后端校验密码并签发会话 token(密码仅此一次传输,后端存内存;后续 JMAP 调用只带 token)
const result = await mailApi.login(loginForm.value.Password)
const c = new JmapClient(result.MailboxName, result.Token)
await c.discover() await c.discover()
client.value = c client.value = c
jmapPassword.value = loginForm.value.Password
loginVisible.value = false loginVisible.value = false
loginForm.value.Password = '' loginForm.value.Password = ''
await loadMailboxes() await loadMailboxes()