diff --git a/mail-vue/src/i18n/en.js b/mail-vue/src/i18n/en.js index 0c7a97e..d030b63 100644 --- a/mail-vue/src/i18n/en.js +++ b/mail-vue/src/i18n/en.js @@ -148,6 +148,9 @@ const en = { sendEmail: 'Send Email', resendToken: 'Resend Token', blackList: 'Blocklist', + codeRecognition: 'Verification Code Recognition', + codeRecognitionRules: 'Recognition Rules', + codeRecognitionRulesDesc: 'Only recognize emails containing the following info. Empty means all.', oss: 'Object Storage', osDomain: 'Domain', ossDomainDesc: 'Leave empty if using KV storage.', @@ -315,6 +318,7 @@ const en = { blackSubjectDesc: 'Subject', blackContentDesc: 'Content', blackFromDesc: 'Sender or domain', + senderRules: 'Sender address or domain', blackListDesc: 'Reject emails containing the following information', setSuccess: 'Settings saved successfully', details: 'Details', diff --git a/mail-vue/src/i18n/zh.js b/mail-vue/src/i18n/zh.js index 5730c39..fc382d2 100644 --- a/mail-vue/src/i18n/zh.js +++ b/mail-vue/src/i18n/zh.js @@ -148,6 +148,9 @@ const zh = { sendEmail: '邮件发送', resendToken: 'Resend Token', blackList: '邮件黑名单', + codeRecognition: '识别验证码', + codeRecognitionRules: '识别规则', + codeRecognitionRulesDesc: '只识别包含以下信息的邮件,为空识别全部', oss: '对象存储', osDomain: '访问域名', ossDomainDesc: '如果是KV存储不要填', @@ -315,6 +318,7 @@ const zh = { blackSubjectDesc: '邮件标题', blackContentDesc: '邮件内容', blackFromDesc: '发件人邮箱或域名', + senderRules: '发件人邮箱或域名', blackListDesc: '拒收包含以下信息的邮件', setSuccess: '设置成功', details: '详情', diff --git a/mail-vue/src/views/sys-setting/index.vue b/mail-vue/src/views/sys-setting/index.vue index 7a0b0cf..ddda292 100644 --- a/mail-vue/src/views/sys-setting/index.vue +++ b/mail-vue/src/views/sys-setting/index.vue @@ -361,6 +361,27 @@ +
+
Workers AI
+
+
+
{{ $t('codeRecognition') }}
+
+ +
+
+
+
{{ $t('codeRecognitionRules') }}
+
+ + + +
+
+
+
+
{{ $t('about') }}
@@ -759,6 +780,22 @@ {{ $t('save') }} + + + + + + + + {{ $t('save') }} +
@@ -797,6 +834,7 @@ const userStore = useUserStore(); const editTitleShow = ref(false) const resendTokenFormShow = ref(false) const blackFormShow = ref(false) +const aiCodeFilterShow = ref(false) const r2DomainShow = ref(false) const turnstileShow = ref(false) const tgSettingShow = ref(false) @@ -864,6 +902,7 @@ const blackListForm = ref({ blackContent: [], blackFrom: [] }) +const aiCodeFilter = ref([]) const authRefreshOptions = computed(() => [ {label: t('disable'), value: 0}, @@ -913,6 +952,7 @@ function getSettings() { resetAddS3Form() resetEmailPrefix() resetBlackList() + resetAiCodeFilter() }) } @@ -1192,6 +1232,11 @@ function resetBlackList() { blackListForm.value.blackContent = setting.value.blackContent ? setting.value.blackContent.split(',') : [] blackListForm.value.blackSubject = setting.value.blackSubject ? setting.value.blackSubject.split(',') : [] } + +function resetAiCodeFilter() { + aiCodeFilter.value = setting.value.aiCodeFilter ? setting.value.aiCodeFilter.split(',') : [] +} + function saveEmailPrefix() { const form = {} form.minEmailPrefix = minEmailPrefix.value @@ -1199,6 +1244,10 @@ function saveEmailPrefix() { editSetting(form, true) } +function saveAiCodeFilter() { + editSetting({aiCodeFilter: aiCodeFilter.value + ''}) +} + const opacityChange = debounce(doOpacityChange, 1000, { leading: false, trailing: true @@ -1241,6 +1290,20 @@ function banEmailAddTag(val) { }) } +function aiCodeFilterAddTag(val) { + const emails = Array.from(new Set( + val.split(/[,,]/).map(item => item.trim()).filter(item => item) + )); + + aiCodeFilter.value.splice(aiCodeFilter.value.length - 1, 1) + + emails.forEach(email => { + if ((isEmail(email) || isDomain(email)) && !aiCodeFilter.value.includes(email)) { + aiCodeFilter.value.push(email) + } + }) +} + function delBackground() { ElMessageBox.confirm(t('delBackgroundConfirm'), { @@ -1331,6 +1394,10 @@ function openBlackListForm() { blackFormShow.value = true } +function openAiCodeFilter() { + aiCodeFilterShow.value = true +} + function saveResendToken() { const settingForm = { resendTokens: {} @@ -1408,6 +1475,7 @@ function editSetting(settingForm, refreshStatus = true) { noticePopupShow.value = false addS3Show.value = false emailPrefixShow.value = false + aiCodeFilterShow.value = false }).catch((e) => { loginOpacity.value = setting.value.loginOpacity setting.value = {...setting.value, ...JSON.parse(backup)} diff --git a/mail-worker/src/const/entity-const.js b/mail-worker/src/const/entity-const.js index 946b108..711b2af 100644 --- a/mail-worker/src/const/entity-const.js +++ b/mail-worker/src/const/entity-const.js @@ -128,6 +128,10 @@ export const settingConst = { OPEN: 0, CLOSE: 1 }, + aiCode: { + OPEN: 0, + CLOSE: 1 + }, authRefresh: { OPEN: 1, CLOSE: 0 diff --git a/mail-worker/src/email/email.js b/mail-worker/src/email/email.js index 41f62e3..1136be3 100644 --- a/mail-worker/src/email/email.js +++ b/mail-worker/src/email/email.js @@ -28,7 +28,9 @@ export async function email(message, env, ctx) { noRecipient, blackSubject, blackContent, - blackFrom + blackFrom, + aiCode, + aiCodeFilter } = await settingService.query({ env }); if (receive === settingConst.receive.CLOSE) { @@ -90,7 +92,7 @@ export async function email(message, env, ctx) { } const toName = email.to.find(item => item.address === message.to)?.name || ''; - const code = await aiService.extractCode({ env }, email); + const code = aiCode === settingConst.aiCode.OPEN && checkAiCodeFilter(aiCodeFilter, email) ? await aiService.extractCode({ env }, email) : ''; const params = { toEmail: message.to, @@ -183,6 +185,19 @@ export async function email(message, env, ctx) { } } +function checkAiCodeFilter(aiCodeFilterStr, email) { + const filterList = aiCodeFilterStr ? aiCodeFilterStr.split(',').map(item => item.trim().toLowerCase()).filter(Boolean) : []; + + if (filterList.length === 0) { + return true; + } + + const fromEmail = (email.from?.address || '').trim().toLowerCase(); + const fromDomain = emailUtils.getDomain(fromEmail).toLowerCase(); + + return filterList.some(item => item === fromEmail || item === fromDomain); +} + function checkBlock(blackSubjectStr, blackContentStr, blackFromStr, email) { const blackFromList = blackFromStr ? blackFromStr.split(',') : [] diff --git a/mail-worker/src/entity/setting.js b/mail-worker/src/entity/setting.js index 7172ff3..b50519f 100644 --- a/mail-worker/src/entity/setting.js +++ b/mail-worker/src/entity/setting.js @@ -49,6 +49,8 @@ export const setting = sqliteTable('setting', { emailPrefixFilter: text('email_prefix_filter').default('').notNull(), blackSubject: text('black_subject').default('').notNull(), blackContent: text('black_content').default('').notNull(), - blackFrom: text('black_from').default('').notNull() + blackFrom: text('black_from').default('').notNull(), + aiCode: integer('ai_code').default(1).notNull(), + aiCodeFilter: text('ai_code_filter').default('').notNull() }); export default setting diff --git a/mail-worker/src/init/init.js b/mail-worker/src/init/init.js index 7f65607..d9ad096 100644 --- a/mail-worker/src/init/init.js +++ b/mail-worker/src/init/init.js @@ -49,6 +49,18 @@ const dbInit = { } catch (e) { console.warn(`跳过字段:${e.message}`); } + + try { + await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN ai_code INTEGER NOT NULL DEFAULT 1;`).run(); + } catch (e) { + console.warn(`跳过字段:${e.message}`); + } + + try { + await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN ai_code_filter TEXT NOT NULL DEFAULT '';`).run(); + } catch (e) { + console.warn(`跳过字段:${e.message}`); + } }, async v2_9DB(c) { @@ -592,7 +604,9 @@ const dbInit = { title TEXT NOT NULL, auto_refresh INTEGER NOT NULL, register_verify INTEGER NOT NULL, - add_email_verify INTEGER NOT NULL + add_email_verify INTEGER NOT NULL, + ai_code INTEGER NOT NULL DEFAULT 1, + ai_code_filter TEXT NOT NULL DEFAULT '' ) `).run(); diff --git a/mail-worker/src/service/setting-service.js b/mail-worker/src/service/setting-service.js index da94c02..8cc4743 100644 --- a/mail-worker/src/service/setting-service.js +++ b/mail-worker/src/service/setting-service.js @@ -133,6 +133,10 @@ const settingService = { params.emailPrefixFilter = params.emailPrefixFilter + ''; } + if (Array.isArray(params.aiCodeFilter)) { + params.aiCodeFilter = params.aiCodeFilter + ''; + } + params.resendTokens = JSON.stringify(resendTokens); await orm(c).update(setting).set({ ...params }).returning().get(); await this.refresh(c);