mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
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:
@@ -9,6 +9,7 @@ import { api } from '../../api'
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const isPreview = ref(false)
|
const isPreview = ref(false)
|
||||||
const editorRef = shallowRef()
|
const editorRef = shallowRef()
|
||||||
|
const sending = ref(false)
|
||||||
|
|
||||||
const sendMailModel = useSessionStorage('sendMailByAdminModel', {
|
const sendMailModel = useSessionStorage('sendMailByAdminModel', {
|
||||||
fromName: "",
|
fromName: "",
|
||||||
@@ -33,6 +34,10 @@ const { t } = useI18n({
|
|||||||
preview: 'Preview',
|
preview: 'Preview',
|
||||||
content: 'Content',
|
content: 'Content',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
fromMailEmpty: 'Sender address is empty',
|
||||||
|
subjectEmpty: 'Subject is empty',
|
||||||
|
toMailEmpty: 'Recipient address is empty',
|
||||||
|
contentEmpty: 'Content is empty',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
html: 'HTML',
|
html: 'HTML',
|
||||||
'rich text': 'Rich Text',
|
'rich text': 'Rich Text',
|
||||||
@@ -48,6 +53,10 @@ const { t } = useI18n({
|
|||||||
preview: '预览',
|
preview: '预览',
|
||||||
content: '内容',
|
content: '内容',
|
||||||
send: '发送',
|
send: '发送',
|
||||||
|
fromMailEmpty: '发件人地址不能为空',
|
||||||
|
subjectEmpty: '主题不能为空',
|
||||||
|
toMailEmpty: '收件人地址不能为空',
|
||||||
|
contentEmpty: '内容不能为空',
|
||||||
text: '文本',
|
text: '文本',
|
||||||
html: 'HTML',
|
html: 'HTML',
|
||||||
'rich text': '富文本',
|
'rich text': '富文本',
|
||||||
@@ -62,21 +71,77 @@ const contentTypes = [
|
|||||||
{ label: t('rich text'), value: 'rich' },
|
{ 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 () => {
|
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 {
|
try {
|
||||||
await api.fetch(`/admin/send_mail`,
|
await api.fetch(`/admin/send_mail`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body:
|
body: JSON.stringify(payload)
|
||||||
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,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
sendMailModel.value = {
|
sendMailModel.value = {
|
||||||
fromName: "",
|
fromName: "",
|
||||||
@@ -87,10 +152,11 @@ const send = async () => {
|
|||||||
contentType: 'text',
|
contentType: 'text',
|
||||||
content: "",
|
content: "",
|
||||||
}
|
}
|
||||||
|
message.success(t("successSend"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
} finally {
|
} finally {
|
||||||
message.success(t("successSend"));
|
sending.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +191,7 @@ const handleCreated = (editor) => {
|
|||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<n-flex justify="end">
|
<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>
|
</n-flex>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<n-form :model="sendMailModel">
|
<n-form :model="sendMailModel">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { api } from '../../api'
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const isPreview = ref(false)
|
const isPreview = ref(false)
|
||||||
const editorRef = shallowRef()
|
const editorRef = shallowRef()
|
||||||
|
const sending = ref(false)
|
||||||
|
|
||||||
|
|
||||||
const { settings, sendMailModel, indexTab, userSettings } = useGlobalState()
|
const { settings, sendMailModel, indexTab, userSettings } = useGlobalState()
|
||||||
@@ -28,6 +29,9 @@ const { t } = useI18n({
|
|||||||
preview: 'Preview',
|
preview: 'Preview',
|
||||||
content: 'Content',
|
content: 'Content',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
subjectEmpty: 'Subject is empty',
|
||||||
|
toMailEmpty: 'Recipient address is empty',
|
||||||
|
contentEmpty: 'Content is empty',
|
||||||
requestAccess: 'Request Access',
|
requestAccess: 'Request Access',
|
||||||
requestAccessTip: 'You need to request access to send mail, if have request, please contact admin.',
|
requestAccessTip: 'You need to request access to send mail, if have request, please contact admin.',
|
||||||
send_balance: 'Send Mail Balance Left',
|
send_balance: 'Send Mail Balance Left',
|
||||||
@@ -46,6 +50,9 @@ const { t } = useI18n({
|
|||||||
preview: '预览',
|
preview: '预览',
|
||||||
content: '内容',
|
content: '内容',
|
||||||
send: '发送',
|
send: '发送',
|
||||||
|
subjectEmpty: '主题不能为空',
|
||||||
|
toMailEmpty: '收件人地址不能为空',
|
||||||
|
contentEmpty: '内容不能为空',
|
||||||
requestAccess: '申请权限',
|
requestAccess: '申请权限',
|
||||||
requestAccessTip: '您需要申请权限才能发送邮件, 如果已经申请过, 请联系管理员提升额度。',
|
requestAccessTip: '您需要申请权限才能发送邮件, 如果已经申请过, 请联系管理员提升额度。',
|
||||||
send_balance: '剩余发送邮件额度',
|
send_balance: '剩余发送邮件额度',
|
||||||
@@ -63,20 +70,71 @@ const contentTypes = [
|
|||||||
{ label: t('rich text'), value: 'rich' },
|
{ 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 () => {
|
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 {
|
try {
|
||||||
await api.fetch(`/api/send_mail`,
|
await api.fetch(`/api/send_mail`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body:
|
body: JSON.stringify(payload)
|
||||||
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,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
sendMailModel.value = {
|
sendMailModel.value = {
|
||||||
fromName: "",
|
fromName: "",
|
||||||
@@ -86,11 +144,13 @@ const send = async () => {
|
|||||||
contentType: 'text',
|
contentType: 'text',
|
||||||
content: "",
|
content: "",
|
||||||
}
|
}
|
||||||
|
isPreview.value = false
|
||||||
|
message.success(t("successSend"));
|
||||||
|
indexTab.value = 'sendbox'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
} finally {
|
} finally {
|
||||||
message.success(t("successSend"));
|
sending.value = false
|
||||||
indexTab.value = 'sendbox'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +218,7 @@ onMounted(async () => {
|
|||||||
{{ t('send_balance') }}: {{ settings.send_balance }}
|
{{ t('send_balance') }}: {{ settings.send_balance }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-flex justify="end">
|
<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>
|
</n-flex>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<n-form :model="sendMailModel">
|
<n-form :model="sendMailModel">
|
||||||
|
|||||||
Reference in New Issue
Block a user