diff --git a/CHANGELOG.md b/CHANGELOG.md index 2edb248..3aefd6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ ### Features - feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限 -- feat: |用户系统| 用户中心新增绑定邮箱选择、发送邮件和发件箱,提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API +- feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API ### Bug Fixes @@ -26,7 +26,7 @@ - test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复 - fix: |E2E| 覆盖发信页面草稿编辑、正文格式切换及 HTML 预览 -- test: |E2E| 覆盖用户 JWT 发信接口的地址归属、额度扣减、实际投递和发件箱操作,以及用户中心选择绑定邮箱后发信的完整流程 +- test: |E2E| 覆盖用户 JWT 发信接口的地址归属、额度扣减、实际投递和发件箱操作,以及用户中心查看地址凭证、切换发件地址和按地址过滤发件箱的完整流程 ## v1.11.0 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 5c15a49..0c1cccd 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -11,7 +11,7 @@ ### Features - feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit -- feat: |User| Add bound-address selection, mail composition, and sent items to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management +- feat: |User| Add mail composition, inbox-style sent-item filtering by bound address, and the shared address-credentials dialog to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management ### Bug Fixes @@ -26,7 +26,7 @@ - test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads - fix: |E2E| Cover draft editing, content-format switching, and HTML preview in the send-mail composer -- test: |E2E| Cover address ownership, balance decrement, delivery, and sent-item operations through the User JWT API, plus the complete user-center address selection and send flow +- test: |E2E| Cover address ownership, balance decrement, delivery, and sent-item operations through the User JWT API, plus user-center credential display, sender switching, and sent-item filtering by address ## v1.11.0 diff --git a/e2e/tests/api/user-send-mail.spec.ts b/e2e/tests/api/user-send-mail.spec.ts index d164f1b..7bff1fb 100644 --- a/e2e/tests/api/user-send-mail.spec.ts +++ b/e2e/tests/api/user-send-mail.spec.ts @@ -114,6 +114,38 @@ test.describe('User send mail API', () => { const delivered = await listener.message; expect(delivered.From.Address).toBe(bound.address); + const outsiderSubject = `Outsider send ${Date.now()}`; + const outsiderSendRes = await request.post(`${WORKER_URL}/api/send_mail`, { + headers: { Authorization: `Bearer ${outsider.jwt}` }, + data: { + to_mail: 'recipient@test.example.com', + subject: outsiderSubject, + content: 'This sent item must remain inaccessible to the user', + is_html: false, + }, + }); + expect(outsiderSendRes.ok()).toBe(true); + const outsiderAddressSendboxRes = await request.get( + `${WORKER_URL}/api/sendbox?limit=20&offset=0`, + { headers: { Authorization: `Bearer ${outsider.jwt}` } }, + ); + const outsiderAddressSendbox = await outsiderAddressSendboxRes.json(); + const outsiderMail = outsiderAddressSendbox.results.find((item: { raw: string }) => ( + JSON.parse(item.raw).subject === outsiderSubject + )); + expect(outsiderMail).toBeTruthy(); + + const unauthorizedDeleteRes = await request.delete( + `${WORKER_URL}/user_api/sendbox/${outsiderMail.id}`, + { headers: { 'x-user-token': user.jwt } }, + ); + expect(unauthorizedDeleteRes.ok()).toBe(true); + const outsiderSendboxAfterDeleteRes = await request.get( + `${WORKER_URL}/api/sendbox?limit=20&offset=0`, + { headers: { Authorization: `Bearer ${outsider.jwt}` } }, + ); + expect((await outsiderSendboxAfterDeleteRes.json()).count).toBe(1); + const updatedSettingsRes = await request.get( `${WORKER_URL}/user_api/address/${bound.address_id}/settings`, { headers: { 'x-user-token': user.jwt } }, @@ -130,6 +162,29 @@ test.describe('User send mail API', () => { expect(sendbox.results).toHaveLength(1); expect(JSON.parse(sendbox.results[0].raw).subject).toBe(subject); + const userSendboxRes = await request.get( + `${WORKER_URL}/user_api/sendbox?limit=20&offset=0`, + { headers: { 'x-user-token': user.jwt } }, + ); + expect(userSendboxRes.ok()).toBe(true); + const userSendbox = await userSendboxRes.json(); + expect(userSendbox.count).toBe(1); + expect(userSendbox.results).toHaveLength(1); + + const filteredSendboxRes = await request.get( + `${WORKER_URL}/user_api/sendbox?limit=20&offset=0&address=${encodeURIComponent(bound.address)}`, + { headers: { 'x-user-token': user.jwt } }, + ); + expect(filteredSendboxRes.ok()).toBe(true); + expect((await filteredSendboxRes.json()).count).toBe(1); + + const outsiderFilterRes = await request.get( + `${WORKER_URL}/user_api/sendbox?limit=20&offset=0&address=${encodeURIComponent(outsider.address)}`, + { headers: { 'x-user-token': user.jwt } }, + ); + expect(outsiderFilterRes.ok()).toBe(true); + expect((await outsiderFilterRes.json()).count).toBe(0); + const outsiderSendboxRes = await request.get( `${WORKER_URL}/user_api/address/${outsider.address_id}/sendbox?limit=20&offset=0`, { headers: { 'x-user-token': user.jwt } }, @@ -137,7 +192,7 @@ test.describe('User send mail API', () => { expect(outsiderSendboxRes.status()).toBe(400); const deleteRes = await request.delete( - `${WORKER_URL}/user_api/address/${bound.address_id}/sendbox/${sendbox.results[0].id}`, + `${WORKER_URL}/user_api/sendbox/${sendbox.results[0].id}`, { headers: { 'x-user-token': user.jwt } }, ); expect(deleteRes.ok()).toBe(true); diff --git a/e2e/tests/browser/user-send-mail.spec.ts b/e2e/tests/browser/user-send-mail.spec.ts index dc3911c..fd00d83 100644 --- a/e2e/tests/browser/user-send-mail.spec.ts +++ b/e2e/tests/browser/user-send-mail.spec.ts @@ -64,21 +64,27 @@ test.describe('User send mail page', () => { await page.goto(`${FRONTEND_URL}/en/user`); await expect(page.getByText(user.email)).toBeVisible({ timeout: 15_000 }); - await page.getByText('Send Mail', { exact: true }).click(); + const credentialResponse = page.waitForResponse((response) => ( + response.request().method() === 'GET' + && new URL(response.url()).pathname + === `/user_api/bind_address_jwt/${address!.address_id}` + )); + const addressRow = page.getByRole('row').filter({ hasText: address.address }); + await addressRow.getByRole('button', { name: 'Credentials & Connection Methods' }).click(); + expect((await credentialResponse).ok()).toBe(true); + await expect(page.getByRole('dialog')).toContainText(address.address); + await page.getByRole('button', { name: 'close' }).click(); - const addressSelect = page.locator('.address-picker-select'); - await addressSelect.click(); const settingsResponse = page.waitForResponse((response) => ( new URL(response.url()).pathname === `/user_api/address/${address!.address_id}/settings` )); - await page.locator('.n-base-select-menu:visible') - .getByText(address.address, { exact: true }) - .click(); + await page.getByText('Send Mail', { exact: true }).click(); expect((await settingsResponse).ok()).toBe(true); await expect(page.getByRole('heading', { name: 'Compose email', exact: true })).toBeVisible(); await expect(page.locator('.composer-title')).toContainText(address.address); + await expect(page.locator('.address-picker-select')).toContainText(address.address); const subject = `Browser user send ${Date.now()}`; await page.getByRole('textbox', { name: /^Recipient address/ }) @@ -91,9 +97,15 @@ test.describe('User send mail page', () => { && new URL(response.url()).pathname === `/user_api/address/${address!.address_id}/send_mail` )); + const sendboxResponse = page.waitForResponse((response) => ( + response.request().method() === 'GET' + && new URL(response.url()).pathname === '/user_api/sendbox' + )); await page.getByRole('button', { name: 'Send', exact: true }).click(); expect((await sendResponse).ok()).toBe(true); + expect((await sendboxResponse).ok()).toBe(true); + await expect(page.locator('.n-tabs-tab--active')).toHaveText('Sent'); await expect(page.getByText(subject, { exact: true })).toBeVisible({ timeout: 15_000 }); } finally { try { diff --git a/frontend/src/i18n/locales/source/de.ts b/frontend/src/i18n/locales/source/de.ts index ded74ec..56c18be 100644 --- a/frontend/src/i18n/locales/source/de.ts +++ b/frontend/src/i18n/locales/source/de.ts @@ -653,8 +653,5 @@ export const deMessages = { "components.AddressCredentialModal.username": "Benutzername", "views.User.send_mail": "E-Mail senden", "views.user.UserMailClient.noAddress": "Wähle eine verknüpfte E-Mail-Adresse aus", - "views.user.UserMailClient.selectAddress": "Absenderadresse", - "views.user.UserMailClient.selectAddressTip": "Wähle eine verknüpfte Adresse, um E-Mails zu schreiben und gesendete Nachrichten anzuzeigen", - "views.user.UserMailClient.sendbox": "Gesendet", - "views.user.UserMailClient.sendMail": "Verfassen" + "views.user.UserMailClient.sendbox": "Gesendet" } diff --git a/frontend/src/i18n/locales/source/es.ts b/frontend/src/i18n/locales/source/es.ts index ee79a98..12af463 100644 --- a/frontend/src/i18n/locales/source/es.ts +++ b/frontend/src/i18n/locales/source/es.ts @@ -653,8 +653,5 @@ export const esMessages = { "components.AddressCredentialModal.username": "Usuario", "views.User.send_mail": "Enviar correo", "views.user.UserMailClient.noAddress": "Selecciona una dirección de correo vinculada", - "views.user.UserMailClient.selectAddress": "Dirección del remitente", - "views.user.UserMailClient.selectAddressTip": "Elige una dirección vinculada para redactar correos y ver los enviados", - "views.user.UserMailClient.sendbox": "Enviados", - "views.user.UserMailClient.sendMail": "Redactar" + "views.user.UserMailClient.sendbox": "Enviados" } diff --git a/frontend/src/i18n/locales/source/ja.ts b/frontend/src/i18n/locales/source/ja.ts index b130b27..b4158a5 100644 --- a/frontend/src/i18n/locales/source/ja.ts +++ b/frontend/src/i18n/locales/source/ja.ts @@ -653,8 +653,5 @@ export const jaMessages = { "components.AddressCredentialModal.username": "ユーザー名", "views.User.send_mail": "メール送信", "views.user.UserMailClient.noAddress": "紐付け済みのメールアドレスを選択してください", - "views.user.UserMailClient.selectAddress": "送信元アドレス", - "views.user.UserMailClient.selectAddressTip": "紐付け済みアドレスを選択して、メール作成と送信済みメールの確認ができます", - "views.user.UserMailClient.sendbox": "送信済み", - "views.user.UserMailClient.sendMail": "作成" + "views.user.UserMailClient.sendbox": "送信済み" } diff --git a/frontend/src/i18n/locales/source/ptBR.ts b/frontend/src/i18n/locales/source/ptBR.ts index 3affcb7..2c069cf 100644 --- a/frontend/src/i18n/locales/source/ptBR.ts +++ b/frontend/src/i18n/locales/source/ptBR.ts @@ -653,8 +653,5 @@ export const ptBRMessages = { "components.AddressCredentialModal.username": "Nome de usuário", "views.User.send_mail": "Enviar e-mail", "views.user.UserMailClient.noAddress": "Selecione um endereço de e-mail vinculado", - "views.user.UserMailClient.selectAddress": "Endereço do remetente", - "views.user.UserMailClient.selectAddressTip": "Escolha um endereço vinculado para escrever e ver e-mails enviados", - "views.user.UserMailClient.sendbox": "Enviados", - "views.user.UserMailClient.sendMail": "Escrever" + "views.user.UserMailClient.sendbox": "Enviados" } diff --git a/frontend/src/i18n/message-registry.ts b/frontend/src/i18n/message-registry.ts index c7e2ef6..f8e426b 100644 --- a/frontend/src/i18n/message-registry.ts +++ b/frontend/src/i18n/message-registry.ts @@ -756,21 +756,9 @@ export const MESSAGE_REGISTRY = { "en": "Select a bound email address to continue", "zh": "请选择一个已绑定的邮箱地址" }, - "selectAddress": { - "en": "Sender address", - "zh": "发件邮箱" - }, - "selectAddressTip": { - "en": "Choose a bound address to compose mail and view its sent items", - "zh": "选择已绑定邮箱后,可发送邮件并查看该邮箱的发件箱" - }, "sendbox": { "en": "Sent", "zh": "发件箱" - }, - "sendMail": { - "en": "Compose", - "zh": "写邮件" } }, "views.user.UserLogin": { diff --git a/frontend/src/views/User.vue b/frontend/src/views/User.vue index d58da9c..0265ea7 100644 --- a/frontend/src/views/User.vue +++ b/frontend/src/views/User.vue @@ -15,6 +15,7 @@ const { } = useGlobalState() const { t } = useScopedI18n('views.User') +const { t: userMailT } = useScopedI18n('views.user.UserMailClient') @@ -28,8 +29,11 @@ const { t } = useScopedI18n('views.User') + + + - + diff --git a/frontend/src/views/index/SendMail.vue b/frontend/src/views/index/SendMail.vue index c73d60c..ad13ba9 100644 --- a/frontend/src/views/index/SendMail.vue +++ b/frontend/src/views/index/SendMail.vue @@ -26,9 +26,21 @@ const props = defineProps({ type: Number, default: 0, }, + userAddressMode: { + type: Boolean, + default: false, + }, + addressOptions: { + type: Array, + default: () => [], + }, + addressLoading: { + type: Boolean, + default: false, + }, }) -const emit = defineEmits(['sent']) +const emit = defineEmits(['addressScroll', 'sent', 'update:addressId']) const { @@ -38,7 +50,7 @@ const { const { t } = useScopedI18n('views.index.SendMail') -const isUserAddressMode = computed(() => props.addressId > 0) +const isUserAddressMode = computed(() => props.userAddressMode || props.addressId > 0) const mailSettings = computed(() => ( isUserAddressMode.value ? userAddressSettings.value : settings.value )) @@ -210,7 +222,7 @@ onMounted(async () => {