Fix send mail form validation (#989)

* fix: harden send mail form validation

* fix: tighten send mail content checks

* fix: refine send mail empty-content checks

* fix: reset send mail preview state
This commit is contained in:
Dream Hunter
2026-04-18 15:29:48 +08:00
committed by GitHub
parent 5c40eeec80
commit d1fb1f773b
2 changed files with 150 additions and 24 deletions

View File

@@ -9,6 +9,7 @@ import { api } from '../../api'
const message = useMessage()
const isPreview = ref(false)
const editorRef = shallowRef()
const sending = ref(false)
const sendMailModel = useSessionStorage('sendMailByAdminModel', {
fromName: "",
@@ -33,6 +34,10 @@ const { t } = useI18n({
preview: 'Preview',
content: 'Content',
send: 'Send',
fromMailEmpty: 'Sender address is empty',
subjectEmpty: 'Subject is empty',
toMailEmpty: 'Recipient address is empty',
contentEmpty: 'Content is empty',
text: 'Text',
html: 'HTML',
'rich text': 'Rich Text',
@@ -48,6 +53,10 @@ const { t } = useI18n({
preview: '预览',
content: '内容',
send: '发送',
fromMailEmpty: '发件人地址不能为空',
subjectEmpty: '主题不能为空',
toMailEmpty: '收件人地址不能为空',
contentEmpty: '内容不能为空',
text: '文本',
html: 'HTML',
'rich text': '富文本',
@@ -62,21 +71,77 @@ const contentTypes = [
{ label: t('rich text'), value: 'rich' },
]
const normalizeSendMailText = (content) => {
return content
.replace(/[\u00AD\u200B-\u200D\u2060\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim()
}
const hasSendMailContent = (content, contentType) => {
if (typeof content !== 'string' || !content) {
return false
}
if (contentType === 'text') {
return normalizeSendMailText(content).length > 0
}
const container = document.createElement('div')
container.innerHTML = content
container.querySelectorAll('script, style, noscript, template').forEach((node) => node.remove())
const plainContent = normalizeSendMailText(container.textContent ?? '')
if (plainContent.length > 0) {
return true
}
return Boolean(container.querySelector('img, audio, video, iframe, svg, canvas, table'))
}
const send = async () => {
if (sending.value) {
return
}
const fromMail = `${sendMailModel.value.fromMail ?? ''}`.trim()
const toMail = `${sendMailModel.value.toMail ?? ''}`.trim()
const subject = `${sendMailModel.value.subject ?? ''}`.trim()
const content = `${sendMailModel.value.content ?? ''}`
if (!fromMail) {
message.error(t('fromMailEmpty'))
return
}
if (!subject) {
message.error(t('subjectEmpty'))
return
}
if (!toMail) {
message.error(t('toMailEmpty'))
return
}
if (!hasSendMailContent(content, sendMailModel.value.contentType)) {
message.error(t('contentEmpty'))
return
}
const payload = {
from_name: sendMailModel.value.fromName,
from_mail: fromMail,
to_name: sendMailModel.value.toName,
to_mail: toMail,
subject,
is_html: sendMailModel.value.contentType != 'text',
content,
}
sending.value = true
try {
await api.fetch(`/admin/send_mail`,
{
method: 'POST',
body:
JSON.stringify({
from_name: sendMailModel.value.fromName,
from_mail: sendMailModel.value.fromMail,
to_name: sendMailModel.value.toName,
to_mail: sendMailModel.value.toMail,
subject: sendMailModel.value.subject,
is_html: sendMailModel.value.contentType != 'text',
content: sendMailModel.value.content,
})
body: JSON.stringify(payload)
})
sendMailModel.value = {
fromName: "",
@@ -87,10 +152,11 @@ const send = async () => {
contentType: 'text',
content: "",
}
message.success(t("successSend"));
} catch (error) {
message.error(error.message || "error");
} finally {
message.success(t("successSend"));
sending.value = false
}
}
@@ -125,7 +191,7 @@ const handleCreated = (editor) => {
<div class="center">
<n-card :bordered="false" embedded>
<n-flex justify="end">
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
<n-button type="primary" :loading="sending" :disabled="sending" @click="send">{{ t('send') }}</n-button>
</n-flex>
<div class="left">
<n-form :model="sendMailModel">

View File

@@ -11,6 +11,7 @@ import { api } from '../../api'
const message = useMessage()
const isPreview = ref(false)
const editorRef = shallowRef()
const sending = ref(false)
const { settings, sendMailModel, indexTab, userSettings } = useGlobalState()
@@ -28,6 +29,9 @@ const { t } = useI18n({
preview: 'Preview',
content: 'Content',
send: 'Send',
subjectEmpty: 'Subject is empty',
toMailEmpty: 'Recipient address is empty',
contentEmpty: 'Content is empty',
requestAccess: 'Request Access',
requestAccessTip: 'You need to request access to send mail, if have request, please contact admin.',
send_balance: 'Send Mail Balance Left',
@@ -46,6 +50,9 @@ const { t } = useI18n({
preview: '预览',
content: '内容',
send: '发送',
subjectEmpty: '主题不能为空',
toMailEmpty: '收件人地址不能为空',
contentEmpty: '内容不能为空',
requestAccess: '申请权限',
requestAccessTip: '您需要申请权限才能发送邮件, 如果已经申请过, 请联系管理员提升额度。',
send_balance: '剩余发送邮件额度',
@@ -63,20 +70,71 @@ const contentTypes = [
{ label: t('rich text'), value: 'rich' },
]
const normalizeSendMailText = (content) => {
return content
.replace(/[\u00AD\u200B-\u200D\u2060\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim()
}
const hasSendMailContent = (content, contentType) => {
if (typeof content !== 'string' || !content) {
return false
}
if (contentType === 'text') {
return normalizeSendMailText(content).length > 0
}
const container = document.createElement('div')
container.innerHTML = content
container.querySelectorAll('script, style, noscript, template').forEach((node) => node.remove())
const plainContent = normalizeSendMailText(container.textContent ?? '')
if (plainContent.length > 0) {
return true
}
return Boolean(container.querySelector('img, audio, video, iframe, svg, canvas, table'))
}
const send = async () => {
if (sending.value) {
return
}
const subject = `${sendMailModel.value.subject ?? ''}`.trim()
const toMail = `${sendMailModel.value.toMail ?? ''}`.trim()
const content = `${sendMailModel.value.content ?? ''}`
if (!subject) {
message.error(t('subjectEmpty'))
return
}
if (!toMail) {
message.error(t('toMailEmpty'))
return
}
if (!hasSendMailContent(content, sendMailModel.value.contentType)) {
message.error(t('contentEmpty'))
return
}
const payload = {
from_name: sendMailModel.value.fromName,
to_name: sendMailModel.value.toName,
to_mail: toMail,
subject,
is_html: sendMailModel.value.contentType != 'text',
content,
}
sending.value = true
try {
await api.fetch(`/api/send_mail`,
{
method: 'POST',
body:
JSON.stringify({
from_name: sendMailModel.value.fromName,
to_name: sendMailModel.value.toName,
to_mail: sendMailModel.value.toMail,
subject: sendMailModel.value.subject,
is_html: sendMailModel.value.contentType != 'text',
content: sendMailModel.value.content,
})
body: JSON.stringify(payload)
})
sendMailModel.value = {
fromName: "",
@@ -86,11 +144,13 @@ const send = async () => {
contentType: 'text',
content: "",
}
isPreview.value = false
message.success(t("successSend"));
indexTab.value = 'sendbox'
} catch (error) {
message.error(error.message || "error");
} finally {
message.success(t("successSend"));
indexTab.value = 'sendbox'
sending.value = false
}
}
@@ -158,7 +218,7 @@ onMounted(async () => {
{{ t('send_balance') }}: {{ settings.send_balance }}
</n-alert>
<n-flex justify="end">
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
<n-button type="primary" :loading="sending" :disabled="sending" @click="send">{{ t('send') }}</n-button>
</n-flex>
<div class="left">
<n-form :model="sendMailModel">