feat: add email blacklist filtering
This commit is contained in:
@@ -147,6 +147,7 @@ const en = {
|
|||||||
autoRefreshDesc: 'Automatically fetch the latest emails from the server',
|
autoRefreshDesc: 'Automatically fetch the latest emails from the server',
|
||||||
sendEmail: 'Send Email',
|
sendEmail: 'Send Email',
|
||||||
resendToken: 'Resend Token',
|
resendToken: 'Resend Token',
|
||||||
|
blackList: 'Blocklist',
|
||||||
oss: 'Object Storage',
|
oss: 'Object Storage',
|
||||||
osDomain: 'Domain',
|
osDomain: 'Domain',
|
||||||
ossDomainDesc: 'Leave empty if using KV storage.',
|
ossDomainDesc: 'Leave empty if using KV storage.',
|
||||||
@@ -311,6 +312,9 @@ const en = {
|
|||||||
character: '',
|
character: '',
|
||||||
mustNotContain: 'Must Not Contain',
|
mustNotContain: 'Must Not Contain',
|
||||||
mustNotContainDesc: 'Separate with commas',
|
mustNotContainDesc: 'Separate with commas',
|
||||||
|
blackSubjectDesc: '输入多个值用,分开',
|
||||||
|
blackContentDesc: '输入多个值用,分开',
|
||||||
|
blackFromDesc: '输入多个值用,分开',
|
||||||
setSuccess: 'Settings saved successfully',
|
setSuccess: 'Settings saved successfully',
|
||||||
details: 'Details',
|
details: 'Details',
|
||||||
userDetails: 'User Details',
|
userDetails: 'User Details',
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ const zh = {
|
|||||||
autoRefreshDesc: '轮询请求服务器获取最新邮件',
|
autoRefreshDesc: '轮询请求服务器获取最新邮件',
|
||||||
sendEmail: '邮件发送',
|
sendEmail: '邮件发送',
|
||||||
resendToken: 'Resend Token',
|
resendToken: 'Resend Token',
|
||||||
|
blackList: '邮件黑名单',
|
||||||
oss: '对象存储',
|
oss: '对象存储',
|
||||||
osDomain: '访问域名',
|
osDomain: '访问域名',
|
||||||
ossDomainDesc: '如果是KV存储不要填',
|
ossDomainDesc: '如果是KV存储不要填',
|
||||||
@@ -311,6 +312,9 @@ const zh = {
|
|||||||
character: '位',
|
character: '位',
|
||||||
mustNotContain: '禁止包含',
|
mustNotContain: '禁止包含',
|
||||||
mustNotContainDesc: '输入多个值用,分开',
|
mustNotContainDesc: '输入多个值用,分开',
|
||||||
|
blackSubjectDesc: '邮件标题',
|
||||||
|
blackContentDesc: '邮件内容',
|
||||||
|
blackFromDesc: '发件人邮箱或域名',
|
||||||
setSuccess: '设置成功',
|
setSuccess: '设置成功',
|
||||||
details: '详情',
|
details: '详情',
|
||||||
userDetails: '用户详情',
|
userDetails: '用户详情',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import http from '@/axios/index.js';
|
import http from '@/axios/index.js';
|
||||||
|
|
||||||
export function settingSet(setting) {
|
export function settingSet(setting) {
|
||||||
return http.put('/setting/set',setting)
|
return http.put('/setting/set', setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function settingQuery() {
|
export function settingQuery() {
|
||||||
@@ -19,3 +19,7 @@ export function setBackground(background) {
|
|||||||
export function deleteBackground() {
|
export function deleteBackground() {
|
||||||
return http.delete('/setting/deleteBackground')
|
return http.delete('/setting/deleteBackground')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setBlackList(params) {
|
||||||
|
return http.put('/setting/setBlacklist', params)
|
||||||
|
}
|
||||||
@@ -189,6 +189,15 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div><span>{{ $t('blackList') }}</span></div>
|
||||||
|
<div>
|
||||||
|
<el-button class="opt-button" style="margin-top: 0" @click="openBlackListForm" size="small"
|
||||||
|
type="primary">
|
||||||
|
<Icon icon="fluent:settings-48-regular" width="16" height="16"/>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -724,17 +733,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="prefix-filter">
|
<div class="prefix-filter">
|
||||||
<div style="margin-bottom: 10px;">{{ t('mustNotContain') }}</div>
|
<div style="margin-bottom: 10px;">{{ t('mustNotContain') }}</div>
|
||||||
<el-input-tag style="margin-bottom: 10px;" v-model="emailPrefixFilter" :placeholder="t('mustNotContainDesc')" />
|
<el-input-tag style="margin-bottom: 10px;" v-model="emailPrefixFilter" />
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveEmailPrefix">{{ $t('save') }}</el-button>
|
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveEmailPrefix">{{ $t('save') }}</el-button>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<el-dialog v-model="blackFormShow" :title="t('blackList')" class="forward-dialog" @closed="resetBlackList">
|
||||||
|
<el-form>
|
||||||
|
<el-form-item :label="t('blackFromDesc')" label-position="top">
|
||||||
|
<el-input-tag v-model="blackListForm.blackFrom" @add-tag="banEmailAddTag" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('blackSubjectDesc')" label-position="top">
|
||||||
|
<el-input-tag v-model="blackListForm.blackSubject"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('blackContentDesc')" label-position="top">
|
||||||
|
<el-input-tag v-model="blackListForm.blackContent"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveBlackList">{{ $t('save') }}</el-button>
|
||||||
|
</el-dialog>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {computed, defineOptions, reactive, ref} from "vue";
|
import {computed, defineOptions, reactive, ref} from "vue";
|
||||||
import {deleteBackground, setBackground, settingQuery, settingSet} from "@/request/setting.js";
|
import {deleteBackground, setBackground, setBlackList, settingQuery, settingSet} from "@/request/setting.js";
|
||||||
import {useSettingStore} from "@/store/setting.js";
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
import {useUiStore} from "@/store/ui.js";
|
import {useUiStore} from "@/store/ui.js";
|
||||||
import {useUserStore} from "@/store/user.js";
|
import {useUserStore} from "@/store/user.js";
|
||||||
@@ -743,7 +766,7 @@ import {Icon} from "@iconify/vue";
|
|||||||
import {cvtR2Url} from "@/utils/convert.js";
|
import {cvtR2Url} from "@/utils/convert.js";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {debounce} from 'lodash-es'
|
import {debounce} from 'lodash-es'
|
||||||
import {isEmail} from "@/utils/verify-utils.js";
|
import {isDomain, isEmail} from "@/utils/verify-utils.js";
|
||||||
import loading from "@/components/loading/index.vue";
|
import loading from "@/components/loading/index.vue";
|
||||||
import {getTextWidth} from "@/utils/text.js";
|
import {getTextWidth} from "@/utils/text.js";
|
||||||
import {fileToBase64} from "@/utils/file-utils.js"
|
import {fileToBase64} from "@/utils/file-utils.js"
|
||||||
@@ -765,6 +788,7 @@ const accountStore = useAccountStore();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const editTitleShow = ref(false)
|
const editTitleShow = ref(false)
|
||||||
const resendTokenFormShow = ref(false)
|
const resendTokenFormShow = ref(false)
|
||||||
|
const blackFormShow = ref(false)
|
||||||
const r2DomainShow = ref(false)
|
const r2DomainShow = ref(false)
|
||||||
const turnstileShow = ref(false)
|
const turnstileShow = ref(false)
|
||||||
const tgSettingShow = ref(false)
|
const tgSettingShow = ref(false)
|
||||||
@@ -827,6 +851,12 @@ const regKeyOptions = computed(() => [
|
|||||||
{label: t('optional'), value: 2},
|
{label: t('optional'), value: 2},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const blackListForm = ref({
|
||||||
|
blackSubject: [],
|
||||||
|
blackContent: [],
|
||||||
|
blackFrom: []
|
||||||
|
})
|
||||||
|
|
||||||
const authRefreshOptions = computed(() => [
|
const authRefreshOptions = computed(() => [
|
||||||
{label: t('disable'), value: 0},
|
{label: t('disable'), value: 0},
|
||||||
{label: '3s', value: 3},
|
{label: '3s', value: 3},
|
||||||
@@ -874,6 +904,7 @@ function getSettings() {
|
|||||||
resetNoticeForm()
|
resetNoticeForm()
|
||||||
resetAddS3Form()
|
resetAddS3Form()
|
||||||
resetEmailPrefix()
|
resetEmailPrefix()
|
||||||
|
resetBlackList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1148,6 +1179,11 @@ function resetEmailPrefix() {
|
|||||||
emailPrefixFilter.value = setting.value.emailPrefixFilter
|
emailPrefixFilter.value = setting.value.emailPrefixFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetBlackList() {
|
||||||
|
blackListForm.value.blackFrom = setting.value.blackFrom ? setting.value.blackFrom.split(',') : []
|
||||||
|
blackListForm.value.blackContent = setting.value.blackContent ? setting.value.blackContent.split(',') : []
|
||||||
|
blackListForm.value.blackSubject = setting.value.blackSubject ? setting.value.blackSubject.split(',') : []
|
||||||
|
}
|
||||||
function saveEmailPrefix() {
|
function saveEmailPrefix() {
|
||||||
const form = {}
|
const form = {}
|
||||||
form.minEmailPrefix = minEmailPrefix.value
|
form.minEmailPrefix = minEmailPrefix.value
|
||||||
@@ -1160,6 +1196,44 @@ const opacityChange = debounce(doOpacityChange, 1000, {
|
|||||||
trailing: true
|
trailing: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function saveBlackList() {
|
||||||
|
|
||||||
|
let form = {
|
||||||
|
blackContent: blackListForm.value.blackContent + '',
|
||||||
|
blackSubject: blackListForm.value.blackSubject + '',
|
||||||
|
blackFrom: blackListForm.value.blackFrom + ''
|
||||||
|
}
|
||||||
|
|
||||||
|
settingLoading.value = true
|
||||||
|
|
||||||
|
setBlackList(form).then(() => {
|
||||||
|
getSettings()
|
||||||
|
ElMessage({
|
||||||
|
message: t('setSuccess'),
|
||||||
|
type: "success",
|
||||||
|
plain: true
|
||||||
|
})
|
||||||
|
blackFormShow.value = false;
|
||||||
|
}).finally(() => {
|
||||||
|
settingLoading.value = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function banEmailAddTag(val) {
|
||||||
|
const emails = Array.from(new Set(
|
||||||
|
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||||
|
));
|
||||||
|
|
||||||
|
blackListForm.value.blackFrom.splice(blackListForm.value.blackFrom.length - 1, 1)
|
||||||
|
|
||||||
|
emails.forEach(email => {
|
||||||
|
if ((isEmail(email) || isDomain(email)) && !blackListForm.value.blackFrom.includes(email)) {
|
||||||
|
blackListForm.value.blackFrom.push(email)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function delBackground() {
|
function delBackground() {
|
||||||
ElMessageBox.confirm(t('delBackgroundConfirm'), {
|
ElMessageBox.confirm(t('delBackgroundConfirm'), {
|
||||||
confirmButtonText: t('confirm'),
|
confirmButtonText: t('confirm'),
|
||||||
@@ -1245,6 +1319,10 @@ function openResendForm() {
|
|||||||
resendTokenFormShow.value = true
|
resendTokenFormShow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openBlackListForm() {
|
||||||
|
blackFormShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function saveResendToken() {
|
function saveResendToken() {
|
||||||
const settingForm = {
|
const settingForm = {
|
||||||
resendTokens: {}
|
resendTokens: {}
|
||||||
|
|||||||
@@ -27,3 +27,8 @@ app.delete('/setting/deleteBackground', async (c) => {
|
|||||||
return c.json(result.ok());
|
return c.json(result.ok());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put('/setting/setBlacklist', async (c) => {
|
||||||
|
const setting = await settingService.setBlacklist(c, await c.req.json());
|
||||||
|
return c.json(result.ok(setting));
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ export async function email(message, env, ctx) {
|
|||||||
ruleEmail,
|
ruleEmail,
|
||||||
ruleType,
|
ruleType,
|
||||||
r2Domain,
|
r2Domain,
|
||||||
noRecipient
|
noRecipient,
|
||||||
|
blackSubject,
|
||||||
|
blackContent,
|
||||||
|
blackFrom
|
||||||
} = await settingService.query({ env });
|
} = await settingService.query({ env });
|
||||||
|
|
||||||
if (receive === settingConst.receive.CLOSE) {
|
if (receive === settingConst.receive.CLOSE) {
|
||||||
@@ -32,7 +35,6 @@ export async function email(message, env, ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const reader = message.raw.getReader();
|
const reader = message.raw.getReader();
|
||||||
let content = '';
|
let content = '';
|
||||||
|
|
||||||
@@ -44,6 +46,14 @@ export async function email(message, env, ctx) {
|
|||||||
|
|
||||||
const email = await PostalMime.parse(content);
|
const email = await PostalMime.parse(content);
|
||||||
|
|
||||||
|
|
||||||
|
const blockFlag = checkBlock(blackSubject, blackContent, blackFrom, email);
|
||||||
|
|
||||||
|
if (blockFlag) {
|
||||||
|
message.setReject('Message rejected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
||||||
|
|
||||||
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
|
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
|
||||||
@@ -169,3 +179,31 @@ export async function email(message, env, ctx) {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkBlock(blackSubjectStr, blackContentStr, blackFromStr, email) {
|
||||||
|
|
||||||
|
const blackFromList = blackFromStr ? blackFromStr.split(',') : []
|
||||||
|
const blackContentList = blackContentStr ? blackContentStr.split(',') : []
|
||||||
|
const blackSubjectList = blackSubjectStr ? blackSubjectStr.split(',') : []
|
||||||
|
|
||||||
|
for (const blackSubject of blackSubjectList) {
|
||||||
|
if (email.subject?.includes(blackSubject)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const blackContent of blackContentList) {
|
||||||
|
if (email.html?.includes(blackContent) || email.text?.includes(blackContent)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const blackFrom of blackFromList) {
|
||||||
|
if (email.from.address.includes(blackFrom)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ export const setting = sqliteTable('setting', {
|
|||||||
tgMsgTo: text('tg_msg_to').default('show').notNull(),
|
tgMsgTo: text('tg_msg_to').default('show').notNull(),
|
||||||
tgMsgText: text('tg_msg_text').default('hide').notNull(),
|
tgMsgText: text('tg_msg_text').default('hide').notNull(),
|
||||||
minEmailPrefix: integer('min_email_prefix').default(0).notNull(),
|
minEmailPrefix: integer('min_email_prefix').default(0).notNull(),
|
||||||
emailPrefixFilter: text('email_prefix_filter').default('').notNull()
|
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()
|
||||||
});
|
});
|
||||||
export default setting
|
export default setting
|
||||||
|
|||||||
@@ -28,10 +28,23 @@ const dbInit = {
|
|||||||
await this.v2_7DB(c);
|
await this.v2_7DB(c);
|
||||||
await this.v2_8DB(c);
|
await this.v2_8DB(c);
|
||||||
await this.v2_9DB(c);
|
await this.v2_9DB(c);
|
||||||
|
await this.v3_0DB(c);
|
||||||
await settingService.refresh(c);
|
await settingService.refresh(c);
|
||||||
return c.text('success');
|
return c.text('success');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async v3_0DB(c) {
|
||||||
|
try {
|
||||||
|
await c.env.db.batch([
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN black_subject TEXT NOT NULL DEFAULT '';`),
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN black_content TEXT NOT NULL DEFAULT '';`),
|
||||||
|
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN black_from TEXT NOT NULL DEFAULT '';`)
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`跳过字段:${e.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async v2_9DB(c) {
|
async v2_9DB(c) {
|
||||||
try {
|
try {
|
||||||
await c.env.db.prepare(`UPDATE setting SET auto_refresh = 5 WHERE auto_refresh = 1;`).run();
|
await c.env.db.prepare(`UPDATE setting SET auto_refresh = 5 WHERE auto_refresh = 1;`).run();
|
||||||
|
|||||||
@@ -184,6 +184,14 @@ const settingService = {
|
|||||||
return background;
|
return background;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async setBlacklist(c, params) {
|
||||||
|
const { blackSubject, blackContent, blackFrom } = params
|
||||||
|
await orm(c).update(setting).set({ blackSubject, blackContent, blackFrom }).run();
|
||||||
|
await this.refresh(c);
|
||||||
|
return this.get(c);
|
||||||
|
},
|
||||||
|
|
||||||
async websiteConfig(c) {
|
async websiteConfig(c) {
|
||||||
|
|
||||||
const settingRow = await this.get(c, true);
|
const settingRow = await this.get(c, true);
|
||||||
@@ -220,7 +228,8 @@ const settingService = {
|
|||||||
minEmailPrefix: settingRow.minEmailPrefix,
|
minEmailPrefix: settingRow.minEmailPrefix,
|
||||||
projectLink: settingRow.projectLink
|
projectLink: settingRow.projectLink
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default settingService;
|
export default settingService;
|
||||||
|
|||||||
Reference in New Issue
Block a user