diff --git a/frontend/src/views/admin/SendMail.vue b/frontend/src/views/admin/SendMail.vue index d9c59ca9..013af92e 100644 --- a/frontend/src/views/admin/SendMail.vue +++ b/frontend/src/views/admin/SendMail.vue @@ -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) => {
- {{ t('send') }} + {{ t('send') }}
diff --git a/frontend/src/views/index/SendMail.vue b/frontend/src/views/index/SendMail.vue index 2231e467..78eb3628 100644 --- a/frontend/src/views/index/SendMail.vue +++ b/frontend/src/views/index/SendMail.vue @@ -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 }} - {{ t('send') }} + {{ t('send') }}