新增注册邮箱名自定义字符过滤

This commit is contained in:
eoao
2025-11-16 12:52:37 +08:00
parent fd90625651
commit aa86b2dc38
12 changed files with 79 additions and 25 deletions
+4 -2
View File
@@ -1,5 +1,5 @@
const en = { const en = {
'收件箱': 'Inbox', inbox: 'Inbox',
drafts: 'Drafts', drafts: 'Drafts',
sent: 'Sent', sent: 'Sent',
starred: 'Starred', starred: 'Starred',
@@ -307,7 +307,9 @@ const en = {
emailText: 'Email Text', emailText: 'Email Text',
emailPrefix: 'Email Prefix', emailPrefix: 'Email Prefix',
atLeast: 'At Least', atLeast: 'At Least',
character: '' character: '',
mustNotContain: 'Must Not Contain',
mustNotContainDesc: 'Separate with commas'
} }
export default en export default en
+7 -5
View File
@@ -1,5 +1,5 @@
const zh = { const zh = {
'收件箱': '收件箱', inbox: '收件箱',
drafts: '草稿箱', drafts: '草稿箱',
sent: '已发送', sent: '已发送',
starred: '星标邮件', starred: '星标邮件',
@@ -192,8 +192,8 @@ const zh = {
emptyEmailMsg: '邮箱不能为空', emptyEmailMsg: '邮箱不能为空',
notEmailMsg: '输入的邮箱不合法', notEmailMsg: '输入的邮箱不合法',
emptyPwdMsg: '密码不能为空', emptyPwdMsg: '密码不能为空',
pwdLengthMsg: '密码少六位', pwdLengthMsg: '密码少六位',
minEmailPrefix: '邮箱名不能小于{msg}位', minEmailPrefix: '邮箱名至少{msg}位',
confirmPwdFailMsg: '两次密码输入不一致', confirmPwdFailMsg: '两次密码输入不一致',
emptyRegKeyMsg: '注册码不能为空', emptyRegKeyMsg: '注册码不能为空',
regSuccessMsg: '注册成功', regSuccessMsg: '注册成功',
@@ -228,7 +228,7 @@ const zh = {
banRestore: '确认禁用 {msg} 吗?', banRestore: '确认禁用 {msg} 吗?',
logOut: '退出', logOut: '退出',
clearContentConfirm: '确定要清空所有内容吗?', clearContentConfirm: '确定要清空所有内容吗?',
attLimitMsg: '附件大小限制28mb', attLimitMsg: '附件不能超过28MB',
emptyRecipientMsg: '收件人邮箱地址不能为空', emptyRecipientMsg: '收件人邮箱地址不能为空',
emptySubjectMsg: '主题不能为空', emptySubjectMsg: '主题不能为空',
emptyContentMsg: '邮件正文不能为空', emptyContentMsg: '邮件正文不能为空',
@@ -307,6 +307,8 @@ const zh = {
emailText: '邮件文本', emailText: '邮件文本',
emailPrefix: '邮箱前缀', emailPrefix: '邮箱前缀',
atLeast: '至少', atLeast: '至少',
character: '位' character: '位',
mustNotContain: '禁止包含',
mustNotContainDesc: '输入多个值用,分开'
} }
export default zh export default zh
+10 -1
View File
@@ -9,7 +9,7 @@
<el-menu-item @click="router.push({name: 'email'})" index="email" <el-menu-item @click="router.push({name: 'email'})" index="email"
:class="route.meta.name === 'email' ? 'choose-item' : ''"> :class="route.meta.name === 'email' ? 'choose-item' : ''">
<Icon icon="hugeicons:mailbox-01" width="20" height="20" /> <Icon icon="hugeicons:mailbox-01" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">{{$t('收件箱')}}</span> <span class="menu-name" style="margin-left: 21px">{{$t('inbox')}}</span>
</el-menu-item> </el-menu-item>
<el-menu-item @click="router.push({name: 'send'})" index="send" v-perm="'email:send'" <el-menu-item @click="router.push({name: 'send'})" index="send" v-perm="'email:send'"
:class="route.meta.name === 'send' ? 'choose-item' : ''"> :class="route.meta.name === 'send' ? 'choose-item' : ''">
@@ -96,8 +96,17 @@ const route = useRoute();
color: #ffffff; color: #ffffff;
background: linear-gradient(135deg, #1890ff, #3a80dd); background: linear-gradient(135deg, #1890ff, #3a80dd);
transition: all 0.3s ease; transition: all 0.3s ease;
max-width: 240px;
padding: 0 10px;
> div {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: calc(240px - 20px - 30px);
}
:deep(.el-icon) { :deep(.el-icon) {
flex-shrink: 0;
font-size: 20px; font-size: 20px;
} }
+1 -1
View File
@@ -16,7 +16,7 @@ const routes = [
name: 'email', name: 'email',
component: () => import('@/views/email/index.vue'), component: () => import('@/views/email/index.vue'),
meta: { meta: {
title: '收件箱', title: 'inbox',
name: 'email', name: 'email',
menu: true menu: true
} }
+24 -11
View File
@@ -713,15 +713,20 @@
</div> </div>
</form> </form>
</el-dialog> </el-dialog>
<el-dialog v-model="emailPrefixShow" :title="t('emailPrefix')" width="30" > <el-dialog v-model="emailPrefixShow" :title="t('emailPrefix')" @closed="resetEmailPrefix" >
<div class="email-prefix"> <div class="email-prefix">
<div>{{ t('atLeast') }}</div> <div>{{ t('atLeast') }}</div>
<el-input-number v-model="minEmailPrefix" :min="1" :max="20" @change="EmailPrefixChange" style="width: 150px" > <el-input-number v-model="minEmailPrefix" :min="1" :max="20" style="width: 150px" >
<template #suffix> <template #suffix>
<span>{{ t('character') }}</span> <span>{{ t('character') }}</span>
</template> </template>
</el-input-number> </el-input-number>
</div> </div>
<div class="prefix-filter">
<div style="margin-bottom: 10px;">{{ t('mustNotContain') }}</div>
<el-input-tag style="margin-bottom: 10px;" v-model="emailPrefixFilter" :placeholder="t('mustNotContainDesc')" />
</div>
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveEmailPrefix">{{ $t('save') }}</el-button>
</el-dialog> </el-dialog>
</el-scrollbar> </el-scrollbar>
</div> </div>
@@ -777,6 +782,7 @@ const clearS3Loading = ref(false)
const r2DomainInput = ref('') const r2DomainInput = ref('')
const loginOpacity = ref(0) const loginOpacity = ref(0)
const minEmailPrefix = ref(0) const minEmailPrefix = ref(0)
const emailPrefixFilter = ref([])
const backgroundUrl = ref('') const backgroundUrl = ref('')
let backgroundFile = {} let backgroundFile = {}
const showSetBackground = ref(false) const showSetBackground = ref(false)
@@ -868,6 +874,7 @@ function getSettings() {
regVerifyCount.value = setting.value.regVerifyCount regVerifyCount.value = setting.value.regVerifyCount
resetNoticeForm() resetNoticeForm()
resetAddS3Form() resetAddS3Form()
resetEmailPrefix()
}) })
} }
@@ -1137,16 +1144,17 @@ function doOpacityChange() {
editSetting(form, true) editSetting(form, true)
} }
function doEmailPrefix() { function resetEmailPrefix() {
const form = {} minEmailPrefix.value = setting.value.minEmailPrefix
form.minEmailPrefix = minEmailPrefix.value emailPrefixFilter.value = setting.value.emailPrefixFilter
editSetting(form, true)
} }
const EmailPrefixChange = debounce(doEmailPrefix, 1000, { function saveEmailPrefix() {
leading: false, const form = {}
trailing: true form.minEmailPrefix = minEmailPrefix.value
}) form.emailPrefixFilter = emailPrefixFilter.value
editSetting(form, true)
}
const opacityChange = debounce(doOpacityChange, 1000, { const opacityChange = debounce(doOpacityChange, 1000, {
leading: false, leading: false,
@@ -1314,9 +1322,9 @@ function editSetting(settingForm, refreshStatus = true) {
regVerifyCountShow.value = false regVerifyCountShow.value = false
noticePopupShow.value = false noticePopupShow.value = false
addS3Show.value = false addS3Show.value = false
emailPrefixShow.value = false
}).catch((e) => { }).catch((e) => {
loginOpacity.value = setting.value.loginOpacity loginOpacity.value = setting.value.loginOpacity
minEmailPrefix.value = setting.value.minEmailPrefix
setting.value = {...setting.value, ...JSON.parse(backup)} setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => { }).finally(() => {
settingLoading.value = false settingLoading.value = false
@@ -1675,6 +1683,11 @@ function editSetting(settingForm, refreshStatus = true) {
justify-content: space-between; justify-content: space-between;
} }
.prefix-filter {
display: flex;
flex-direction: column;
}
.s3-button { .s3-button {
display: grid; display: grid;
grid-template-columns: 80px 1fr; grid-template-columns: 80px 1fr;
+2 -1
View File
@@ -46,6 +46,7 @@ export const setting = sqliteTable('setting', {
tgMsgFrom: text('tg_msg_from').default('only-name').notNull(), tgMsgFrom: text('tg_msg_from').default('only-name').notNull(),
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()
}); });
export default setting export default setting
+1
View File
@@ -28,6 +28,7 @@ const en = {
pwdLengthLimit: 'Password length exceeds the limit', pwdLengthLimit: 'Password length exceeds the limit',
emailLengthLimit: 'Email length exceeds the limit', emailLengthLimit: 'Email length exceeds the limit',
minEmailPrefix: 'Email must be at least {{msg}} characters', minEmailPrefix: 'Email must be at least {{msg}} characters',
banEmailPrefix: 'Invalid characters in email address',
pwdMinLength: 'Password must be at least 6 characters', pwdMinLength: 'Password must be at least 6 characters',
notEmailDomain: 'Invalid email domain', notEmailDomain: 'Invalid email domain',
emptyRegKey: 'Invite code cannot be empty', emptyRegKey: 'Invite code cannot be empty',
+3 -2
View File
@@ -27,8 +27,9 @@ const zh = {
notExistEmailReply: '邮件不存在无法回复', notExistEmailReply: '邮件不存在无法回复',
pwdLengthLimit: '密码长度超出限制', pwdLengthLimit: '密码长度超出限制',
emailLengthLimit: '邮箱长度超出限制', emailLengthLimit: '邮箱长度超出限制',
minEmailPrefix: '邮箱名不能小于{{msg}}位', minEmailPrefix: '邮箱名至少{{msg}}位',
pwdMinLength: '密码不能小于6位', banEmailPrefix: '邮箱名包含非法字符',
pwdMinLength: '密码至少六位',
notEmailDomain: '非法邮箱域名', notEmailDomain: '非法邮箱域名',
emptyRegKey: '注册码不能为空', emptyRegKey: '注册码不能为空',
notExistRegKey: '注册码不存在', notExistRegKey: '注册码不存在',
+10
View File
@@ -24,10 +24,19 @@ const init = {
await this.v2DB(c); await this.v2DB(c);
await this.v2_3DB(c); await this.v2_3DB(c);
await this.v2_4DB(c); await this.v2_4DB(c);
await this.v2_5DB(c);
await settingService.refresh(c); await settingService.refresh(c);
return c.text(t('initSuccess')); return c.text(t('initSuccess'));
}, },
async v2_5DB(c) {
try {
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN email_prefix_filter text NOT NULL DEFAULT '';`).run();
} catch (e) {
console.error(e)
}
},
async v2_4DB(c) { async v2_4DB(c) {
try { try {
await c.env.db.prepare(` await c.env.db.prepare(`
@@ -54,6 +63,7 @@ const init = {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
}, },
async v2_3DB(c) { async v2_3DB(c) {
+5 -1
View File
@@ -17,7 +17,7 @@ const accountService = {
async add(c, params, userId) { async add(c, params, userId) {
const {addEmailVerify , addEmail, manyEmail, addVerifyCount, minEmailPrefix} = await settingService.query(c); const { addEmailVerify , addEmail, manyEmail, addVerifyCount, minEmailPrefix, emailPrefixFilter } = await settingService.query(c);
let { email, token } = params; let { email, token } = params;
@@ -43,6 +43,10 @@ const accountService = {
throw new BizError(t('minEmailPrefix', { msg: minEmailPrefix } )); throw new BizError(t('minEmailPrefix', { msg: minEmailPrefix } ));
} }
if (emailPrefixFilter.some(content => emailUtils.getName(email).includes(content))) {
throw new BizError(t('banEmailPrefix'));
}
let accountRow = await this.selectByEmailIncludeDel(c, email); let accountRow = await this.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) { if (accountRow && accountRow.isDel === isDel.DELETE) {
+5 -1
View File
@@ -26,7 +26,7 @@ const loginService = {
const { email, password, token, code } = params; const { email, password, token, code } = params;
let {regKey, register, registerVerify, regVerifyCount, minEmailPrefix} = await settingService.query(c) let { regKey, register, registerVerify, regVerifyCount, minEmailPrefix, emailPrefixFilter } = await settingService.query(c)
if (oauth) { if (oauth) {
registerVerify = settingConst.registerVerify.CLOSE; registerVerify = settingConst.registerVerify.CLOSE;
@@ -45,6 +45,10 @@ const loginService = {
throw new BizError(t('minEmailPrefix', { msg: minEmailPrefix } )); throw new BizError(t('minEmailPrefix', { msg: minEmailPrefix } ));
} }
if (emailPrefixFilter.some(content => emailUtils.getName(email).includes(content))) {
throw new BizError(t('banEmailPrefix'));
}
if (emailUtils.getName(email).length > 64) { if (emailUtils.getName(email).length > 64) {
throw new BizError(t('emailLengthLimit')); throw new BizError(t('emailLengthLimit'));
} }
@@ -58,6 +58,8 @@ const settingService = {
setting.linuxdoCallbackUrl = c.env.linuxdo_callback_url; setting.linuxdoCallbackUrl = c.env.linuxdo_callback_url;
setting.linuxdoSwitch = linuxdoSwitch; setting.linuxdoSwitch = linuxdoSwitch;
setting.emailPrefixFilter = setting.emailPrefixFilter.split(",").filter(Boolean);
c.set?.('setting', setting); c.set?.('setting', setting);
return setting; return setting;
}, },
@@ -108,6 +110,11 @@ const settingService = {
Object.keys(resendTokens).forEach(domain => { Object.keys(resendTokens).forEach(domain => {
if (!resendTokens[domain]) delete resendTokens[domain]; if (!resendTokens[domain]) delete resendTokens[domain];
}); });
if (Array.isArray(params.emailPrefixFilter)) {
params.emailPrefixFilter = params.emailPrefixFilter + '';
}
params.resendTokens = JSON.stringify(resendTokens); params.resendTokens = JSON.stringify(resendTokens);
await orm(c).update(setting).set({ ...params }).returning().get(); await orm(c).update(setting).set({ ...params }).returning().get();
await this.refresh(c); await this.refresh(c);