From 796a5e4ac5b71bd4ce14638d5a35abc3db656eaf Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Thu, 30 Apr 2026 15:33:06 +0800 Subject: [PATCH] feat: improve address credential connections --- CHANGELOG.md | 5 + CHANGELOG_EN.md | 5 + e2e/fixtures/test-helpers.ts | 10 +- e2e/tests/api/address-password.spec.ts | 118 ++++++- e2e/tests/api/login-endpoints.spec.ts | 14 +- frontend/src/api/index.js | 2 + .../src/components/AddressCredentialModal.vue | 322 ++++++++++++++++++ frontend/src/i18n/locales/source/de.ts | 29 +- frontend/src/i18n/locales/source/es.ts | 29 +- frontend/src/i18n/locales/source/ja.ts | 29 +- frontend/src/i18n/locales/source/ptBR.ts | 29 +- frontend/src/i18n/message-registry.ts | 114 ++++++- frontend/src/router/index.js | 16 +- frontend/src/store/index.js | 13 + frontend/src/views/Admin.vue | 2 +- frontend/src/views/Header.vue | 2 +- frontend/src/views/admin/Account.vue | 36 +- frontend/src/views/admin/CreateAccount.vue | 28 +- frontend/src/views/admin/UserManagement.vue | 6 +- frontend/src/views/common/Login.vue | 3 +- frontend/src/views/index/AccountSettings.vue | 16 +- frontend/src/views/index/AddressBar.vue | 28 +- frontend/src/views/user/UserLogin.vue | 9 +- vitepress-docs/docs/en/guide/worker-vars.md | 13 + vitepress-docs/docs/zh/guide/worker-vars.md | 13 + worker/src/admin_api/address_api.ts | 10 +- worker/src/commom_api.ts | 18 + worker/src/common.ts | 21 +- worker/src/mails_api/address_auth.ts | 4 +- worker/src/types.d.ts | 15 + worker/src/user_api/bind_address.ts | 4 +- worker/wrangler.toml.template | 9 + 32 files changed, 854 insertions(+), 118 deletions(-) create mode 100644 frontend/src/components/AddressCredentialModal.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abb7fe5..ab16f7fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,13 @@ ### Features +- feat: |Frontend| 将邮箱地址凭证弹窗升级为“地址凭证与连接方式”,复用普通用户与 admin 创建邮箱结果弹窗;支持通过 `ENABLE_AGENT_EMAIL_INFO` 展示 AI Agent 接入信息,并通过 `SMTP_IMAP_PROXY_CONFIG` 展示 SMTP/IMAP 客户端连接信息 + ### Bug Fixes +- fix: |Admin| 管理员重置邮箱地址密码时改为前端 SHA-256 后提交,后端只接受并存储哈希值,避免该接口继续接收明文密码 +- fix: |Address| 管理员邮箱地址列表与用户绑定地址列表不再返回已存储的地址密码哈希值,避免列表接口暴露敏感字段 + ### Improvements ## v1.8.0 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 287f2257..dcac7f58 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -10,8 +10,13 @@ ### Features +- feat: |Frontend| Upgrade the address credential dialog to "Address Credentials & Connection Methods" and reuse it for both normal users and admin-created addresses; support showing AI Agent access via `ENABLE_AGENT_EMAIL_INFO` and SMTP/IMAP client settings via `SMTP_IMAP_PROXY_CONFIG` + ### Bug Fixes +- fix: |Admin| Hash address passwords in the frontend before admin reset requests, and make the backend accept and store only the hash instead of plaintext +- fix: |Address| Stop returning stored address password hashes from the admin address list and user bound-address list APIs to avoid exposing sensitive fields + ### Improvements ## v1.8.0 diff --git a/e2e/fixtures/test-helpers.ts b/e2e/fixtures/test-helpers.ts index 175928b2..d6fb53f8 100644 --- a/e2e/fixtures/test-helpers.ts +++ b/e2e/fixtures/test-helpers.ts @@ -1,4 +1,5 @@ -import { APIRequestContext } from '@playwright/test'; +import type { APIRequestContext } from '@playwright/test'; +import { createHash } from 'crypto'; import WebSocket from 'ws'; export const WORKER_URL = process.env.WORKER_URL!; @@ -10,6 +11,13 @@ export const FRONTEND_URL = process.env.FRONTEND_URL!; export const MAILPIT_API = process.env.MAILPIT_API!; export const TEST_DOMAIN = 'test.example.com'; +/** + * SHA-256 hash matching the frontend hashPassword utility. + */ +export function hashPassword(password: string): string { + return createHash('sha256').update(password).digest('hex'); +} + /** * Create a new email address via the worker API. * Appends a timestamp suffix to avoid UNIQUE constraint collisions diff --git a/e2e/tests/api/address-password.spec.ts b/e2e/tests/api/address-password.spec.ts index db3fc4ba..83fb171f 100644 --- a/e2e/tests/api/address-password.spec.ts +++ b/e2e/tests/api/address-password.spec.ts @@ -1,15 +1,16 @@ import { test, expect } from '@playwright/test'; -import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers'; +import { WORKER_URL, createTestAddress, deleteAddress, hashPassword } from '../../fixtures/test-helpers'; test.describe('Address Password Login', () => { test('set password then login with it', async ({ request }) => { const { jwt, address } = await createTestAddress(request, 'pwd-login'); + const passwordHash = hashPassword('test-password-123'); try { // Set a password on the address const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, { headers: { Authorization: `Bearer ${jwt}` }, - data: { new_password: 'test-password-123' }, + data: { new_password: passwordHash }, }); expect(changePwdRes.ok()).toBe(true); const changePwdBody = await changePwdRes.json(); @@ -17,7 +18,7 @@ test.describe('Address Password Login', () => { // Login with the correct password const loginRes = await request.post(`${WORKER_URL}/api/address_login`, { - data: { email: address, password: 'test-password-123' }, + data: { email: address, password: passwordHash }, }); expect(loginRes.ok()).toBe(true); const loginBody = await loginRes.json(); @@ -36,12 +37,13 @@ test.describe('Address Password Login', () => { test('login with wrong password returns 401', async ({ request }) => { const { jwt, address } = await createTestAddress(request, 'pwd-wrong'); + const passwordHash = hashPassword('correct-password'); try { // Set a password const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, { headers: { Authorization: `Bearer ${jwt}` }, - data: { new_password: 'correct-password' }, + data: { new_password: passwordHash }, }); expect(changePwdRes.ok()).toBe(true); const changePwdBody = await changePwdRes.json(); @@ -49,11 +51,117 @@ test.describe('Address Password Login', () => { // Login with wrong password const loginRes = await request.post(`${WORKER_URL}/api/address_login`, { - data: { email: address, password: 'wrong-password' }, + data: { email: address, password: hashPassword('wrong-password') }, }); expect(loginRes.status()).toBe(401); } finally { await deleteAddress(request, jwt); } }); + + test('admin reset stores frontend-hashed address password', async ({ request }) => { + const { jwt, address, address_id } = await createTestAddress(request, 'pwd-admin-reset'); + const plainPassword = `admin-reset-${Date.now()}`; + const passwordHash = hashPassword(plainPassword); + + try { + const resetRes = await request.post(`${WORKER_URL}/admin/address/${address_id}/reset_password`, { + data: { password: passwordHash }, + }); + expect(resetRes.ok()).toBe(true); + await expect(resetRes.json()).resolves.toMatchObject({ success: true }); + + const plaintextLoginRes = await request.post(`${WORKER_URL}/api/address_login`, { + data: { email: address, password: plainPassword }, + }); + expect(plaintextLoginRes.status()).toBe(401); + + const loginRes = await request.post(`${WORKER_URL}/api/address_login`, { + data: { email: address, password: passwordHash }, + }); + expect(loginRes.ok()).toBe(true); + const loginBody = await loginRes.json(); + expect(loginBody.jwt).toBeTruthy(); + expect(loginBody.address).toBe(address); + } finally { + await deleteAddress(request, jwt); + } + }); + + test('admin address list does not expose stored password hash', async ({ request }) => { + const { jwt, address } = await createTestAddress(request, 'pwd-list-hidden'); + const passwordHash = hashPassword('list-hidden-password'); + + try { + const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, { + headers: { Authorization: `Bearer ${jwt}` }, + data: { new_password: passwordHash }, + }); + expect(changePwdRes.ok()).toBe(true); + + const listRes = await request.get( + `${WORKER_URL}/admin/address?limit=10&offset=0&query=${encodeURIComponent(address)}` + ); + expect(listRes.ok()).toBe(true); + const listBody = await listRes.json(); + const listedAddress = listBody.results.find((row: { name: string }) => row.name === address); + expect(listedAddress).toBeTruthy(); + expect(listedAddress).not.toHaveProperty('password'); + } finally { + await deleteAddress(request, jwt); + } + }); + + test('user bind address list does not expose stored password hash', async ({ request }) => { + const userEmail = `pwd-bind-hidden-${Date.now()}@test.example.com`; + const userPasswordHash = hashPassword('bind-hidden-user-password'); + const { jwt, address } = await createTestAddress(request, 'pwd-bind-hidden'); + const addressPasswordHash = hashPassword('bind-hidden-address-password'); + + try { + const enableRes = await request.post(`${WORKER_URL}/admin/user_settings`, { + data: { + enable: true, + enableMailVerify: false, + }, + }); + expect(enableRes.ok()).toBe(true); + + const registerRes = await request.post(`${WORKER_URL}/user_api/register`, { + data: { email: userEmail, password: userPasswordHash }, + }); + expect(registerRes.ok()).toBe(true); + + const loginRes = await request.post(`${WORKER_URL}/user_api/login`, { + data: { email: userEmail, password: userPasswordHash }, + }); + expect(loginRes.ok()).toBe(true); + const { jwt: userJwt } = await loginRes.json(); + + const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, { + headers: { Authorization: `Bearer ${jwt}` }, + data: { new_password: addressPasswordHash }, + }); + expect(changePwdRes.ok()).toBe(true); + + const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, { + headers: { + Authorization: `Bearer ${jwt}`, + 'x-user-token': userJwt, + }, + }); + expect(bindRes.ok()).toBe(true); + + const listRes = await request.get(`${WORKER_URL}/user_api/bind_address`, { + headers: { 'x-user-token': userJwt }, + }); + expect(listRes.ok()).toBe(true); + const listBody = await listRes.json(); + const listedAddress = listBody.results.find((row: { name: string }) => row.name === address); + expect(listedAddress).toBeTruthy(); + expect(listedAddress).not.toHaveProperty('password'); + } finally { + await deleteAddress(request, jwt); + } + }); }); diff --git a/e2e/tests/api/login-endpoints.spec.ts b/e2e/tests/api/login-endpoints.spec.ts index 1d4a2450..a6051947 100644 --- a/e2e/tests/api/login-endpoints.spec.ts +++ b/e2e/tests/api/login-endpoints.spec.ts @@ -1,13 +1,5 @@ import { test, expect } from '@playwright/test'; -import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers'; -import * as crypto from 'crypto'; - -/** - * SHA-256 hash matching frontend hashPassword utility. - */ -function hashPassword(password: string): string { - return crypto.createHash('sha256').update(password).digest('hex'); -} +import { WORKER_URL, createTestAddress, deleteAddress, hashPassword } from '../../fixtures/test-helpers'; test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled)', () => { @@ -110,14 +102,14 @@ test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled // Set a password await request.post(`${WORKER_URL}/api/address_change_password`, { headers: { Authorization: `Bearer ${jwt}` }, - data: { new_password: 'addr-pass-123' }, + data: { new_password: hashPassword('addr-pass-123') }, }); // Login with cf_token field present but empty const loginRes = await request.post(`${WORKER_URL}/api/address_login`, { data: { email: address, - password: 'addr-pass-123', + password: hashPassword('addr-pass-123'), cf_token: '' }, }); diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 76f7bf09..c6a67e59 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -105,6 +105,8 @@ const getOpenSettings = async (message, notification) => { enableWebhook: res["enableWebhook"] || false, isS3Enabled: res["isS3Enabled"] || false, enableAddressPassword: res["enableAddressPassword"] || false, + enableAgentEmailInfo: res["enableAgentEmailInfo"] || false, + smtpImapProxyConfig: res["smtpImapProxyConfig"] || openSettings.value.smtpImapProxyConfig, statusUrl: res["statusUrl"] || "", enableGlobalTurnstileCheck: res["enableGlobalTurnstileCheck"] || false, }); diff --git a/frontend/src/components/AddressCredentialModal.vue b/frontend/src/components/AddressCredentialModal.vue new file mode 100644 index 00000000..a752c212 --- /dev/null +++ b/frontend/src/components/AddressCredentialModal.vue @@ -0,0 +1,322 @@ + + + + + diff --git a/frontend/src/i18n/locales/source/de.ts b/frontend/src/i18n/locales/source/de.ts index bf518f42..4bac8a46 100644 --- a/frontend/src/i18n/locales/source/de.ts +++ b/frontend/src/i18n/locales/source/de.ts @@ -586,5 +586,32 @@ export const deMessages = { "views.admin.AccountSettings.tip": "Die folgenden Mehrfachauswahlwerte können manuell eingegeben und mit Enter hinzugefügt werden", "components.MailBox.emptyInbox": "Dein Posteingang ist leer", "views.index.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden", - "views.admin.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden" + "views.admin.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden", + "components.AddressCredentialModal.addressCredential": "Adresszugangsdaten", + "components.AddressCredentialModal.addressCredentialLabel": "Address JWT", + "components.AddressCredentialModal.addressPassword": "Adresspasswort", + "components.AddressCredentialModal.agentAccess": "AI Agent", + "components.AddressCredentialModal.agentAccessTip": "Verwende dieses Postfach in einem AI Agent mit dem Address JWT und den parsed-mail APIs.", + "components.AddressCredentialModal.agentConfig": "Agent-Konfiguration", + "components.AddressCredentialModal.agentSkill": "Agent skill", + "components.AddressCredentialModal.apiBase": "API-Basisadresse", + "components.AddressCredentialModal.autoLoginLink": "Auto-Login-Link", + "components.AddressCredentialModal.copyFailed": "Kopieren fehlgeschlagen", + "components.AddressCredentialModal.copySection": "Kopieren", + "components.AddressCredentialModal.copySuccess": "Kopiert", + "components.AddressCredentialModal.currentAddress": "Aktuelle Adresse", + "components.AddressCredentialModal.docs": "Dokumentation", + "components.AddressCredentialModal.imapHost": "IMAP-Host", + "components.AddressCredentialModal.imapPort": "IMAP-Port", + "components.AddressCredentialModal.password": "Passwort", + "components.AddressCredentialModal.plainOrProxyTls": "Klartext oder Proxy-TLS", + "components.AddressCredentialModal.security": "Sicherheit", + "components.AddressCredentialModal.smtpHost": "SMTP-Host", + "components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP", + "components.AddressCredentialModal.smtpImapTip": "Verwende diese Werte in Mail-Clients, nachdem der Administrator den SMTP/IMAP-Proxy konfiguriert hat. Als Passwort kannst du den hier angezeigten Address JWT oder ein vorhandenes Adresspasswort verwenden.", + "components.AddressCredentialModal.smtpPort": "SMTP-Port", + "components.AddressCredentialModal.starttls": "STARTTLS", + "components.AddressCredentialModal.tip": "Verwende diese Zugangsdaten nur mit Clients und Agents, denen du vertraust.", + "components.AddressCredentialModal.title": "Adresszugangsdaten & Verbindungsmethoden", + "components.AddressCredentialModal.username": "Benutzername" } diff --git a/frontend/src/i18n/locales/source/es.ts b/frontend/src/i18n/locales/source/es.ts index 39b5a134..7dceca9b 100644 --- a/frontend/src/i18n/locales/source/es.ts +++ b/frontend/src/i18n/locales/source/es.ts @@ -586,5 +586,32 @@ export const esMessages = { "views.admin.AccountSettings.tip": "Puedes introducir manualmente los siguientes valores y pulsar Enter para añadirlos", "components.MailBox.emptyInbox": "Tu bandeja de entrada está vacía", "views.index.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo", - "views.admin.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo" + "views.admin.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo", + "components.AddressCredentialModal.addressCredential": "Credencial de dirección", + "components.AddressCredentialModal.addressCredentialLabel": "Address JWT", + "components.AddressCredentialModal.addressPassword": "Contraseña de la dirección", + "components.AddressCredentialModal.agentAccess": "AI Agent", + "components.AddressCredentialModal.agentAccessTip": "Usa este buzón desde un AI Agent con el Address JWT y las APIs parsed-mail.", + "components.AddressCredentialModal.agentConfig": "Configuración del agente", + "components.AddressCredentialModal.agentSkill": "Agent skill", + "components.AddressCredentialModal.apiBase": "Base de la API", + "components.AddressCredentialModal.autoLoginLink": "Enlace de inicio automático", + "components.AddressCredentialModal.copyFailed": "Error al copiar", + "components.AddressCredentialModal.copySection": "Copiar", + "components.AddressCredentialModal.copySuccess": "Copiado", + "components.AddressCredentialModal.currentAddress": "Dirección actual", + "components.AddressCredentialModal.docs": "Documentación", + "components.AddressCredentialModal.imapHost": "Host IMAP", + "components.AddressCredentialModal.imapPort": "Puerto IMAP", + "components.AddressCredentialModal.password": "Contraseña", + "components.AddressCredentialModal.plainOrProxyTls": "Texto plano o TLS del proxy", + "components.AddressCredentialModal.security": "Seguridad", + "components.AddressCredentialModal.smtpHost": "Host SMTP", + "components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP", + "components.AddressCredentialModal.smtpImapTip": "Usa estos valores en clientes de correo después de que el administrador configure el proxy SMTP/IMAP. Como contraseña puedes usar el Address JWT mostrado aquí o la contraseña de la dirección si la tienes.", + "components.AddressCredentialModal.smtpPort": "Puerto SMTP", + "components.AddressCredentialModal.starttls": "STARTTLS", + "components.AddressCredentialModal.tip": "Usa estas credenciales solo con clientes y agentes de confianza.", + "components.AddressCredentialModal.title": "Credenciales de dirección y métodos de conexión", + "components.AddressCredentialModal.username": "Usuario" } diff --git a/frontend/src/i18n/locales/source/ja.ts b/frontend/src/i18n/locales/source/ja.ts index a2846959..0e404d9c 100644 --- a/frontend/src/i18n/locales/source/ja.ts +++ b/frontend/src/i18n/locales/source/ja.ts @@ -586,5 +586,32 @@ export const jaMessages = { "views.admin.AccountSettings.tip": "以下の複数選択項目は手動入力して Enter で追加できます", "components.MailBox.emptyInbox": "受信箱は空です", "views.index.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します", - "views.admin.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します" + "views.admin.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します", + "components.AddressCredentialModal.addressCredential": "アドレス認証情報", + "components.AddressCredentialModal.addressCredentialLabel": "Address JWT", + "components.AddressCredentialModal.addressPassword": "アドレスパスワード", + "components.AddressCredentialModal.agentAccess": "AI Agent", + "components.AddressCredentialModal.agentAccessTip": "AI Agent から Address JWT と parsed-mail API を使ってこのメールボックスを利用できます。", + "components.AddressCredentialModal.agentConfig": "Agent 設定", + "components.AddressCredentialModal.agentSkill": "Agent skill", + "components.AddressCredentialModal.apiBase": "API ベース", + "components.AddressCredentialModal.autoLoginLink": "自動ログインリンク", + "components.AddressCredentialModal.copyFailed": "コピーに失敗しました", + "components.AddressCredentialModal.copySection": "コピー", + "components.AddressCredentialModal.copySuccess": "コピーしました", + "components.AddressCredentialModal.currentAddress": "現在のアドレス", + "components.AddressCredentialModal.docs": "ドキュメント", + "components.AddressCredentialModal.imapHost": "IMAP ホスト", + "components.AddressCredentialModal.imapPort": "IMAP ポート", + "components.AddressCredentialModal.password": "パスワード", + "components.AddressCredentialModal.plainOrProxyTls": "平文またはプロキシ側 TLS", + "components.AddressCredentialModal.security": "セキュリティ", + "components.AddressCredentialModal.smtpHost": "SMTP ホスト", + "components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP", + "components.AddressCredentialModal.smtpImapTip": "管理者が SMTP/IMAP プロキシを設定した後、メールクライアントでこれらの値を使用できます。パスワードにはここに表示される Address JWT、または手元にあるアドレスパスワードを使用できます。", + "components.AddressCredentialModal.smtpPort": "SMTP ポート", + "components.AddressCredentialModal.starttls": "STARTTLS", + "components.AddressCredentialModal.tip": "これらの認証情報は信頼できるクライアントと Agent でのみ使用してください。", + "components.AddressCredentialModal.title": "アドレス認証情報と接続方法", + "components.AddressCredentialModal.username": "ユーザー名" } diff --git a/frontend/src/i18n/locales/source/ptBR.ts b/frontend/src/i18n/locales/source/ptBR.ts index c9d8f00d..d99eddbf 100644 --- a/frontend/src/i18n/locales/source/ptBR.ts +++ b/frontend/src/i18n/locales/source/ptBR.ts @@ -586,5 +586,32 @@ export const ptBRMessages = { "views.admin.AccountSettings.tip": "Você pode inserir manualmente os seguintes valores e pressionar Enter para adicioná-los", "components.MailBox.emptyInbox": "Sua caixa de entrada está vazia", "views.index.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail", - "views.admin.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail" + "views.admin.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail", + "components.AddressCredentialModal.addressCredential": "Credencial do endereço", + "components.AddressCredentialModal.addressCredentialLabel": "Address JWT", + "components.AddressCredentialModal.addressPassword": "Senha do endereço", + "components.AddressCredentialModal.agentAccess": "AI Agent", + "components.AddressCredentialModal.agentAccessTip": "Use esta caixa de entrada em um AI Agent com o Address JWT e as APIs parsed-mail.", + "components.AddressCredentialModal.agentConfig": "Configuração do Agent", + "components.AddressCredentialModal.agentSkill": "Agent skill", + "components.AddressCredentialModal.apiBase": "Base da API", + "components.AddressCredentialModal.autoLoginLink": "Link de login automático", + "components.AddressCredentialModal.copyFailed": "Falha ao copiar", + "components.AddressCredentialModal.copySection": "Copiar", + "components.AddressCredentialModal.copySuccess": "Copiado", + "components.AddressCredentialModal.currentAddress": "Endereço atual", + "components.AddressCredentialModal.docs": "Documentação", + "components.AddressCredentialModal.imapHost": "Host IMAP", + "components.AddressCredentialModal.imapPort": "Porta IMAP", + "components.AddressCredentialModal.password": "Senha", + "components.AddressCredentialModal.plainOrProxyTls": "Texto puro ou TLS do proxy", + "components.AddressCredentialModal.security": "Segurança", + "components.AddressCredentialModal.smtpHost": "Host SMTP", + "components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP", + "components.AddressCredentialModal.smtpImapTip": "Use estes valores em clientes de e-mail depois que o administrador configurar o proxy SMTP/IMAP. Como senha, use o Address JWT mostrado aqui ou a senha do endereço quando você a tiver.", + "components.AddressCredentialModal.smtpPort": "Porta SMTP", + "components.AddressCredentialModal.starttls": "STARTTLS", + "components.AddressCredentialModal.tip": "Use estas credenciais somente com clientes e agents confiáveis.", + "components.AddressCredentialModal.title": "Credenciais do endereço e métodos de conexão", + "components.AddressCredentialModal.username": "Nome de usuário" } diff --git a/frontend/src/i18n/message-registry.ts b/frontend/src/i18n/message-registry.ts index 53f116d8..7306251a 100644 --- a/frontend/src/i18n/message-registry.ts +++ b/frontend/src/i18n/message-registry.ts @@ -281,6 +281,116 @@ export const MESSAGE_REGISTRY = { "zh": "用户地址" } }, + "components.AddressCredentialModal": { + "addressCredential": { + "en": "Address Credential", + "zh": "地址凭证" + }, + "addressCredentialLabel": { + "en": "Address JWT", + "zh": "Address JWT" + }, + "addressPassword": { + "en": "Address Password", + "zh": "地址密码" + }, + "agentAccess": { + "en": "AI Agent", + "zh": "AI Agent" + }, + "agentAccessTip": { + "en": "Use this mailbox from an AI agent with the Address JWT and parsed-mail APIs.", + "zh": "AI Agent 可使用 Address JWT 和 parsed-mail API 读取这个邮箱。" + }, + "agentConfig": { + "en": "Agent config", + "zh": "Agent 配置" + }, + "agentSkill": { + "en": "Agent skill", + "zh": "Agent skill" + }, + "apiBase": { + "en": "API Base", + "zh": "API 地址" + }, + "autoLoginLink": { + "en": "Auto-login link", + "zh": "自动登录链接" + }, + "copyFailed": { + "en": "Copy failed", + "zh": "复制失败" + }, + "copySection": { + "en": "Copy", + "zh": "复制" + }, + "copySuccess": { + "en": "Copied", + "zh": "已复制" + }, + "currentAddress": { + "en": "Current address", + "zh": "当前邮箱" + }, + "docs": { + "en": "Docs", + "zh": "文档" + }, + "imapHost": { + "en": "IMAP host", + "zh": "IMAP 主机" + }, + "imapPort": { + "en": "IMAP port", + "zh": "IMAP 端口" + }, + "password": { + "en": "Password", + "zh": "密码" + }, + "plainOrProxyTls": { + "en": "Plain or proxy TLS", + "zh": "明文或代理层 TLS" + }, + "security": { + "en": "Security", + "zh": "安全" + }, + "smtpHost": { + "en": "SMTP host", + "zh": "SMTP 主机" + }, + "smtpImapAccess": { + "en": "SMTP / IMAP", + "zh": "SMTP / IMAP" + }, + "smtpImapTip": { + "en": "Use these values in mail clients after the administrator configures the SMTP/IMAP proxy. The password can be the Address JWT shown here, or the address password when you have it.", + "zh": "管理员配置 SMTP/IMAP 代理后,可在邮件客户端中使用这些信息。密码可使用这里展示的 Address JWT,也可使用你持有的地址密码。" + }, + "smtpPort": { + "en": "SMTP port", + "zh": "SMTP 端口" + }, + "starttls": { + "en": "STARTTLS", + "zh": "STARTTLS" + }, + "tip": { + "en": "Use these credentials only with clients and agents you trust.", + "zh": "请只在可信的客户端和 Agent 中使用这些凭证。" + }, + "title": { + "en": "Address Credentials & Connection Methods", + "zh": "地址凭证与连接方式" + }, + "username": { + "en": "Username", + "zh": "用户名" + } + }, "views.user.UserMailBox": { "addressQueryTip": { "en": "Leave blank to query all addresses", @@ -817,8 +927,8 @@ export const MESSAGE_REGISTRY = { "zh": "密码不匹配" }, "showAddressCredential": { - "en": "Show Address Credential", - "zh": "查看邮箱地址凭证" + "en": "Credentials & Connection Methods", + "zh": "地址凭证与连接方式" }, "success": { "en": "Success", diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 80bb3852..d10d6947 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -61,8 +61,20 @@ router.beforeEach((to, from, next) => { preferredLocale.value = getPreferredLocale('', getBrowserLocales()) } - if (to.query.jwt) { - jwt.value = to.query.jwt + if (Object.prototype.hasOwnProperty.call(to.query, 'jwt')) { + const jwtQuery = Array.isArray(to.query.jwt) ? to.query.jwt[0] : to.query.jwt + if (typeof jwtQuery === 'string') { + jwt.value = jwtQuery + } + const query = { ...to.query } + delete query.jwt + next({ + path: to.path, + query, + hash: to.hash, + replace: true, + }) + return } if (routeLocale) { diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 00fd8555..f2b8d699 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -40,6 +40,19 @@ export const useGlobalState = createGlobalState( showGithub: true, disableAdminPasswordCheck: false, enableAddressPassword: false, + enableAgentEmailInfo: false, + smtpImapProxyConfig: { + smtp: { + host: '', + port: 8025, + starttls: false, + }, + imap: { + host: '', + port: 11143, + starttls: false, + }, + }, statusUrl: '', enableGlobalTurnstileCheck: false, }) diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 1213e973..829d3689 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -110,7 +110,7 @@ onMounted(async () => {

{{ t('accessTip') }}

- +