From 5e4823a115052c79c38b8645126ae66940523f04 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sat, 12 Sep 2026 16:25:25 +0800 Subject: [PATCH] feat: select email for webhook tests (#1147) --- CHANGELOG.md | 1 + CHANGELOG_EN.md | 1 + e2e/tests/api/webhook-test-mail-id.spec.ts | 91 +++++++++++++++++++ e2e/tests/browser/webhook-test.spec.ts | 74 +++++++++++++++ frontend/src/components/WebhookComponent.vue | 41 ++++++++- frontend/src/i18n/locales/source/de.ts | 5 + frontend/src/i18n/locales/source/es.ts | 5 + frontend/src/i18n/locales/source/ja.ts | 5 + frontend/src/i18n/locales/source/ptBR.ts | 5 + frontend/src/i18n/message-registry.ts | 5 + .../docs/en/guide/feature/webhook.md | 2 + .../docs/zh/guide/feature/webhook.md | 2 + worker/src/admin_api/mail_webhook_settings.ts | 19 +++- worker/src/i18n/en.ts | 3 + worker/src/i18n/type.ts | 3 + worker/src/i18n/zh.ts | 3 + worker/src/mails_api/webhook_settings.ts | 18 +++- 17 files changed, 275 insertions(+), 8 deletions(-) create mode 100644 e2e/tests/api/webhook-test-mail-id.spec.ts create mode 100644 e2e/tests/browser/webhook-test.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ff2197..915f5f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- feat: |Webhook| 测试弹框支持随机邮件或指定邮件 ID,校验请求体及邮箱归属并适配现有前端语言及中英文错误提示 - feat: |Worker| 新增 `DISABLE_ADDRESS_UPDATED_AT`,可关闭单地址及用户批量的主动保活刷新,并禁止内置手动及定时不活跃地址清理,降低 D1 写入量 - feat: |Frontend| 新增 `VITE_DEFAULT_LANG` 构建变量,并支持通过 `index.html` 运行时配置覆盖前端设置 - feat: |兑换码| 新增角色、发信额度及专属邮箱兑换与管理,完善并发保护和表单提示 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 74321d60..c25a2f80 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -10,6 +10,7 @@ ### Features +- feat: |Webhook| Support random or specified email IDs in the test dialog, with request-body validation, mailbox ownership checks, existing UI languages and Chinese/English errors - feat: |Worker| Add `DISABLE_ADDRESS_UPDATED_AT` to disable individual and user-wide address activity keep-alive updates and built-in manual/scheduled inactive-address cleanup, reducing D1 writes - feat: |Frontend| Add the `VITE_DEFAULT_LANG` build variable and support overriding frontend settings through runtime configuration in `index.html` - feat: |Redemption Codes| Add role, sending-credit and custom-mailbox redemption with Admin management, concurrency protection and form validation diff --git a/e2e/tests/api/webhook-test-mail-id.spec.ts b/e2e/tests/api/webhook-test-mail-id.spec.ts new file mode 100644 index 00000000..eb9eb7e7 --- /dev/null +++ b/e2e/tests/api/webhook-test-mail-id.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@playwright/test'; +import http from 'node:http'; +import { WORKER_URL, createTestAddress, seedTestMail } from '../../fixtures/test-helpers'; + +test('Webhook tests support random and specified mail IDs with ownership checks', async ({ request }) => { + const mailbox = await createTestAddress(request, 'webhookid'); + const other = await createTestAddress(request, 'webhookother'); + const headers = { Authorization: `Bearer ${mailbox.jwt}` }; + const payloads: any[] = []; + const server = http.createServer((req, res) => { + const chunks: Buffer[] = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', () => { + try { + payloads.push(JSON.parse(Buffer.concat(chunks).toString())); + res.writeHead(200).end(); + } catch { + res.writeHead(400).end(); + } + }); + }); + await new Promise(resolve => server.listen(0, '0.0.0.0', resolve)); + const port = (server.address() as import('node:net').AddressInfo).port; + const settings = { + enabled: true, url: `http://${process.env.CI ? 'e2e-runner' : 'localhost'}:${port}`, + method: 'POST', headers: '{"Content-Type":"application/json"}', + body: '{"id":"${id}","subject":"${subject}"}', + }; + try { + await seedTestMail(request, mailbox.address, { subject: 'First selected email' }); + await seedTestMail(request, mailbox.address, { subject: 'Second selected email' }); + const list = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, { headers }); + expect(list.ok()).toBe(true); + const { results } = await list.json(); + expect(results).toHaveLength(2); + const selected = results[1]; + for (const endpoint of ['/api/webhook/test', '/admin/mail_webhook/test']) { + const count = payloads.length; + expect((await request.post(`${WORKER_URL}${endpoint}`, { headers, data: settings })).ok()).toBe(true); + await expect.poll(() => payloads.length).toBe(count + 1); + if (endpoint.startsWith('/api/')) { + expect(results.map((mail: any) => String(mail.id))).toContain(payloads[count].id); + } + expect((await request.post(`${WORKER_URL}${endpoint}`, { + headers, data: { ...settings, mail_id: Number(selected.id) }, + })).ok()).toBe(true); + await expect.poll(() => payloads.length).toBe(count + 2); + expect(payloads[count + 1].id).toBe(String(selected.id)); + for (const mail_id of [0, -1, 1.5, '1', null]) { + expect((await request.post(`${WORKER_URL}${endpoint}`, { + headers, data: { ...settings, mail_id }, + })).status()).toBe(400); + } + expect((await request.post(`${WORKER_URL}${endpoint}`, { + headers, data: { ...settings, mail_id: Number.MAX_SAFE_INTEGER }, + })).status()).toBe(404); + expect(payloads).toHaveLength(count + 2); + for (const [lang, invalid, missing] of [ + ['zh', '无效的邮件 ID', '邮件不存在'], + ['en', 'Invalid mail ID', 'Mail not found'], + ]) { + for (const body of ['null', '[]', '1', 'true', '"text"', '{', '']) { + const response = await request.post(`${WORKER_URL}${endpoint}`, { + headers: { ...headers, 'x-lang': lang, 'Content-Type': 'application/json' }, + data: body, + }); + expect(response.status()).toBe(400); + expect(await response.text()).toBe(lang === 'zh' ? '无效的请求体' : 'Invalid request body'); + } + for (const [mail_id, status, message] of [[0, 400, invalid], [999999999, 404, missing]] as const) { + const response = await request.post(`${WORKER_URL}${endpoint}`, { + headers: { ...headers, 'x-lang': lang }, data: { ...settings, mail_id }, + }); + expect(response.status()).toBe(status); + expect(await response.text()).toBe(message); + } + } + expect(payloads).toHaveLength(count + 2); + } + const count = payloads.length; + expect((await request.post(`${WORKER_URL}/api/webhook/test`, { + headers: { Authorization: `Bearer ${other.jwt}` }, + data: { ...settings, mail_id: Number(selected.id) }, + })).status()).toBe(404); + expect(payloads).toHaveLength(count); + } finally { + await request.delete(`${WORKER_URL}/admin/delete_address/${mailbox.address_id}`); + await request.delete(`${WORKER_URL}/admin/delete_address/${other.address_id}`); + await new Promise(resolve => server.close(() => resolve())); + } +}); diff --git a/e2e/tests/browser/webhook-test.spec.ts b/e2e/tests/browser/webhook-test.spec.ts new file mode 100644 index 00000000..d876d40f --- /dev/null +++ b/e2e/tests/browser/webhook-test.spec.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test'; +import { FRONTEND_URL } from '../../fixtures/test-helpers'; + +for (const [locale, mails, webhook, testLabel, specified, mailId, invalid] of [ + ['zh', '邮件', '邮件 Webhook', '测试', '指定 ID', '邮件 ID', '请输入有效的正整数邮件 ID'], + ['en', 'Emails', 'Mail Webhook', 'Test', 'Specify ID', 'Email ID', 'Enter a valid positive integer email ID'], + ['es', 'Correos', 'Webhook de correo', 'Prueba', 'Especificar ID', 'ID del correo', 'Introduce un ID de correo válido que sea un entero positivo'], + ['pt-BR', 'E-mails', 'Webhook de e-mail', 'Teste', 'Especificar ID', 'ID do e-mail', 'Digite um ID de e-mail válido que seja um número inteiro positivo'], + ['ja', 'メール', 'メールWebhook', 'テスト', 'ID を指定', 'メール ID', '有効な正の整数のメール ID を入力してください'], + ['de', 'E-Mails', 'Mail-Webhook', 'Test', 'ID angeben', 'E-Mail-ID', 'Gib eine gültige positive ganze Zahl als E-Mail-ID ein'], +]) { + test(`Webhook test dialog translations: ${locale}`, async ({ page }) => { + await page.route('**/admin/mail_webhook/settings', route => route.fulfill({ + json: { enabled: true, url: 'https://example.com/webhook', method: 'POST', headers: '{}', body: '{}' }, + })); + await page.goto(`${FRONTEND_URL}/${locale}/admin`); + await page.getByText(mails, { exact: true }).click(); + await page.getByText(webhook, { exact: true }).click(); + await page.getByRole('button', { name: testLabel, exact: true }).click(); + const dialog = page.getByRole('dialog'); + await dialog.getByText(specified, { exact: true }).click(); + await expect(dialog.getByPlaceholder(mailId, { exact: true })).toBeVisible(); + await dialog.getByRole('button', { name: testLabel, exact: true }).click(); + await expect(page.getByText(invalid, { exact: true })).toBeVisible(); + }); +} + +test('Webhook test dialog selects random or specified email', async ({ page }) => { + const requests: any[] = []; + let fail = false; + await page.route('**/admin/mail_webhook/settings', route => route.fulfill({ + json: { enabled: true, url: 'https://example.com/webhook', method: 'POST', headers: '{}', body: '{}' }, + })); + await page.route('**/admin/mail_webhook/test', route => { + requests.push(route.request().postDataJSON()); + return route.fulfill({ status: fail ? 404 : 200, body: fail ? 'Mail not found' : '{"success":true}' }); + }); + await page.goto(`${FRONTEND_URL}/zh/admin`); + await page.getByText('邮件', { exact: true }).click(); + await page.getByText('邮件 Webhook', { exact: true }).click(); + const open = page.locator('#app').getByRole('button', { name: '测试', exact: true }); + await open.click(); + const dialog = page.getByRole('dialog'); + await expect(dialog.getByText('随机邮件', { exact: true })).toBeVisible(); + await expect(dialog.getByPlaceholder('邮件 ID', { exact: true })).toHaveCount(0); + await dialog.getByRole('button', { name: '取消', exact: true }).click(); + await expect(dialog).toBeHidden(); + expect(requests).toHaveLength(0); + await open.click(); + await dialog.getByRole('button', { name: '测试', exact: true }).click(); + await expect(dialog).toBeHidden(); + expect(requests).toHaveLength(1); + expect(requests[0]).not.toHaveProperty('mail_id'); + await open.click(); + await dialog.getByText('指定 ID', { exact: true }).click(); + await dialog.getByRole('button', { name: '测试', exact: true }).click(); + await expect(page.getByText('请输入有效的正整数邮件 ID', { exact: true })).toBeVisible(); + expect(requests).toHaveLength(1); + await dialog.getByPlaceholder('邮件 ID', { exact: true }).fill('123'); + fail = true; + await dialog.getByRole('button', { name: '测试', exact: true }).click(); + await expect(page.getByText(/Mail not found/).first()).toBeVisible(); + await expect(dialog).toBeVisible(); + expect(requests[1].mail_id).toBe(123); + fail = false; + await dialog.getByRole('button', { name: '测试', exact: true }).click(); + await expect(dialog).toBeHidden(); + expect(requests[2].mail_id).toBe(123); + await open.click(); + await dialog.getByText('随机邮件', { exact: true }).click(); + await dialog.getByRole('button', { name: '测试', exact: true }).click(); + await expect(dialog).toBeHidden(); + expect(requests[3]).not.toHaveProperty('mail_id'); +}); diff --git a/frontend/src/components/WebhookComponent.vue b/frontend/src/components/WebhookComponent.vue index 1bdf4c5a..15b0da9b 100644 --- a/frontend/src/components/WebhookComponent.vue +++ b/frontend/src/components/WebhookComponent.vue @@ -163,6 +163,10 @@ const handlePresetSelect = (key: number) => { const webhookSettings = ref(new WebhookSettings()) const enableWebhook = ref(false) +const showTestModal = ref(false) +const testMode = ref('random') +const testMailId = ref(null) +const testing = ref(false) const fetchData = async () => { try { @@ -188,15 +192,27 @@ const saveSettings = async () => { } const testSettings = async () => { + if (testing.value) return if (!webhookSettings.value.url) { message.error(t('urlMissing')) return } + if (testMode.value === 'specified' && (!Number.isSafeInteger(testMailId.value) || (testMailId.value ?? 0) <= 0)) { + message.error(t('invalidMailId')) + return + } + testing.value = true try { - await props.testSettings(webhookSettings.value) + await props.testSettings({ + ...webhookSettings.value, + ...(testMode.value === 'specified' ? { mail_id: testMailId.value } : {}), + }) message.success(t('successTip')) + showTestModal.value = false } catch (error) { message.error((error as Error).message || "error"); + } finally { + testing.value = false } } @@ -214,7 +230,7 @@ onMounted(async () => { {{ t('presets') }} - + {{ t('test') }} @@ -242,6 +258,27 @@ onMounted(async () => { + + + + {{ t('randomMail') }} + {{ t('specifiedMail') }} + + + + + + + diff --git a/frontend/src/i18n/locales/source/de.ts b/frontend/src/i18n/locales/source/de.ts index 90e0ff2b..6b2ed32e 100644 --- a/frontend/src/i18n/locales/source/de.ts +++ b/frontend/src/i18n/locales/source/de.ts @@ -1,4 +1,9 @@ export const deMessages = { + "components.WebhookComponent.randomMail": "Zufällige E-Mail", + "components.WebhookComponent.specifiedMail": "ID angeben", + "components.WebhookComponent.mailId": "E-Mail-ID", + "components.WebhookComponent.invalidMailId": "Gib eine gültige positive ganze Zahl als E-Mail-ID ein", + "components.WebhookComponent.cancel": "Abbrechen", "views.index.SendMail.balanceUnavailable": "Kein Sendeguthaben für diese Adresse", "views.index.SendMail.composeMail": "E-Mail verfassen", "views.index.SendMail.contentPlaceholder": "Nachricht schreiben...", diff --git a/frontend/src/i18n/locales/source/es.ts b/frontend/src/i18n/locales/source/es.ts index 006ca5c8..c9235a07 100644 --- a/frontend/src/i18n/locales/source/es.ts +++ b/frontend/src/i18n/locales/source/es.ts @@ -1,4 +1,9 @@ export const esMessages = { + "components.WebhookComponent.randomMail": "Correo aleatorio", + "components.WebhookComponent.specifiedMail": "Especificar ID", + "components.WebhookComponent.mailId": "ID del correo", + "components.WebhookComponent.invalidMailId": "Introduce un ID de correo válido que sea un entero positivo", + "components.WebhookComponent.cancel": "Cancelar", "views.index.SendMail.balanceUnavailable": "No hay saldo de envío para esta dirección", "views.index.SendMail.composeMail": "Redactar correo", "views.index.SendMail.contentPlaceholder": "Escribe tu mensaje...", diff --git a/frontend/src/i18n/locales/source/ja.ts b/frontend/src/i18n/locales/source/ja.ts index b6fedb97..56962f38 100644 --- a/frontend/src/i18n/locales/source/ja.ts +++ b/frontend/src/i18n/locales/source/ja.ts @@ -1,4 +1,9 @@ export const jaMessages = { + "components.WebhookComponent.randomMail": "ランダムなメール", + "components.WebhookComponent.specifiedMail": "ID を指定", + "components.WebhookComponent.mailId": "メール ID", + "components.WebhookComponent.invalidMailId": "有効な正の整数のメール ID を入力してください", + "components.WebhookComponent.cancel": "キャンセル", "views.index.SendMail.balanceUnavailable": "このアドレスには送信残高がありません", "views.index.SendMail.composeMail": "メールを作成", "views.index.SendMail.contentPlaceholder": "メッセージを入力...", diff --git a/frontend/src/i18n/locales/source/ptBR.ts b/frontend/src/i18n/locales/source/ptBR.ts index c80fc058..842fd31d 100644 --- a/frontend/src/i18n/locales/source/ptBR.ts +++ b/frontend/src/i18n/locales/source/ptBR.ts @@ -1,4 +1,9 @@ export const ptBRMessages = { + "components.WebhookComponent.randomMail": "E-mail aleatório", + "components.WebhookComponent.specifiedMail": "Especificar ID", + "components.WebhookComponent.mailId": "ID do e-mail", + "components.WebhookComponent.invalidMailId": "Digite um ID de e-mail válido que seja um número inteiro positivo", + "components.WebhookComponent.cancel": "Cancelar", "views.index.SendMail.balanceUnavailable": "Sem saldo de envio para este endereço", "views.index.SendMail.composeMail": "Escrever e-mail", "views.index.SendMail.contentPlaceholder": "Escreva sua mensagem...", diff --git a/frontend/src/i18n/message-registry.ts b/frontend/src/i18n/message-registry.ts index 1378dff1..57bc8c67 100644 --- a/frontend/src/i18n/message-registry.ts +++ b/frontend/src/i18n/message-registry.ts @@ -1,5 +1,10 @@ export const MESSAGE_REGISTRY = { "components.WebhookComponent": { + "randomMail": { "en": "Random email", "zh": "随机邮件" }, + "specifiedMail": { "en": "Specify ID", "zh": "指定 ID" }, + "mailId": { "en": "Email ID", "zh": "邮件 ID" }, + "invalidMailId": { "en": "Enter a valid positive integer email ID", "zh": "请输入有效的正整数邮件 ID" }, + "cancel": { "en": "Cancel", "zh": "取消" }, "enable": { "en": "Enable", "zh": "启用" diff --git a/vitepress-docs/docs/en/guide/feature/webhook.md b/vitepress-docs/docs/en/guide/feature/webhook.md index e2073bf5..ffcf267c 100644 --- a/vitepress-docs/docs/en/guide/feature/webhook.md +++ b/vitepress-docs/docs/en/guide/feature/webhook.md @@ -118,3 +118,5 @@ To get the url, you need to configure the worker's `FRONTEND_URL` to your fronte ``` When AI email extraction is enabled, webhook templates can use the `aiExtractType`, `aiExtractResult`, and `aiExtractResultText` placeholders. They are empty strings when no extraction result is available. + +Click **Test** to choose a random email (default) or specify an email ID. Missing specified emails return an error without falling back to a random email. Mailbox tests can only use that mailbox's emails; administrators can select any email. The existing `/api/webhook/test` and `/admin/mail_webhook/test` endpoints accept an optional positive integer `mail_id` in the request body. Omitting it preserves random selection. The UI sends this field only for testing, without saving it in the Webhook configuration. diff --git a/vitepress-docs/docs/zh/guide/feature/webhook.md b/vitepress-docs/docs/zh/guide/feature/webhook.md index cfa15717..13226d53 100644 --- a/vitepress-docs/docs/zh/guide/feature/webhook.md +++ b/vitepress-docs/docs/zh/guide/feature/webhook.md @@ -118,3 +118,5 @@ ``` 启用 AI 邮件内容提取后,Webhook 模板可使用 `aiExtractType`、`aiExtractResult`、`aiExtractResultText` 占位符。未提取到结果时这些字段为空字符串。 + +点击“测试”会弹出选择框:默认随机选择邮件,也可以选择“指定 ID”并输入邮件 ID。指定邮件不存在时会报错,不会回退随机;邮箱页面只能使用当前邮箱的邮件,管理员页面可指定任意邮件。现有测试接口 `/api/webhook/test` 和 `/admin/mail_webhook/test` 的请求 Body 支持可选正整数 `mail_id`,不传则沿用随机逻辑。页面仅在测试请求中传入该参数,不会保存到 Webhook 配置。 diff --git a/worker/src/admin_api/mail_webhook_settings.ts b/worker/src/admin_api/mail_webhook_settings.ts index e30c3075..1da82b5f 100644 --- a/worker/src/admin_api/mail_webhook_settings.ts +++ b/worker/src/admin_api/mail_webhook_settings.ts @@ -3,6 +3,7 @@ import { CONSTANTS } from "../constants"; import { WebhookSettings, RawMailRow } from "../models"; import { commonParseMail, sendWebhook } from "../common"; import { resolveRawEmail } from "../gzip"; +import i18n from "../i18n"; async function getWebhookSettings(c: Context): Promise { const settings = await c.env.KV.get( @@ -20,12 +21,24 @@ async function saveWebhookSettings(c: Context): Promise): Promise { - const settings = await c.req.json(); - // random raw email - const mailRow = await c.env.DB.prepare( + const msgs = i18n.getMessagesbyContext(c); + const settings = await c.req.json().catch(() => null); + if (!settings || typeof settings !== "object" || Array.isArray(settings)) { + return c.text(msgs.InvalidRequestBodyMsg, 400); + } + const requestedMailId = settings.mail_id; + if (requestedMailId !== undefined && (!Number.isSafeInteger(requestedMailId) || requestedMailId <= 0)) { + return c.text(msgs.InvalidMailIdMsg, 400); + } + const mailRow = requestedMailId !== undefined ? await c.env.DB.prepare( + `SELECT * FROM raw_mails WHERE id = ?` + ).bind(requestedMailId).first() : await c.env.DB.prepare( `SELECT * FROM raw_mails ORDER BY RANDOM() LIMIT 1` ).first(); const mailId = mailRow?.id; + if (requestedMailId !== undefined && !mailRow) { + return c.text(msgs.MailNotFoundMsg, 404); + } const raw = mailRow ? await resolveRawEmail(mailRow) : ""; const parsedEmailContext: ParsedEmailContext = { rawEmail: raw }; const parsedEmail = await commonParseMail(parsedEmailContext); diff --git a/worker/src/i18n/en.ts b/worker/src/i18n/en.ts index 941cc929..37a1a035 100644 --- a/worker/src/i18n/en.ts +++ b/worker/src/i18n/en.ts @@ -1,6 +1,9 @@ import { LocaleMessages } from "./type"; const messages: LocaleMessages = { + InvalidRequestBodyMsg: "Invalid request body", + InvalidMailIdMsg: "Invalid mail ID", + MailNotFoundMsg: "Mail not found", CustomAuthPasswordMsg: "You have enabled the private site password, please provide the password", UserTokenExpiredMsg: "Your token has expired, please login again", UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page", diff --git a/worker/src/i18n/type.ts b/worker/src/i18n/type.ts index de3bb92b..38d73905 100644 --- a/worker/src/i18n/type.ts +++ b/worker/src/i18n/type.ts @@ -1,4 +1,7 @@ export type LocaleMessages = { + InvalidRequestBodyMsg: string + InvalidMailIdMsg: string + MailNotFoundMsg: string CustomAuthPasswordMsg: string UserTokenExpiredMsg: string UserAcceesTokenExpiredMsg: string diff --git a/worker/src/i18n/zh.ts b/worker/src/i18n/zh.ts index 06ac4960..cc102b0e 100644 --- a/worker/src/i18n/zh.ts +++ b/worker/src/i18n/zh.ts @@ -1,6 +1,9 @@ import { LocaleMessages } from "./type"; const messages: LocaleMessages = { + InvalidRequestBodyMsg: "无效的请求体", + InvalidMailIdMsg: "无效的邮件 ID", + MailNotFoundMsg: "邮件不存在", CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码", UserTokenExpiredMsg: "您的令牌已过期, 请重新登录", UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面", diff --git a/worker/src/mails_api/webhook_settings.ts b/worker/src/mails_api/webhook_settings.ts index 496aba39..e71fef21 100644 --- a/worker/src/mails_api/webhook_settings.ts +++ b/worker/src/mails_api/webhook_settings.ts @@ -35,13 +35,25 @@ async function saveWebhookSettings(c: Context): Promise): Promise { - const settings = await c.req.json(); + const msgs = i18n.getMessagesbyContext(c); + const settings = await c.req.json().catch(() => null); + if (!settings || typeof settings !== "object" || Array.isArray(settings)) { + return c.text(msgs.InvalidRequestBodyMsg, 400); + } + const requestedMailId = settings.mail_id; + if (requestedMailId !== undefined && (!Number.isSafeInteger(requestedMailId) || requestedMailId <= 0)) { + return c.text(msgs.InvalidMailIdMsg, 400); + } const { address } = c.get("jwtPayload"); - // random raw email - const mailRow = await c.env.DB.prepare( + const mailRow = requestedMailId !== undefined ? await c.env.DB.prepare( + `SELECT * FROM raw_mails WHERE id = ? AND address = ?` + ).bind(requestedMailId, address).first() : await c.env.DB.prepare( `SELECT * FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1` ).bind(address).first(); const mailId = mailRow?.id; + if (requestedMailId !== undefined && !mailRow) { + return c.text(msgs.MailNotFoundMsg, 404); + } const raw = mailRow ? await resolveRawEmail(mailRow) : ""; const parsedEmailContext: ParsedEmailContext = { rawEmail: raw }; const parsedEmail = await commonParseMail(parsedEmailContext);