mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 07:27:27 +08:00
feat: add AI email extraction with Cloudflare Workers AI
Add AI-powered email content extraction feature using Cloudflare Workers AI to automatically identify and extract important information from emails including verification codes, authentication links, service links, and subscription links. Features: - AI extraction with priority-based logic (auth_code > auth_link > service_link > subscription_link > other_link) - Admin allowlist configuration with wildcard support (*@example.com) - Frontend display in both email list (compact) and detail view (full mode) - Bilingual documentation (Chinese/English) - Database migration: add metadata field to raw_mails (v0.0.3 -> v0.0.4) Technical highlights: - Proper regex escaping for wildcard pattern matching - Content truncation to avoid AI token limits - Error handling that won't affect email receiving - JSON schema validation for AI responses - Type-safe TypeScript implementation - Vue I18n support with special character escaping References: - Inspired by Alle Project: https://github.com/bestruirui/Alle - Uses Cloudflare Workers AI JSON Mode 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
-- Add metadata column to raw_mails table for storing AI extraction results and other metadata
|
||||||
|
-- This column stores JSON data with flexible schema for various analysis results
|
||||||
|
|
||||||
|
ALTER TABLE raw_mails ADD COLUMN metadata TEXT;
|
||||||
@@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
source TEXT,
|
source TEXT,
|
||||||
address TEXT,
|
address TEXT,
|
||||||
raw TEXT,
|
raw TEXT,
|
||||||
|
metadata TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,12 @@
|
|||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mail-parser-wasm": "^0.2.1",
|
"mail-parser-wasm": "^0.2.1",
|
||||||
"naive-ui": "^2.43.1",
|
"naive-ui": "^2.43.2",
|
||||||
"postal-mime": "^2.6.0",
|
"postal-mime": "^2.6.1",
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.25",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -44,12 +44,12 @@
|
|||||||
"unplugin-auto-import": "^19.3.0",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vite": "^6.4.1",
|
"vite": "^6.4.1",
|
||||||
"vite-plugin-pwa": "^1.1.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vite-plugin-top-level-await": "^1.6.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
"vite-plugin-wasm": "^3.5.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"workbox-build": "^7.3.0",
|
"workbox-build": "^7.4.0",
|
||||||
"workbox-window": "^7.3.0",
|
"workbox-window": "^7.4.0",
|
||||||
"wrangler": "^4.47.0"
|
"wrangler": "^4.53.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+823
-633
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ContentCopyOutlined, LinkRound, CodeRound } from '@vicons/material';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
authCode: 'Verification Code',
|
||||||
|
authLink: 'Authentication Link',
|
||||||
|
serviceLink: 'Service Link',
|
||||||
|
subscriptionLink: 'Subscription Link',
|
||||||
|
otherLink: 'Other Link',
|
||||||
|
copySuccess: 'Copied successfully',
|
||||||
|
copyFailed: 'Copy failed',
|
||||||
|
open: 'Open',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
authCode: '验证码',
|
||||||
|
authLink: '认证链接',
|
||||||
|
serviceLink: '服务链接',
|
||||||
|
subscriptionLink: '订阅链接',
|
||||||
|
otherLink: '其他链接',
|
||||||
|
copySuccess: '复制成功',
|
||||||
|
copyFailed: '复制失败',
|
||||||
|
open: '打开',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
metadata: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
compact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const aiExtract = computed(() => {
|
||||||
|
if (!props.metadata) return null;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(props.metadata);
|
||||||
|
return data.ai_extract || null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeLabel = computed(() => {
|
||||||
|
if (!aiExtract.value) return '';
|
||||||
|
const typeMap = {
|
||||||
|
auth_code: t('authCode'),
|
||||||
|
auth_link: t('authLink'),
|
||||||
|
service_link: t('serviceLink'),
|
||||||
|
subscription_link: t('subscriptionLink'),
|
||||||
|
other_link: t('otherLink'),
|
||||||
|
};
|
||||||
|
return typeMap[aiExtract.value.type] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeIcon = computed(() => {
|
||||||
|
if (!aiExtract.value) return null;
|
||||||
|
const iconMap = {
|
||||||
|
auth_code: CodeRound,
|
||||||
|
auth_link: LinkRound,
|
||||||
|
service_link: LinkRound,
|
||||||
|
subscription_link: LinkRound,
|
||||||
|
other_link: LinkRound,
|
||||||
|
};
|
||||||
|
return iconMap[aiExtract.value.type] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLink = computed(() => {
|
||||||
|
return aiExtract.value && aiExtract.value.type !== 'auth_code';
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayText = computed(() => {
|
||||||
|
if (!aiExtract.value) return '';
|
||||||
|
return aiExtract.value.result_text || aiExtract.value.result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(aiExtract.value.result);
|
||||||
|
message.success(t('copySuccess'));
|
||||||
|
} catch (e) {
|
||||||
|
message.error(t('copyFailed'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLink = () => {
|
||||||
|
if (isLink.value && aiExtract.value.result) {
|
||||||
|
window.open(aiExtract.value.result, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="aiExtract && aiExtract.result" class="ai-extract-info">
|
||||||
|
<n-alert v-if="!compact" type="success" closable>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="typeIcon" />
|
||||||
|
</template>
|
||||||
|
<template #header>
|
||||||
|
{{ typeLabel }}
|
||||||
|
</template>
|
||||||
|
<n-space align="center">
|
||||||
|
<n-text v-if="aiExtract.type === 'auth_code'" strong style="font-size: 18px; font-family: monospace;">
|
||||||
|
{{ aiExtract.result }}
|
||||||
|
</n-text>
|
||||||
|
<n-ellipsis v-else style="max-width: 400px;">
|
||||||
|
{{ displayText }}
|
||||||
|
</n-ellipsis>
|
||||||
|
<n-button size="small" @click="copyToClipboard" tertiary>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="ContentCopyOutlined" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="isLink" size="small" @click="openLink" tertiary type="primary">
|
||||||
|
{{ t('open') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-alert>
|
||||||
|
<n-tag v-else type="success" @click="copyToClipboard" style="cursor: pointer;" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="typeIcon" />
|
||||||
|
</template>
|
||||||
|
<n-ellipsis style="max-width: 150px;">
|
||||||
|
{{ typeLabel }}: {{ displayText }}
|
||||||
|
</n-ellipsis>
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ai-extract-info {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,7 @@ import { useIsMobile } from '../utils/composables'
|
|||||||
import { processItem } from '../utils/email-parser'
|
import { processItem } from '../utils/email-parser'
|
||||||
import { utcToLocalDate } from '../utils';
|
import { utcToLocalDate } from '../utils';
|
||||||
import MailContentRenderer from "./MailContentRenderer.vue";
|
import MailContentRenderer from "./MailContentRenderer.vue";
|
||||||
|
import AiExtractInfo from "./AiExtractInfo.vue";
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -439,6 +440,7 @@ onBeforeUnmount(() => {
|
|||||||
TO: {{ row.address }}
|
TO: {{ row.address }}
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
<AiExtractInfo :metadata="row.metadata" compact />
|
||||||
</template>
|
</template>
|
||||||
</n-thing>
|
</n-thing>
|
||||||
</n-list-item>
|
</n-list-item>
|
||||||
@@ -513,6 +515,7 @@ onBeforeUnmount(() => {
|
|||||||
<n-tag v-if="showEMailTo" type="info">
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
TO: {{ row.address }}
|
TO: {{ row.address }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
<AiExtractInfo :metadata="row.metadata" compact />
|
||||||
</template>
|
</template>
|
||||||
</n-thing>
|
</n-thing>
|
||||||
</n-list-item>
|
</n-list-item>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref } from "vue";
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
||||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||||
|
import AiExtractInfo from "./AiExtractInfo.vue";
|
||||||
import { getDownloadEmlUrl } from '../utils/email-parser';
|
import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||||
import { utcToLocalDate } from '../utils';
|
import { utcToLocalDate } from '../utils';
|
||||||
import { useGlobalState } from '../store';
|
import { useGlobalState } from '../store';
|
||||||
@@ -179,6 +180,9 @@ const handleSaveToS3 = async (filename, blob) => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
|
||||||
|
<!-- AI 提取信息 -->
|
||||||
|
<AiExtractInfo :metadata="mail.metadata" />
|
||||||
|
|
||||||
<!-- 邮件内容 -->
|
<!-- 邮件内容 -->
|
||||||
<div class="mail-content">
|
<div class="mail-content">
|
||||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import Webhook from './admin/Webhook.vue';
|
|||||||
import MailWebhook from './admin/MailWebhook.vue';
|
import MailWebhook from './admin/MailWebhook.vue';
|
||||||
import WorkerConfig from './admin/WorkerConfig.vue';
|
import WorkerConfig from './admin/WorkerConfig.vue';
|
||||||
import IpBlacklistSettings from './admin/IpBlacklistSettings.vue';
|
import IpBlacklistSettings from './admin/IpBlacklistSettings.vue';
|
||||||
|
import AiExtractSettings from './admin/AiExtractSettings.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
adminAuth, showAdminAuth, adminTab, loading,
|
adminAuth, showAdminAuth, adminTab, loading,
|
||||||
@@ -74,6 +75,7 @@ const { t } = useI18n({
|
|||||||
database: 'Database',
|
database: 'Database',
|
||||||
workerconfig: 'Worker Config',
|
workerconfig: 'Worker Config',
|
||||||
ipBlacklistSettings: 'IP Blacklist',
|
ipBlacklistSettings: 'IP Blacklist',
|
||||||
|
aiExtractSettings: 'AI Extract Settings',
|
||||||
appearance: 'Appearance',
|
appearance: 'Appearance',
|
||||||
about: 'About',
|
about: 'About',
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
@@ -103,6 +105,7 @@ const { t } = useI18n({
|
|||||||
database: '数据库',
|
database: '数据库',
|
||||||
workerconfig: 'Worker 配置',
|
workerconfig: 'Worker 配置',
|
||||||
ipBlacklistSettings: 'IP 黑名单',
|
ipBlacklistSettings: 'IP 黑名单',
|
||||||
|
aiExtractSettings: 'AI 提取设置',
|
||||||
appearance: '外观',
|
appearance: '外观',
|
||||||
about: '关于',
|
about: '关于',
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
@@ -166,6 +169,9 @@ onMounted(async () => {
|
|||||||
<n-tab-pane name="ipBlacklistSettings" :tab="t('ipBlacklistSettings')">
|
<n-tab-pane name="ipBlacklistSettings" :tab="t('ipBlacklistSettings')">
|
||||||
<IpBlacklistSettings />
|
<IpBlacklistSettings />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="aiExtractSettings" :tab="t('aiExtractSettings')">
|
||||||
|
<AiExtractSettings />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="webhook" :tab="t('webhookSettings')">
|
<n-tab-pane name="webhook" :tab="t('webhookSettings')">
|
||||||
<Webhook />
|
<Webhook />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
// @ts-ignore
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
title: 'AI Email Extraction Settings',
|
||||||
|
successTip: 'Success',
|
||||||
|
save: 'Save',
|
||||||
|
enableAllowList: 'Enable Address Allowlist',
|
||||||
|
enableAllowListTip: 'When enabled, AI extraction will only process emails sent to addresses in the allowlist',
|
||||||
|
allowList: 'Address Allowlist (Enter address and press Enter, wildcards supported)',
|
||||||
|
allowListTip: "Wildcard * matches any characters, e.g. *{'@'}example.com matches all addresses under example.com domain",
|
||||||
|
manualInputPrompt: 'Type and press Enter to add',
|
||||||
|
disabledTip: 'When disabled, AI extraction will process all email addresses',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
title: 'AI 邮件提取设置',
|
||||||
|
successTip: '成功',
|
||||||
|
save: '保存',
|
||||||
|
enableAllowList: '启用地址白名单',
|
||||||
|
enableAllowListTip: '启用后,AI 提取功能仅对白名单中的邮箱地址生效',
|
||||||
|
allowList: '地址白名单 (请输入地址并回车,支持通配符)',
|
||||||
|
allowListTip: "通配符 * 可匹配任意字符,如 *{'@'}example.com 可匹配 example.com 域名下的所有地址",
|
||||||
|
manualInputPrompt: '输入后按回车键添加',
|
||||||
|
disabledTip: '未启用时,所有邮箱地址都可使用 AI 提取功能',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type AiExtractSettings = {
|
||||||
|
enableAllowList: boolean
|
||||||
|
allowList: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = ref<AiExtractSettings>({
|
||||||
|
enableAllowList: false,
|
||||||
|
allowList: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.fetch(`/admin/ai_extract/settings`) as AiExtractSettings
|
||||||
|
Object.assign(settings.value, res)
|
||||||
|
} catch (error) {
|
||||||
|
message.error((error as Error).message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/admin/ai_extract/settings`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(settings.value),
|
||||||
|
})
|
||||||
|
message.success(t('successTip'))
|
||||||
|
} catch (error) {
|
||||||
|
message.error((error as Error).message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<n-card :title="t('title')" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="saveSettings" type="primary">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
|
||||||
|
<n-form-item-row :label="t('enableAllowList')">
|
||||||
|
<n-switch v-model:value="settings.enableAllowList" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
|
|
||||||
|
<n-alert v-if="!settings.enableAllowList" type="info" style="margin-bottom: 16px;">
|
||||||
|
{{ t('disabledTip') }}
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<div v-if="settings.enableAllowList">
|
||||||
|
<n-alert type="warning" style="margin-bottom: 16px;">
|
||||||
|
{{ t('enableAllowListTip') }}
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<n-form-item-row :label="t('allowList')">
|
||||||
|
<n-select v-model:value="settings.allowList" filterable multiple tag
|
||||||
|
:placeholder="t('allowListTip')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
|
</n-form-item-row>
|
||||||
|
|
||||||
|
<n-text depth="3" style="font-size: 12px;">
|
||||||
|
{{ t('allowListTip') }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^4.47.0"
|
"wrangler": "^4.53.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
text: 'Additional Features',
|
text: 'Additional Features',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
|
{ text: 'AI Email Recognition', link: 'feature/ai-extract' },
|
||||||
{ text: 'Configure SMTP IMAP Proxy', link: 'feature/config-smtp-proxy' },
|
{ text: 'Configure SMTP IMAP Proxy', link: 'feature/config-smtp-proxy' },
|
||||||
{ text: 'Send Email API', link: 'feature/send-mail-api' },
|
{ text: 'Send Email API', link: 'feature/send-mail-api' },
|
||||||
{ text: 'View Email API', link: 'feature/mail-api' },
|
{ text: 'View Email API', link: 'feature/mail-api' },
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
text: '附加功能',
|
text: '附加功能',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
|
{ text: 'AI 邮件识别', link: 'feature/ai-extract' },
|
||||||
{ text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' },
|
{ text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' },
|
||||||
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
||||||
{ text: '查看邮件 API', link: 'feature/mail-api' },
|
{ text: '查看邮件 API', link: 'feature/mail-api' },
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# AI Email Recognition
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This feature is supported from version v1.1.0
|
||||||
|
>
|
||||||
|
> This feature is inspired by the [Alle project](https://github.com/bestruirui/Alle/blob/62e74629ded0c7966c12d4e1c54f0bcc2e54f12c/src/lib/email/extract.ts#L54)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The AI email recognition feature uses Cloudflare Workers AI to automatically analyze incoming email content and intelligently extract important information, including:
|
||||||
|
|
||||||
|
- **Verification Code** (auth_code) - OTP, security code, confirmation code, etc.
|
||||||
|
- **Authentication Link** (auth_link) - Login, verify, activate, password reset links
|
||||||
|
- **Service Link** (service_link) - GitHub, GitLab, deployment notifications and other service-related links
|
||||||
|
- **Subscription Link** (subscription_link) - Unsubscribe, manage subscription links
|
||||||
|
- **Other Link** (other_link) - Other valuable links
|
||||||
|
|
||||||
|
Extraction results are automatically saved to the `metadata` field in the database, and the frontend can directly display extracted verification codes or links.
|
||||||
|
|
||||||
|
## Configuration Variables
|
||||||
|
|
||||||
|
| Variable Name | Type | Description | Example |
|
||||||
|
| -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||||
|
| `ENABLE_AI_EMAIL_EXTRACT` | Text/JSON | Whether to enable AI email recognition feature | `true` |
|
||||||
|
| `AI_EXTRACT_MODEL` | Text | AI model name, choose from [models supporting JSON mode](https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models) | `@cf/meta/llama-3.1-8b-instruct` |
|
||||||
|
|
||||||
|
## Workers AI Binding
|
||||||
|
|
||||||
|
Configure Workers AI binding in `wrangler.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[ai]
|
||||||
|
binding = "AI"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add in Cloudflare Dashboard Worker settings:
|
||||||
|
- **Variable name**: `AI`
|
||||||
|
- **Type**: Workers AI
|
||||||
|
|
||||||
|
## Address Allowlist (Optional)
|
||||||
|
|
||||||
|
To control costs and resource usage, you can configure an address allowlist in the Admin console's **AI Extract Settings** page:
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- **Allowlist Disabled**: AI extraction will process all email addresses
|
||||||
|
- **Allowlist Enabled**: AI extraction will only process addresses in the allowlist
|
||||||
|
|
||||||
|
### Allowlist Format
|
||||||
|
|
||||||
|
One address per line, supporting wildcard `*` to match any characters:
|
||||||
|
|
||||||
|
- **Exact Match**: `user@example.com` - Only matches this specific email
|
||||||
|
- **Domain Wildcard**: `*@example.com` - Matches all emails under example.com domain
|
||||||
|
- **User Wildcard**: `admin*@example.com` - Matches emails starting with admin
|
||||||
|
- **Wildcard Anywhere**: `*test*@example.com` - Matches emails containing test
|
||||||
|
- **Multiple Wildcards**: `admin*@*.com` - Matches emails starting with admin under any .com domain
|
||||||
|
|
||||||
|
### Configuration Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
user@example.com
|
||||||
|
*@mydomain.com
|
||||||
|
admin*@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration will only perform AI extraction for:
|
||||||
|
- `user@example.com` (exact match)
|
||||||
|
- All emails under `@mydomain.com` (e.g., `test@mydomain.com`, `admin@mydomain.com`)
|
||||||
|
- All emails starting with `admin` under `@company.com` (e.g., `admin@company.com`, `admin123@company.com`)
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
## Required Variables
|
## Required Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| -------------------------- | ----------- | ------------------------------------------------------------ | ------------------------------------ |
|
| -------------------------- | ----------- | ---------------------------------------------------------------------- | ------------------------------------ |
|
||||||
| `DOMAINS` | JSON | All domains for temporary email, supports multiple domains | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
| `DOMAINS` | JSON | All domains for temporary email, supports multiple domains | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||||
| `JWT_SECRET` | Text/Secret | Secret key for generating JWT, used for login and authentication | `xxx` |
|
| `JWT_SECRET` | Text/Secret | Secret key for generating JWT, used for login and authentication | `xxx` |
|
||||||
| `ADMIN_PASSWORDS` | JSON | Admin console passwords, console access disabled if not configured | `["123", "456"]` |
|
| `ADMIN_PASSWORDS` | JSON | Admin console passwords, console access disabled if not configured | `["123", "456"]` |
|
||||||
@@ -16,14 +16,14 @@
|
|||||||
## Console Related Variables
|
## Console Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ------------------------------ | --------- | -------------------------------------------------- | ---------------- |
|
| ------------------------------ | --------- | ------------------------------------------------------- | ---------------- |
|
||||||
| `PASSWORDS` | JSON | Website private passwords, required after configuration | `["123", "456"]` |
|
| `PASSWORDS` | JSON | Website private passwords, required after configuration | `["123", "456"]` |
|
||||||
| `DISABLE_ADMIN_PASSWORD_CHECK` | Text/JSON | Warning: Admin console without password or user check | `false` |
|
| `DISABLE_ADMIN_PASSWORD_CHECK` | Text/JSON | Warning: Admin console without password or user check | `false` |
|
||||||
|
|
||||||
## Email Related Variables
|
## Email Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
| ------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
||||||
| `PREFIX` | Text | Default prefix for new `email address` names, can be left unconfigured if no prefix needed | `tmp` |
|
| `PREFIX` | Text | Default prefix for new `email address` names, can be left unconfigured if no prefix needed | `tmp` |
|
||||||
| `MIN_ADDRESS_LEN` | Number | Minimum length of `email address` name | `1` |
|
| `MIN_ADDRESS_LEN` | Number | Minimum length of `email address` name | `1` |
|
||||||
| `MAX_ADDRESS_LEN` | Number | Maximum length of `email address` name | `30` |
|
| `MAX_ADDRESS_LEN` | Number | Maximum length of `email address` name | `30` |
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
## Email Reception Related Variables
|
## Email Reception Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ------------------------------- | --------- | -------------------------------------------------------------------------------------- | -------------------------- |
|
| ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||||
| `BLACK_LIST` | Text | Blacklist for filtering senders, comma separated | `gov.cn,edu.cn` |
|
| `BLACK_LIST` | Text | Blacklist for filtering senders, comma separated | `gov.cn,edu.cn` |
|
||||||
| `ENABLE_CHECK_JUNK_MAIL` | Text/JSON | Whether to enable junk mail checking, used with the following two lists | `false` |
|
| `ENABLE_CHECK_JUNK_MAIL` | Text/JSON | Whether to enable junk mail checking, used with the following two lists | `false` |
|
||||||
| `JUNK_MAIL_CHECK_LIST` | JSON | Junk mail check configuration, marked as junk if any item `exists` and `fails` | `["spf", "dkim", "dmarc"]` |
|
| `JUNK_MAIL_CHECK_LIST` | JSON | Junk mail check configuration, marked as junk if any item `exists` and `fails` | `["spf", "dkim", "dmarc"]` |
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
## Webhook Related Variables
|
## Webhook Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ---------------- | --------- | -------------------------------------------- | ------------------ |
|
| ---------------- | --------- | ------------------------------------------------- | ------------------ |
|
||||||
| `ENABLE_WEBHOOK` | Text/JSON | Whether to enable webhook | `true` |
|
| `ENABLE_WEBHOOK` | Text/JSON | Whether to enable webhook | `true` |
|
||||||
| `FRONTEND_URL` | Text | Frontend URL, used for sending webhook email URLs | `https://xxxx.xxx` |
|
| `FRONTEND_URL` | Text | Frontend URL, used for sending webhook email URLs | `https://xxxx.xxx` |
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
## User Related Variables
|
## User Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ------------------------------------- | --------- | ---------------------------------------------------------------------------------------------- | ------- |
|
| ------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- | --------- |
|
||||||
| `USER_DEFAULT_ROLE` | Text | Default role for new users, only effective when email verification is enabled | `vip` |
|
| `USER_DEFAULT_ROLE` | Text | Default role for new users, only effective when email verification is enabled | `vip` |
|
||||||
| `ADMIN_USER_ROLE` | Text | Admin role configuration, if user role equals ADMIN_USER_ROLE, user can access admin console | `admin` |
|
| `ADMIN_USER_ROLE` | Text | Admin role configuration, if user role equals ADMIN_USER_ROLE, user can access admin console | `admin` |
|
||||||
| `USER_ROLES` | JSON | - | See below |
|
| `USER_ROLES` | JSON | - | See below |
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
## Web Related Variables
|
## Web Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| -------------------------- | ----------- | -------------------------------------------------------- | --------------------- |
|
| -------------------------- | ----------- | ------------------------------------------------------------------------ | --------------------- |
|
||||||
| `DEFAULT_LANG` | Text | Worker error message default language, zh/en | `zh` |
|
| `DEFAULT_LANG` | Text | Worker error message default language, zh/en | `zh` |
|
||||||
| `TITLE` | Text | Custom frontend page website title, supports html | `Custom Title` |
|
| `TITLE` | Text | Custom frontend page website title, supports html | `Custom Title` |
|
||||||
| `ANNOUNCEMENT` | Text | Custom frontend page announcement, supports html | `Custom Announcement` |
|
| `ANNOUNCEMENT` | Text | Custom frontend page announcement, supports html | `Custom Announcement` |
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
## Telegram Bot Related Variables
|
## Telegram Bot Related Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ---------------- | ------ | ---------------------------------------------------------------------------------------- | ------- |
|
| ---------------- | ------ | --------------------------------------------------------------------------- | ------- |
|
||||||
| `TG_MAX_ADDRESS` | Number | Maximum number of mailboxes that can be bound to telegram bot | `5` |
|
| `TG_MAX_ADDRESS` | Number | Maximum number of mailboxes that can be bound to telegram bot | `5` |
|
||||||
| `TG_BOT_INFO` | Text | Optional, telegram BOT_INFO, predefined BOT_INFO can reduce webhook latency | `{}` |
|
| `TG_BOT_INFO` | Text | Optional, telegram BOT_INFO, predefined BOT_INFO can reduce webhook latency | `{}` |
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
## Other Variables
|
## Other Variables
|
||||||
|
|
||||||
| Variable Name | Type | Description | Example |
|
| Variable Name | Type | Description | Example |
|
||||||
| ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
| ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
|
||||||
| `ENABLE_ANOTHER_WORKER` | Text/JSON | Whether to enable other workers to process emails | `false` |
|
| `ENABLE_ANOTHER_WORKER` | Text/JSON | Whether to enable other workers to process emails | `false` |
|
||||||
| `ANOTHER_WORKER_LIST` | JSON | - Configuration for other workers to process emails, multiple workers can be configured <br/> - Filter by keywords, call the bound worker's method (default method name is rpcEmail)<br/> - keywords are required, otherwise the worker will not be triggered | See below |
|
| `ANOTHER_WORKER_LIST` | JSON | - Configuration for other workers to process emails, multiple workers can be configured <br/> - Filter by keywords, call the bound worker's method (default method name is rpcEmail)<br/> - keywords are required, otherwise the worker will not be triggered | See below |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# AI 邮件识别
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 此功能从 v1.1.0 版本开始支持
|
||||||
|
>
|
||||||
|
> 本功能参考自 [Alle 项目](https://github.com/bestruirui/Alle/blob/62e74629ded0c7966c12d4e1c54f0bcc2e54f12c/src/lib/email/extract.ts#L54)
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
AI 邮件识别功能使用 Cloudflare Workers AI 自动分析收到的邮件内容,智能提取重要信息,包括:
|
||||||
|
|
||||||
|
- **验证码** (auth_code) - OTP、安全码、确认码等
|
||||||
|
- **认证链接** (auth_link) - 登录、验证、激活、重置密码链接
|
||||||
|
- **服务链接** (service_link) - GitHub、GitLab、部署通知等服务相关链接
|
||||||
|
- **订阅管理链接** (subscription_link) - 退订、管理订阅等链接
|
||||||
|
- **其他链接** (other_link) - 其他有价值的链接
|
||||||
|
|
||||||
|
提取结果会自动保存到数据库的 `metadata` 字段中,前端可以直接展示提取的验证码或链接。
|
||||||
|
|
||||||
|
## 配置变量
|
||||||
|
|
||||||
|
| 变量名 | 类型 | 说明 | 示例 |
|
||||||
|
| ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
|
||||||
|
| `ENABLE_AI_EMAIL_EXTRACT` | 文本/JSON | 是否启用 AI 邮件识别功能 | `true` |
|
||||||
|
| `AI_EXTRACT_MODEL` | 文本 | AI 模型名称,从[支持 JSON 模式的模型](https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models)中选择 | `@cf/meta/llama-3.1-8b-instruct` |
|
||||||
|
|
||||||
|
## Workers AI 绑定
|
||||||
|
|
||||||
|
需要在 `wrangler.toml` 中配置 Workers AI 绑定:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[ai]
|
||||||
|
binding = "AI"
|
||||||
|
```
|
||||||
|
|
||||||
|
或在 Cloudflare Dashboard 的 Worker 设置中添加:
|
||||||
|
- **Variable name**: `AI`
|
||||||
|
- **Type**: Workers AI
|
||||||
|
|
||||||
|
## 地址白名单(可选)
|
||||||
|
|
||||||
|
为了控制成本和资源使用,可以在 Admin 控制台的 **AI 提取设置** 页面配置地址白名单:
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
- **未启用白名单**:所有邮箱地址都可使用 AI 提取功能
|
||||||
|
- **启用白名单**:仅白名单中的邮箱地址会进行 AI 提取
|
||||||
|
|
||||||
|
### 白名单格式
|
||||||
|
|
||||||
|
每行一个地址,支持通配符 `*` 匹配任意字符:
|
||||||
|
|
||||||
|
- **精确匹配**:`user@example.com` - 仅匹配该邮箱
|
||||||
|
- **域名通配符**:`*@example.com` - 匹配 example.com 域名下的所有邮箱
|
||||||
|
- **用户通配符**:`admin*@example.com` - 匹配 admin 开头的邮箱
|
||||||
|
- **任意位置通配符**:`*test*@example.com` - 匹配包含 test 的邮箱
|
||||||
|
- **多个通配符**:`admin*@*.com` - 匹配所有 .com 域名下 admin 开头的邮箱
|
||||||
|
|
||||||
|
### 配置示例
|
||||||
|
|
||||||
|
```text
|
||||||
|
user@example.com
|
||||||
|
*@mydomain.com
|
||||||
|
admin*@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
此配置将只对以下邮箱进行 AI 提取:
|
||||||
|
- `user@example.com`(精确匹配)
|
||||||
|
- 所有 `@mydomain.com` 的邮箱(如 `test@mydomain.com`、`admin@mydomain.com`)
|
||||||
|
- 所有 `admin` 开头的 `@company.com` 邮箱(如 `admin@company.com`、`admin123@company.com`)
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"vitepress": "^1.6.4",
|
"vitepress": "^1.6.4",
|
||||||
"wrangler": "^4.47.0"
|
"wrangler": "^4.53.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
|
|||||||
Generated
+481
-471
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -11,26 +11,26 @@
|
|||||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20251111.0",
|
"@cloudflare/workers-types": "^4.20251205.0",
|
||||||
"@eslint/js": "9.18.0",
|
"@eslint/js": "9.18.0",
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"@types/node": "^22.19.1",
|
"@types/node": "^22.19.1",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.18.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.48.1",
|
||||||
"wrangler": "^4.47.0"
|
"wrangler": "^4.53.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.888.0",
|
"@aws-sdk/client-s3": "3.888.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.888.0",
|
"@aws-sdk/s3-request-presigner": "3.888.0",
|
||||||
"@simplewebauthn/server": "10.0.1",
|
"@simplewebauthn/server": "10.0.1",
|
||||||
"hono": "^4.10.5",
|
"hono": "^4.10.7",
|
||||||
"jsonpath-plus": "^10.3.0",
|
"jsonpath-plus": "^10.3.0",
|
||||||
"mimetext": "^3.0.27",
|
"mimetext": "^3.0.27",
|
||||||
"postal-mime": "^2.6.0",
|
"postal-mime": "^2.6.1",
|
||||||
"resend": "^4.8.0",
|
"resend": "^4.8.0",
|
||||||
"telegraf": "4.16.3",
|
"telegraf": "4.16.3",
|
||||||
"worker-mailer": "^1.2.0"
|
"worker-mailer": "^1.2.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
|||||||
Generated
+375
-458
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
import { Context } from "hono";
|
||||||
|
import { CONSTANTS } from "../constants";
|
||||||
|
import { getJsonSetting, saveSetting } from "../utils";
|
||||||
|
|
||||||
|
export type AiExtractSettings = {
|
||||||
|
enableAllowList: boolean;
|
||||||
|
allowList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAiExtractSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
|
const settings = await getJsonSetting<AiExtractSettings>(c, CONSTANTS.AI_EXTRACT_SETTINGS_KEY) || {
|
||||||
|
enableAllowList: false,
|
||||||
|
allowList: []
|
||||||
|
};
|
||||||
|
return c.json(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAiExtractSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||||
|
const settings = await c.req.json<AiExtractSettings>();
|
||||||
|
await saveSetting(c, CONSTANTS.AI_EXTRACT_SETTINGS_KEY, JSON.stringify(settings));
|
||||||
|
return c.json({ success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAiExtractSettings,
|
||||||
|
saveAiExtractSettings,
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
source TEXT,
|
source TEXT,
|
||||||
address TEXT,
|
address TEXT,
|
||||||
raw TEXT,
|
raw TEXT,
|
||||||
|
metadata TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,8 +143,11 @@ export default {
|
|||||||
const query = `ALTER TABLE address ADD password TEXT;`
|
const query = `ALTER TABLE address ADD password TEXT;`
|
||||||
await c.env.DB.exec(query);
|
await c.env.DB.exec(query);
|
||||||
}
|
}
|
||||||
|
if (version == "v0.0.3") {
|
||||||
|
// migration from v0.0.3 to v0.0.4
|
||||||
|
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN metadata TEXT;`);
|
||||||
|
}
|
||||||
if (version != CONSTANTS.DB_VERSION) {
|
if (version != CONSTANTS.DB_VERSION) {
|
||||||
// TODO: Perform migration logic here
|
|
||||||
// remove all \r and \n characters from the query string
|
// remove all \r and \n characters from the query string
|
||||||
// split by ; and join with a ;\n
|
// split by ; and join with a ;\n
|
||||||
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import admin_mail_api from './admin_mail_api'
|
|||||||
import { sendMailbyAdmin } from './send_mail'
|
import { sendMailbyAdmin } from './send_mail'
|
||||||
import db_api from './db_api'
|
import db_api from './db_api'
|
||||||
import ip_blacklist_settings from './ip_blacklist_settings'
|
import ip_blacklist_settings from './ip_blacklist_settings'
|
||||||
|
import ai_extract_settings from './ai_extract_settings'
|
||||||
import { EmailRuleSettings } from '../models'
|
import { EmailRuleSettings } from '../models'
|
||||||
|
|
||||||
export const api = new Hono<HonoCustomType>()
|
export const api = new Hono<HonoCustomType>()
|
||||||
@@ -377,3 +378,7 @@ api.post('admin/db_migration', db_api.migrate);
|
|||||||
// IP blacklist settings
|
// IP blacklist settings
|
||||||
api.get("/admin/ip_blacklist/settings", ip_blacklist_settings.getIpBlacklistSettings);
|
api.get("/admin/ip_blacklist/settings", ip_blacklist_settings.getIpBlacklistSettings);
|
||||||
api.post("/admin/ip_blacklist/settings", ip_blacklist_settings.saveIpBlacklistSettings);
|
api.post("/admin/ip_blacklist/settings", ip_blacklist_settings.saveIpBlacklistSettings);
|
||||||
|
|
||||||
|
// AI extract settings
|
||||||
|
api.get("/admin/ai_extract/settings", ai_extract_settings.getAiExtractSettings);
|
||||||
|
api.post("/admin/ai_extract/settings", ai_extract_settings.saveAiExtractSettings);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const CONSTANTS = {
|
|||||||
|
|
||||||
// DB Version
|
// DB Version
|
||||||
DB_VERSION_KEY: 'db_version',
|
DB_VERSION_KEY: 'db_version',
|
||||||
DB_VERSION: "v0.0.3",
|
DB_VERSION: "v0.0.4",
|
||||||
|
|
||||||
// DB settings
|
// DB settings
|
||||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||||
@@ -16,6 +16,7 @@ export const CONSTANTS = {
|
|||||||
EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings',
|
EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings',
|
||||||
ROLE_ADDRESS_CONFIG_KEY: 'role_address_config',
|
ROLE_ADDRESS_CONFIG_KEY: 'role_address_config',
|
||||||
IP_BLACKLIST_SETTINGS_KEY: 'ip_blacklist_settings',
|
IP_BLACKLIST_SETTINGS_KEY: 'ip_blacklist_settings',
|
||||||
|
AI_EXTRACT_SETTINGS_KEY: 'ai_extract_settings',
|
||||||
|
|
||||||
// KV
|
// KV
|
||||||
TG_KV_PREFIX: "temp-mail-telegram",
|
TG_KV_PREFIX: "temp-mail-telegram",
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* AI Email Extraction Module
|
||||||
|
*
|
||||||
|
* This module provides email content analysis using Cloudflare Workers AI.
|
||||||
|
* It extracts important information like verification codes, authentication links,
|
||||||
|
* service links, and subscription management links from email content.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { commonParseMail } from "../common";
|
||||||
|
import { getBooleanValue, getJsonSetting } from "../utils";
|
||||||
|
import { CONSTANTS } from "../constants";
|
||||||
|
import { Context } from "hono";
|
||||||
|
import type { AiExtractSettings } from "../admin_api/ai_extract_settings";
|
||||||
|
|
||||||
|
// AI Prompt for email analysis
|
||||||
|
const PROMPT = `
|
||||||
|
You are an expert email analyzer. Your task is to first UNDERSTAND the email content, then EXTRACT the most relevant information based on priority.
|
||||||
|
|
||||||
|
# Step 1: UNDERSTAND the Email
|
||||||
|
Read the entire email carefully and determine its:
|
||||||
|
- Overall purpose (verification, marketing, notification, etc.)
|
||||||
|
- Key context and situation
|
||||||
|
- What the sender wants the recipient to do
|
||||||
|
- Any security-sensitive content
|
||||||
|
|
||||||
|
# Step 2: EXTRACT Based on Priority
|
||||||
|
After understanding, extract the most important item according to this priority order:
|
||||||
|
|
||||||
|
**Priority 1: auth_code (Authentication Code)**
|
||||||
|
- Numeric or alphanumeric codes used for login verification
|
||||||
|
- Keywords: verification code, OTP, security code, confirmation code, auth code, 验证码, 校验码
|
||||||
|
- Extract ONLY the code itself (remove spaces, hyphens, etc.)
|
||||||
|
- Example: "123456" from "Your verification code is 123-456"
|
||||||
|
|
||||||
|
**Priority 2: auth_link (Authentication Link)**
|
||||||
|
- Links used for login, email verification, account activation, or password reset
|
||||||
|
- Keywords: verify, confirm, activate, login, signin, signup, reset, 验证, 激活, 登录
|
||||||
|
- Must be a real, complete URL (http:// or https://)
|
||||||
|
- Never fabricate or infer links that don't exist in the content
|
||||||
|
- Example: "https://example.com/verify?token=abc123"
|
||||||
|
|
||||||
|
**Priority 3: service_link (Service Link)**
|
||||||
|
- Links related to specific services or actions
|
||||||
|
- Keywords: commit, pull request, issue, repository, deployment, GitHub, GitLab, code review
|
||||||
|
- Real URLs for technical or service-related notifications
|
||||||
|
- Example: GitHub commit link, deployment notification link
|
||||||
|
|
||||||
|
**Priority 4: subscription_link (Subscription Management Link)**
|
||||||
|
- Links for managing email subscriptions, typically unsubscribe
|
||||||
|
- Keywords: unsubscribe, opt-out, manage preferences, 退订, 取消订阅
|
||||||
|
- Usually found at the bottom of marketing emails
|
||||||
|
- Real URLs for subscription control
|
||||||
|
|
||||||
|
**Priority 5: other_link (Other Valuable Link)**
|
||||||
|
- Any other link that might be useful or important
|
||||||
|
- Only extract if no higher-priority items exist
|
||||||
|
- Must be a real, complete URL from the content
|
||||||
|
|
||||||
|
**Priority 6: none**
|
||||||
|
- No relevant codes, links, or valuable content found
|
||||||
|
- Email appears to be plain text or irrelevant
|
||||||
|
|
||||||
|
# Special Case: Markdown Link Format
|
||||||
|
If the extracted content is in markdown link format [text](url):
|
||||||
|
|
||||||
|
- Extract the text inside the brackets as result_text
|
||||||
|
- When brackets are empty, analyze the email context and language
|
||||||
|
- Generate a concise, meaningful description (2-5 words) for result_text
|
||||||
|
- Match the email's language (Chinese → Chinese description, English → English)
|
||||||
|
|
||||||
|
# Critical Rules
|
||||||
|
1. **Understand First**: Always analyze the email's purpose before extracting
|
||||||
|
2. **Single Selection**: Choose ONLY ONE type based on the highest priority match
|
||||||
|
3. **Real Data Only**: Never invent, guess, or fabricate content
|
||||||
|
4. **Complete URLs**: Links must be full, valid URLs as they appear in the email
|
||||||
|
5. **Clean Extraction**: Return only the raw extracted content, no extra text
|
||||||
|
|
||||||
|
# Output Format (JSON only)
|
||||||
|
{
|
||||||
|
"type": "auth_code|auth_link|service_link|subscription_link|other_link|none",
|
||||||
|
"result": "the extracted code/link OR empty string",
|
||||||
|
"result_text": "the display text from markdown-format links."
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPORTANT: Return ONLY the JSON, no explanations or additional text.
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract important information from email content using Cloudflare Workers AI
|
||||||
|
*
|
||||||
|
* @param content - The email content to analyze (plain text or HTML)
|
||||||
|
* @param env - Cloudflare Workers environment bindings
|
||||||
|
* @returns Promise<ExtractResult> - The extracted information
|
||||||
|
*/
|
||||||
|
async function extractWithCloudflareAI(
|
||||||
|
content: string,
|
||||||
|
env: Bindings
|
||||||
|
): Promise<ExtractResult> {
|
||||||
|
// Get the AI model name from environment variable or use default
|
||||||
|
const modelName = env.AI_EXTRACT_MODEL || '@cf/meta/llama-3.1-8b-instruct';
|
||||||
|
|
||||||
|
const result = await env.AI.run(modelName as keyof AiModels, {
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: PROMPT },
|
||||||
|
{ role: 'user', content },
|
||||||
|
],
|
||||||
|
response_format: {
|
||||||
|
type: 'json_schema',
|
||||||
|
json_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['auth_code', 'auth_link', 'service_link', 'subscription_link', 'other_link', 'none']
|
||||||
|
},
|
||||||
|
result: { type: 'string' },
|
||||||
|
result_text: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['type', 'result', 'result_text'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stream: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error result.response
|
||||||
|
const response = result.response;
|
||||||
|
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
return JSON.parse(response) as ExtractResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && typeof response === 'object') {
|
||||||
|
return response as ExtractResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unexpected response format from Cloudflare AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main extraction function
|
||||||
|
* Checks if AI extraction is enabled, processes the email content, and saves to database
|
||||||
|
*
|
||||||
|
* @param parsedEmailContext - The parsed email context
|
||||||
|
* @param env - Cloudflare Workers environment bindings
|
||||||
|
* @param message_id - The email message ID
|
||||||
|
* @param address - The recipient email address
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export async function extractEmailInfo(
|
||||||
|
parsedEmailContext: ParsedEmailContext,
|
||||||
|
env: Bindings,
|
||||||
|
message_id: string | null,
|
||||||
|
address: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if AI extraction is enabled via environment variable
|
||||||
|
if (!getBooleanValue(env.ENABLE_AI_EMAIL_EXTRACT)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure AI binding is available
|
||||||
|
if (!env.AI) {
|
||||||
|
console.error('AI binding not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check allowlist if enabled
|
||||||
|
const aiSettings = await getJsonSetting<AiExtractSettings>(
|
||||||
|
{ env: env } as Context<HonoCustomType>,
|
||||||
|
CONSTANTS.AI_EXTRACT_SETTINGS_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
if (aiSettings?.enableAllowList && aiSettings.allowList?.length > 0) {
|
||||||
|
const isAllowed = aiSettings.allowList.some(pattern => {
|
||||||
|
// Support wildcard matching
|
||||||
|
if (pattern.includes('*')) {
|
||||||
|
// Escape special regex characters except *
|
||||||
|
const escapedPattern = pattern
|
||||||
|
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
.replace(/\*/g, '.*');
|
||||||
|
const regex = new RegExp('^' + escapedPattern + '$');
|
||||||
|
return regex.test(address);
|
||||||
|
}
|
||||||
|
// Exact match
|
||||||
|
return address === pattern;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
console.log(`AI extraction skipped for ${address}: not in allowlist`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse email to get content
|
||||||
|
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||||
|
const emailContent = parsedEmail?.text || parsedEmail?.html || "";
|
||||||
|
|
||||||
|
if (!emailContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate content if too long (max 4000 characters to avoid token limits)
|
||||||
|
const truncatedContent = emailContent.length > 4000
|
||||||
|
? emailContent.substring(0, 4000) + '...[truncated]'
|
||||||
|
: emailContent;
|
||||||
|
|
||||||
|
const result = await extractWithCloudflareAI(truncatedContent, env);
|
||||||
|
|
||||||
|
// If extraction found something useful, save it to database
|
||||||
|
if (result.type !== 'none' && result.result) {
|
||||||
|
const metadata = JSON.stringify({
|
||||||
|
ai_extract: result,
|
||||||
|
extracted_at: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the raw_mails record with metadata
|
||||||
|
await env.DB.prepare(
|
||||||
|
`UPDATE raw_mails SET metadata = ? WHERE message_id = ?`
|
||||||
|
).bind(metadata, message_id).run();
|
||||||
|
|
||||||
|
console.log(`AI extraction completed for ${message_id}: ${result.type}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('AI email extraction error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definition for extraction result
|
||||||
|
*/
|
||||||
|
export type ExtractResult = {
|
||||||
|
type: 'auth_code' | 'auth_link' | 'service_link' | 'subscription_link' | 'other_link' | 'none';
|
||||||
|
result: string;
|
||||||
|
result_text: string;
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { isBlocked } from "./black_list";
|
|||||||
import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common";
|
import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common";
|
||||||
import { check_if_junk_mail } from "./check_junk";
|
import { check_if_junk_mail } from "./check_junk";
|
||||||
import { remove_attachment_if_need } from "./check_attachment";
|
import { remove_attachment_if_need } from "./check_attachment";
|
||||||
|
import { extractEmailInfo } from "./ai_extract";
|
||||||
import { EmailRuleSettings } from "../models";
|
import { EmailRuleSettings } from "../models";
|
||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
|
|
||||||
@@ -155,6 +156,9 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
|||||||
|
|
||||||
// auto reply email
|
// auto reply email
|
||||||
await auto_reply(message, env);
|
await auto_reply(message, env);
|
||||||
|
|
||||||
|
// AI email content extraction
|
||||||
|
await extractEmailInfo(parsedEmailContext, env, message_id, message.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { email }
|
export { email }
|
||||||
|
|||||||
Vendored
+5
@@ -11,6 +11,7 @@ type Bindings = {
|
|||||||
RATE_LIMITER: any
|
RATE_LIMITER: any
|
||||||
SEND_MAIL: any
|
SEND_MAIL: any
|
||||||
ASSETS: Fetcher
|
ASSETS: Fetcher
|
||||||
|
AI: Ai
|
||||||
|
|
||||||
// config
|
// config
|
||||||
DEFAULT_LANG: string | undefined
|
DEFAULT_LANG: string | undefined
|
||||||
@@ -86,6 +87,10 @@ type Bindings = {
|
|||||||
|
|
||||||
// webhook config
|
// webhook config
|
||||||
FRONTEND_URL: string | undefined
|
FRONTEND_URL: string | undefined
|
||||||
|
|
||||||
|
// AI extraction config
|
||||||
|
ENABLE_AI_EMAIL_EXTRACT: string | boolean | undefined
|
||||||
|
AI_EXTRACT_MODEL: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type JwtPayload = {
|
type JwtPayload = {
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
|
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
|
||||||
# remove all attachment, mail maybe mising some information due to parsing
|
# remove all attachment, mail maybe mising some information due to parsing
|
||||||
# REMOVE_ALL_ATTACHMENT = true
|
# REMOVE_ALL_ATTACHMENT = true
|
||||||
|
# AI email extraction, automatically extract verification codes, auth links, etc.
|
||||||
|
# ENABLE_AI_EMAIL_EXTRACT = true
|
||||||
|
# AI model name, choose from https://developers.cloudflare.com/workers-ai/models/#text-generation
|
||||||
|
# AI_EXTRACT_MODEL = "@cf/meta/llama-3.1-8b-instruct"
|
||||||
# Calling other woker to process email
|
# Calling other woker to process email
|
||||||
# ENABLE_ANOTHER_WORKER = false
|
# ENABLE_ANOTHER_WORKER = false
|
||||||
# ANOTHER_WORKER_LIST = """
|
# ANOTHER_WORKER_LIST = """
|
||||||
@@ -127,6 +131,10 @@ binding = "DB"
|
|||||||
database_name = "xxx"
|
database_name = "xxx"
|
||||||
database_id = "xxx"
|
database_id = "xxx"
|
||||||
|
|
||||||
|
# Workers AI binding (required for AI email extraction)
|
||||||
|
# [ai]
|
||||||
|
# binding = "AI"
|
||||||
|
|
||||||
# kv config for send email verification code
|
# kv config for send email verification code
|
||||||
# [[kv_namespaces]]
|
# [[kv_namespaces]]
|
||||||
# binding = "KV"
|
# binding = "KV"
|
||||||
|
|||||||
Reference in New Issue
Block a user