mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-09 01:17:02 +08:00
feat: add AI extract webhook placeholders
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- feat: |Telegram| Telegram 新邮件推送与 `/mails` 历史邮件查看支持展示 AI 提取结果,包含验证码、验证链接、服务链接、订阅链接等关键信息
|
- feat: |Telegram| Telegram 新邮件推送与 `/mails` 历史邮件查看支持展示 AI 提取结果,包含验证码、验证链接、服务链接、订阅链接等关键信息
|
||||||
|
- feat: |Webhook| 邮件 Webhook 模板支持填充 AI 提取结果占位符,包括 `aiExtractType`、`aiExtractResult`、`aiExtractResultText`
|
||||||
- feat: |Frontend| 新增 `DISABLE_SHOW_GITHUB_FOR_USER` 配置,可仅对普通用户隐藏 Header 的 GitHub/版本入口,admin 仍可见(issue #1041)
|
- feat: |Frontend| 新增 `DISABLE_SHOW_GITHUB_FOR_USER` 配置,可仅对普通用户隐藏 Header 的 GitHub/版本入口,admin 仍可见(issue #1041)
|
||||||
- feat: |Frontend| 将邮箱地址凭证弹窗升级为“地址凭证与连接方式”,复用普通用户与 admin 创建邮箱结果弹窗;支持通过 `ENABLE_AGENT_EMAIL_INFO` 展示 AI Agent 接入信息,并通过 `SMTP_IMAP_PROXY_CONFIG` 展示 SMTP/IMAP 客户端连接信息
|
- feat: |Frontend| 将邮箱地址凭证弹窗升级为“地址凭证与连接方式”,复用普通用户与 admin 创建邮箱结果弹窗;支持通过 `ENABLE_AGENT_EMAIL_INFO` 展示 AI Agent 接入信息,并通过 `SMTP_IMAP_PROXY_CONFIG` 展示 SMTP/IMAP 客户端连接信息
|
||||||
- docs: |随机子域名| 在前端“启用随机子域名”提示与 `subdomain` / `worker-vars` 文档(中英)中明确说明:要让 `name@<随机>.abc.com` 真正收到邮件,必须在基础域名 DNS 中为 `*` 子域添加通配 MX 记录,Email Routing 子域不继承父域配置(issue #1035)
|
- docs: |随机子域名| 在前端“启用随机子域名”提示与 `subdomain` / `worker-vars` 文档(中英)中明确说明:要让 `name@<随机>.abc.com` 真正收到邮件,必须在基础域名 DNS 中为 `*` 子域添加通配 MX 记录,Email Routing 子域不继承父域配置(issue #1035)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- feat: |Telegram| Show AI extraction results in Telegram new-mail notifications and `/mails` history views, including verification codes, auth links, service links, and subscription links
|
- feat: |Telegram| Show AI extraction results in Telegram new-mail notifications and `/mails` history views, including verification codes, auth links, service links, and subscription links
|
||||||
|
- feat: |Webhook| Support AI extraction placeholders in mail webhook templates, including `aiExtractType`, `aiExtractResult`, and `aiExtractResultText`
|
||||||
- feat: |Frontend| Add `DISABLE_SHOW_GITHUB_FOR_USER` to hide the Header GitHub/version entry from normal users while keeping it visible to admin users (issue #1041)
|
- feat: |Frontend| Add `DISABLE_SHOW_GITHUB_FOR_USER` to hide the Header GitHub/version entry from normal users while keeping it visible to admin users (issue #1041)
|
||||||
- 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`
|
- 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`
|
||||||
- docs: |Random Subdomain| Clarify in the "Use Random Subdomain" frontend tip and the `subdomain` / `worker-vars` docs (zh & en) that receiving mail on `name@<random>.abc.com` requires a wildcard `*` MX record under the base domain in DNS, because Cloudflare Email Routing does not inherit the apex configuration onto subdomains (issue #1035)
|
- docs: |Random Subdomain| Clarify in the "Use Random Subdomain" frontend tip and the `subdomain` / `worker-vars` docs (zh & en) that receiving mail on `name@<random>.abc.com` requires a wildcard `*` MX record under the base domain in DNS, because Cloudflare Email Routing does not inherit the apex configuration onto subdomains (issue #1035)
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ test.describe('Webhook — triggered on incoming mail', () => {
|
|||||||
from: '${from}',
|
from: '${from}',
|
||||||
to: '${to}',
|
to: '${to}',
|
||||||
subject: '${subject}',
|
subject: '${subject}',
|
||||||
|
aiExtractType: '${aiExtractType}',
|
||||||
|
aiExtractResult: '${aiExtractResult}',
|
||||||
|
aiExtractResultText: '${aiExtractResultText}',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -93,7 +96,16 @@ test.describe('Webhook — triggered on incoming mail', () => {
|
|||||||
].join('\r\n');
|
].join('\r\n');
|
||||||
|
|
||||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||||
data: { from, to: address, raw },
|
data: {
|
||||||
|
from,
|
||||||
|
to: address,
|
||||||
|
raw,
|
||||||
|
ai_extract_result: {
|
||||||
|
type: 'auth_code',
|
||||||
|
result: '654321',
|
||||||
|
result_text: 'Login verification code',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(res.ok()).toBe(true);
|
expect(res.ok()).toBe(true);
|
||||||
|
|
||||||
@@ -106,6 +118,9 @@ test.describe('Webhook — triggered on incoming mail', () => {
|
|||||||
expect(payload.from).toContain('webhook-sender@test.example.com');
|
expect(payload.from).toContain('webhook-sender@test.example.com');
|
||||||
expect(payload.to).toBe(address);
|
expect(payload.to).toBe(address);
|
||||||
expect(payload.subject).toBe(subject);
|
expect(payload.subject).toBe(subject);
|
||||||
|
expect(payload.aiExtractType).toBe('auth_code');
|
||||||
|
expect(payload.aiExtractResult).toBe('654321');
|
||||||
|
expect(payload.aiExtractResultText).toBe('Login verification code');
|
||||||
} finally {
|
} finally {
|
||||||
server.close();
|
server.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,5 +111,10 @@ To get the url, you need to configure the worker's `FRONTEND_URL` to your fronte
|
|||||||
"raw": "${raw}",
|
"raw": "${raw}",
|
||||||
"parsedText": "${parsedText}",
|
"parsedText": "${parsedText}",
|
||||||
"parsedHtml": "${parsedHtml}",
|
"parsedHtml": "${parsedHtml}",
|
||||||
|
"aiExtractType": "${aiExtractType}",
|
||||||
|
"aiExtractResult": "${aiExtractResult}",
|
||||||
|
"aiExtractResultText": "${aiExtractResultText}",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -111,5 +111,10 @@
|
|||||||
"raw": "${raw}",
|
"raw": "${raw}",
|
||||||
"parsedText": "${parsedText}",
|
"parsedText": "${parsedText}",
|
||||||
"parsedHtml": "${parsedHtml}",
|
"parsedHtml": "${parsedHtml}",
|
||||||
|
"aiExtractType": "${aiExtractType}",
|
||||||
|
"aiExtractResult": "${aiExtractResult}",
|
||||||
|
"aiExtractResultText": "${aiExtractResultText}",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
启用 AI 邮件内容提取后,Webhook 模板可使用 `aiExtractType`、`aiExtractResult`、`aiExtractResultText` 占位符。未提取到结果时这些字段为空字符串。
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response
|
|||||||
subject: parsedEmail?.subject || "test subject",
|
subject: parsedEmail?.subject || "test subject",
|
||||||
raw: raw || "test raw email",
|
raw: raw || "test raw email",
|
||||||
parsedText: parsedEmail?.text || "test parsed text",
|
parsedText: parsedEmail?.text || "test parsed text",
|
||||||
parsedHtml: parsedEmail?.html || "test parsed html"
|
parsedHtml: parsedEmail?.html || "test parsed html",
|
||||||
|
aiExtract: null,
|
||||||
|
aiExtractType: "",
|
||||||
|
aiExtractResult: "",
|
||||||
|
aiExtractResultText: ""
|
||||||
});
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return c.text(res.message || "send webhook error", 400);
|
return c.text(res.message || "send webhook error", 400);
|
||||||
|
|||||||
+10
-2
@@ -5,7 +5,7 @@ import { WorkerMailerOptions } from 'worker-mailer';
|
|||||||
import { getBooleanValue, getDomains, getStringArray, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains, getDomainMapValue, normalizeDomains, trimLower } from './utils';
|
import { getBooleanValue, getDomains, getStringArray, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains, getDomainMapValue, normalizeDomains, trimLower } from './utils';
|
||||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||||
import { CONSTANTS } from './constants';
|
import { CONSTANTS } from './constants';
|
||||||
import { AddressCreationSettings, AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
import { AddressCreationSettings, AdminWebhookSettings, ExtractResult, WebhookMail, WebhookSettings } from './models';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
|
|
||||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||||
@@ -810,7 +810,8 @@ export async function triggerWebhook(
|
|||||||
c: Context<HonoCustomType>,
|
c: Context<HonoCustomType>,
|
||||||
address: string,
|
address: string,
|
||||||
parsedEmailContext: ParsedEmailContext,
|
parsedEmailContext: ParsedEmailContext,
|
||||||
message_id: string | null
|
message_id: string | null,
|
||||||
|
aiExtract?: ExtractResult | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) {
|
if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) {
|
||||||
return
|
return
|
||||||
@@ -843,6 +844,9 @@ export async function triggerWebhook(
|
|||||||
).bind(address, message_id).first<string>("id");
|
).bind(address, message_id).first<string>("id");
|
||||||
|
|
||||||
const parsedEmail = await commonParseMail(parsedEmailContext);
|
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||||
|
const usableAiExtract = aiExtract?.type !== "none" && aiExtract?.result
|
||||||
|
? aiExtract
|
||||||
|
: null;
|
||||||
const webhookMail = {
|
const webhookMail = {
|
||||||
id: mailId || "",
|
id: mailId || "",
|
||||||
url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "",
|
url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "",
|
||||||
@@ -852,6 +856,10 @@ export async function triggerWebhook(
|
|||||||
raw: parsedEmailContext.rawEmail || "",
|
raw: parsedEmailContext.rawEmail || "",
|
||||||
parsedText: parsedEmail?.text || "",
|
parsedText: parsedEmail?.text || "",
|
||||||
parsedHtml: parsedEmail?.html || "",
|
parsedHtml: parsedEmail?.html || "",
|
||||||
|
aiExtract: usableAiExtract,
|
||||||
|
aiExtractType: usableAiExtract?.type || "",
|
||||||
|
aiExtractResult: usableAiExtract?.result || "",
|
||||||
|
aiExtractResultText: usableAiExtract?.result_text || "",
|
||||||
}
|
}
|
||||||
for (const settings of webhookList) {
|
for (const settings of webhookList) {
|
||||||
const res = await sendWebhook(settings, webhookMail);
|
const res = await sendWebhook(settings, webhookMail);
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
|||||||
try {
|
try {
|
||||||
await triggerWebhook(
|
await triggerWebhook(
|
||||||
{ env: env } as Context<HonoCustomType>,
|
{ env: env } as Context<HonoCustomType>,
|
||||||
toAddress, parsedEmailContext, message_id
|
toAddress, parsedEmailContext, message_id, aiExtractResult
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("send webhook error", error);
|
console.error("send webhook error", error);
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response
|
|||||||
subject: parsedEmail?.subject || "test subject",
|
subject: parsedEmail?.subject || "test subject",
|
||||||
raw: raw || "test raw email",
|
raw: raw || "test raw email",
|
||||||
parsedText: parsedEmail?.text || "test parsed text",
|
parsedText: parsedEmail?.text || "test parsed text",
|
||||||
parsedHtml: parsedEmail?.html || "test parsed html"
|
parsedHtml: parsedEmail?.html || "test parsed html",
|
||||||
|
aiExtract: null,
|
||||||
|
aiExtractType: "",
|
||||||
|
aiExtractResult: "",
|
||||||
|
aiExtractResultText: ""
|
||||||
});
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
return c.text(res.message || "send webhook error", 400);
|
return c.text(res.message || "send webhook error", 400);
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export type WebhookMail = {
|
|||||||
raw: string;
|
raw: string;
|
||||||
parsedText: string;
|
parsedText: string;
|
||||||
parsedHtml: string;
|
parsedHtml: string;
|
||||||
|
aiExtract: ExtractResult | null;
|
||||||
|
aiExtractType: string;
|
||||||
|
aiExtractResult: string;
|
||||||
|
aiExtractResultText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomSqlCleanup = {
|
export type CustomSqlCleanup = {
|
||||||
@@ -156,6 +160,9 @@ export class WebhookSettings {
|
|||||||
"raw": "${raw}",
|
"raw": "${raw}",
|
||||||
"parsedText": "${parsedText}",
|
"parsedText": "${parsedText}",
|
||||||
"parsedHtml": "${parsedHtml}",
|
"parsedHtml": "${parsedHtml}",
|
||||||
|
"aiExtractType": "${aiExtractType}",
|
||||||
|
"aiExtractResult": "${aiExtractResult}",
|
||||||
|
"aiExtractResultText": "${aiExtractResultText}",
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user