mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-12 19:06:36 +08:00
feat: select email for webhook tests (#1147)
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- feat: |Webhook| 测试弹框支持随机邮件或指定邮件 ID,校验请求体及邮箱归属并适配现有前端语言及中英文错误提示
|
||||||
- feat: |Worker| 新增 `DISABLE_ADDRESS_UPDATED_AT`,可关闭单地址及用户批量的主动保活刷新,并禁止内置手动及定时不活跃地址清理,降低 D1 写入量
|
- feat: |Worker| 新增 `DISABLE_ADDRESS_UPDATED_AT`,可关闭单地址及用户批量的主动保活刷新,并禁止内置手动及定时不活跃地址清理,降低 D1 写入量
|
||||||
- feat: |Frontend| 新增 `VITE_DEFAULT_LANG` 构建变量,并支持通过 `index.html` 运行时配置覆盖前端设置
|
- feat: |Frontend| 新增 `VITE_DEFAULT_LANG` 构建变量,并支持通过 `index.html` 运行时配置覆盖前端设置
|
||||||
- feat: |兑换码| 新增角色、发信额度及专属邮箱兑换与管理,完善并发保护和表单提示
|
- feat: |兑换码| 新增角色、发信额度及专属邮箱兑换与管理,完善并发保护和表单提示
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
### Features
|
### 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: |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: |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
|
- feat: |Redemption Codes| Add role, sending-credit and custom-mailbox redemption with Admin management, concurrency protection and form validation
|
||||||
|
|||||||
@@ -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<void>(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<void>(resolve => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
@@ -163,6 +163,10 @@ const handlePresetSelect = (key: number) => {
|
|||||||
|
|
||||||
const webhookSettings = ref<WebhookSettings>(new WebhookSettings())
|
const webhookSettings = ref<WebhookSettings>(new WebhookSettings())
|
||||||
const enableWebhook = ref(false)
|
const enableWebhook = ref(false)
|
||||||
|
const showTestModal = ref(false)
|
||||||
|
const testMode = ref('random')
|
||||||
|
const testMailId = ref<number | null>(null)
|
||||||
|
const testing = ref(false)
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -188,15 +192,27 @@ const saveSettings = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testSettings = async () => {
|
const testSettings = async () => {
|
||||||
|
if (testing.value) return
|
||||||
if (!webhookSettings.value.url) {
|
if (!webhookSettings.value.url) {
|
||||||
message.error(t('urlMissing'))
|
message.error(t('urlMissing'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (testMode.value === 'specified' && (!Number.isSafeInteger(testMailId.value) || (testMailId.value ?? 0) <= 0)) {
|
||||||
|
message.error(t('invalidMailId'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testing.value = true
|
||||||
try {
|
try {
|
||||||
await props.testSettings(webhookSettings.value)
|
await props.testSettings({
|
||||||
|
...webhookSettings.value,
|
||||||
|
...(testMode.value === 'specified' ? { mail_id: testMailId.value } : {}),
|
||||||
|
})
|
||||||
message.success(t('successTip'))
|
message.success(t('successTip'))
|
||||||
|
showTestModal.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error((error as Error).message || "error");
|
message.error((error as Error).message || "error");
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +230,7 @@ onMounted(async () => {
|
|||||||
{{ t('presets') }}
|
{{ t('presets') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
<n-button v-if="webhookSettings.enabled" @click="testSettings" secondary>
|
<n-button v-if="webhookSettings.enabled" @click="showTestModal = true" secondary>
|
||||||
{{ t('test') }}
|
{{ t('test') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="saveSettings" type="primary">
|
<n-button @click="saveSettings" type="primary">
|
||||||
@@ -242,6 +258,27 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-result v-else status="404" :title="t('notEnabled')" />
|
<n-result v-else status="404" :title="t('notEnabled')" />
|
||||||
|
<n-modal v-model:show="showTestModal" preset="card" :title="t('test')"
|
||||||
|
style="width: min(420px, calc(100vw - 32px))" :mask-closable="!testing"
|
||||||
|
:close-on-esc="!testing" :closable="!testing">
|
||||||
|
<n-radio-group v-model:value="testMode" :disabled="testing">
|
||||||
|
<n-space>
|
||||||
|
<n-radio value="random">{{ t('randomMail') }}</n-radio>
|
||||||
|
<n-radio value="specified">{{ t('specifiedMail') }}</n-radio>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
<n-form-item v-if="testMode === 'specified'" :label="t('mailId')" style="margin-top: 16px">
|
||||||
|
<n-input-number v-model:value="testMailId" :min="1" :max="Number.MAX_SAFE_INTEGER"
|
||||||
|
:precision="0" :show-button="false" :disabled="testing" :placeholder="t('mailId')"
|
||||||
|
style="width: 100%" />
|
||||||
|
</n-form-item>
|
||||||
|
<template #footer>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button :disabled="testing" @click="showTestModal = false">{{ t('cancel') }}</n-button>
|
||||||
|
<n-button type="primary" :loading="testing" @click="testSettings">{{ t('test') }}</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const deMessages = {
|
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.balanceUnavailable": "Kein Sendeguthaben für diese Adresse",
|
||||||
"views.index.SendMail.composeMail": "E-Mail verfassen",
|
"views.index.SendMail.composeMail": "E-Mail verfassen",
|
||||||
"views.index.SendMail.contentPlaceholder": "Nachricht schreiben...",
|
"views.index.SendMail.contentPlaceholder": "Nachricht schreiben...",
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const esMessages = {
|
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.balanceUnavailable": "No hay saldo de envío para esta dirección",
|
||||||
"views.index.SendMail.composeMail": "Redactar correo",
|
"views.index.SendMail.composeMail": "Redactar correo",
|
||||||
"views.index.SendMail.contentPlaceholder": "Escribe tu mensaje...",
|
"views.index.SendMail.contentPlaceholder": "Escribe tu mensaje...",
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const jaMessages = {
|
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.balanceUnavailable": "このアドレスには送信残高がありません",
|
||||||
"views.index.SendMail.composeMail": "メールを作成",
|
"views.index.SendMail.composeMail": "メールを作成",
|
||||||
"views.index.SendMail.contentPlaceholder": "メッセージを入力...",
|
"views.index.SendMail.contentPlaceholder": "メッセージを入力...",
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const ptBRMessages = {
|
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.balanceUnavailable": "Sem saldo de envio para este endereço",
|
||||||
"views.index.SendMail.composeMail": "Escrever e-mail",
|
"views.index.SendMail.composeMail": "Escrever e-mail",
|
||||||
"views.index.SendMail.contentPlaceholder": "Escreva sua mensagem...",
|
"views.index.SendMail.contentPlaceholder": "Escreva sua mensagem...",
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
export const MESSAGE_REGISTRY = {
|
export const MESSAGE_REGISTRY = {
|
||||||
"components.WebhookComponent": {
|
"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": {
|
"enable": {
|
||||||
"en": "Enable",
|
"en": "Enable",
|
||||||
"zh": "启用"
|
"zh": "启用"
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|||||||
@@ -118,3 +118,5 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
启用 AI 邮件内容提取后,Webhook 模板可使用 `aiExtractType`、`aiExtractResult`、`aiExtractResultText` 占位符。未提取到结果时这些字段为空字符串。
|
启用 AI 邮件内容提取后,Webhook 模板可使用 `aiExtractType`、`aiExtractResult`、`aiExtractResultText` 占位符。未提取到结果时这些字段为空字符串。
|
||||||
|
|
||||||
|
点击“测试”会弹出选择框:默认随机选择邮件,也可以选择“指定 ID”并输入邮件 ID。指定邮件不存在时会报错,不会回退随机;邮箱页面只能使用当前邮箱的邮件,管理员页面可指定任意邮件。现有测试接口 `/api/webhook/test` 和 `/admin/mail_webhook/test` 的请求 Body 支持可选正整数 `mail_id`,不传则沿用随机逻辑。页面仅在测试请求中传入该参数,不会保存到 Webhook 配置。
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CONSTANTS } from "../constants";
|
|||||||
import { WebhookSettings, RawMailRow } from "../models";
|
import { WebhookSettings, RawMailRow } from "../models";
|
||||||
import { commonParseMail, sendWebhook } from "../common";
|
import { commonParseMail, sendWebhook } from "../common";
|
||||||
import { resolveRawEmail } from "../gzip";
|
import { resolveRawEmail } from "../gzip";
|
||||||
|
import i18n from "../i18n";
|
||||||
|
|
||||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const settings = await c.env.KV.get<WebhookSettings>(
|
const settings = await c.env.KV.get<WebhookSettings>(
|
||||||
@@ -20,12 +21,24 @@ async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const settings = await c.req.json<WebhookSettings>();
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
// random raw email
|
const settings = await c.req.json<WebhookSettings & { mail_id?: number }>().catch(() => null);
|
||||||
const mailRow = await c.env.DB.prepare(
|
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<RawMailRow>() : await c.env.DB.prepare(
|
||||||
`SELECT * FROM raw_mails ORDER BY RANDOM() LIMIT 1`
|
`SELECT * FROM raw_mails ORDER BY RANDOM() LIMIT 1`
|
||||||
).first<RawMailRow>();
|
).first<RawMailRow>();
|
||||||
const mailId = mailRow?.id;
|
const mailId = mailRow?.id;
|
||||||
|
if (requestedMailId !== undefined && !mailRow) {
|
||||||
|
return c.text(msgs.MailNotFoundMsg, 404);
|
||||||
|
}
|
||||||
const raw = mailRow ? await resolveRawEmail(mailRow) : "";
|
const raw = mailRow ? await resolveRawEmail(mailRow) : "";
|
||||||
const parsedEmailContext: ParsedEmailContext = { rawEmail: raw };
|
const parsedEmailContext: ParsedEmailContext = { rawEmail: raw };
|
||||||
const parsedEmail = await commonParseMail(parsedEmailContext);
|
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { LocaleMessages } from "./type";
|
import { LocaleMessages } from "./type";
|
||||||
|
|
||||||
const messages: LocaleMessages = {
|
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",
|
CustomAuthPasswordMsg: "You have enabled the private site password, please provide the password",
|
||||||
UserTokenExpiredMsg: "Your token has expired, please login again",
|
UserTokenExpiredMsg: "Your token has expired, please login again",
|
||||||
UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page",
|
UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export type LocaleMessages = {
|
export type LocaleMessages = {
|
||||||
|
InvalidRequestBodyMsg: string
|
||||||
|
InvalidMailIdMsg: string
|
||||||
|
MailNotFoundMsg: string
|
||||||
CustomAuthPasswordMsg: string
|
CustomAuthPasswordMsg: string
|
||||||
UserTokenExpiredMsg: string
|
UserTokenExpiredMsg: string
|
||||||
UserAcceesTokenExpiredMsg: string
|
UserAcceesTokenExpiredMsg: string
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { LocaleMessages } from "./type";
|
import { LocaleMessages } from "./type";
|
||||||
|
|
||||||
const messages: LocaleMessages = {
|
const messages: LocaleMessages = {
|
||||||
|
InvalidRequestBodyMsg: "无效的请求体",
|
||||||
|
InvalidMailIdMsg: "无效的邮件 ID",
|
||||||
|
MailNotFoundMsg: "邮件不存在",
|
||||||
CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码",
|
CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码",
|
||||||
UserTokenExpiredMsg: "您的令牌已过期, 请重新登录",
|
UserTokenExpiredMsg: "您的令牌已过期, 请重新登录",
|
||||||
UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面",
|
UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面",
|
||||||
|
|||||||
@@ -35,13 +35,25 @@ async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
const settings = await c.req.json<WebhookSettings>();
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
|
const settings = await c.req.json<WebhookSettings & { mail_id?: number }>().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");
|
const { address } = c.get("jwtPayload");
|
||||||
// random raw email
|
const mailRow = requestedMailId !== undefined ? await c.env.DB.prepare(
|
||||||
const mailRow = await c.env.DB.prepare(
|
`SELECT * FROM raw_mails WHERE id = ? AND address = ?`
|
||||||
|
).bind(requestedMailId, address).first<RawMailRow>() : await c.env.DB.prepare(
|
||||||
`SELECT * FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1`
|
`SELECT * FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1`
|
||||||
).bind(address).first<RawMailRow>();
|
).bind(address).first<RawMailRow>();
|
||||||
const mailId = mailRow?.id;
|
const mailId = mailRow?.id;
|
||||||
|
if (requestedMailId !== undefined && !mailRow) {
|
||||||
|
return c.text(msgs.MailNotFoundMsg, 404);
|
||||||
|
}
|
||||||
const raw = mailRow ? await resolveRawEmail(mailRow) : "";
|
const raw = mailRow ? await resolveRawEmail(mailRow) : "";
|
||||||
const parsedEmailContext: ParsedEmailContext = { rawEmail: raw };
|
const parsedEmailContext: ParsedEmailContext = { rawEmail: raw };
|
||||||
const parsedEmail = await commonParseMail(parsedEmailContext);
|
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||||
|
|||||||
Reference in New Issue
Block a user