feat: support send mail from admin portal(no balance limit) (#524)

This commit is contained in:
Dream Hunter
2024-12-22 15:40:26 +08:00
committed by GitHub
parent 0b48baff6d
commit a9bb8785ba
9 changed files with 245 additions and 9 deletions

View File

@@ -3,7 +3,9 @@
# main(v0.8.2)
- feat: |Doc| 修复文档中的一些错误
- fix: |Doc| 修复文档中的一些错误
- fix: |Github Action| 修复 frontend 部署分支错误的问题
- fix: admin 发送邮件功能
# v0.8.2

View File

@@ -23,6 +23,7 @@ import Telegram from './admin/Telegram.vue';
import Webhook from './admin/Webhook.vue';
import MailWebhook from './admin/MailWebhook.vue';
import WorkerConfig from './admin/WorkerConfig.vue';
import SendMail from './admin/SendMail.vue';
const {
adminAuth, showAdminAuth, adminTab, loading,
@@ -45,6 +46,7 @@ const { t } = useI18n({
accessHeader: 'Admin Password',
accessTip: 'Please enter the admin password',
mails: 'Emails',
sendMail: 'Send Mail',
qucickSetup: 'Quick Setup',
account: 'Account',
account_create: 'Create Account',
@@ -70,6 +72,7 @@ const { t } = useI18n({
accessHeader: 'Admin 密码',
accessTip: '请输入 Admin 密码',
mails: '邮件',
sendMail: '发送邮件',
qucickSetup: '快速设置',
account: '账号',
account_create: '创建账号',
@@ -172,6 +175,9 @@ onMounted(async () => {
<n-tab-pane name="sendBox" :tab="t('sendBox')">
<SendBox />
</n-tab-pane>
<n-tab-pane name="sendMail" :tab="t('sendMail')">
<SendMail />
</n-tab-pane>
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
<MailWebhook />
</n-tab-pane>

View File

@@ -0,0 +1,199 @@
<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { useI18n } from 'vue-i18n'
import { onBeforeUnmount, ref, shallowRef } from 'vue'
import { useSessionStorage } from '@vueuse/core'
import { api } from '../../api'
const message = useMessage()
const isPreview = ref(false)
const editorRef = shallowRef()
const sendMailModel = useSessionStorage('sendMailByAdminModel', {
fromName: "",
fromMail: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
});
const { t } = useI18n({
locale: 'zh',
messages: {
en: {
successSend: 'Please check your sendbox. If failed, please try again later.',
fromName: 'Your Name and Address, leave Name blank to use email address',
toName: 'Recipient Name and Address, leave Name blank to use email address',
subject: 'Subject',
options: 'Options',
edit: 'Edit',
preview: 'Preview',
content: 'Content',
send: 'Send',
text: 'Text',
html: 'HTML',
'rich text': 'Rich Text',
tooLarge: 'Too large file, please upload file less than 1MB.',
},
zh: {
successSend: '请查看您的发件箱, 如果失败, 请检查稍后重试。',
fromName: '你的名称和地址,名称不填写则使用邮箱地址',
toName: '收件人名称和地址,名称不填写则使用邮箱地址',
subject: '主题',
options: '选项',
edit: '编辑',
preview: '预览',
content: '内容',
send: '发送',
text: '文本',
html: 'HTML',
'rich text': '富文本',
tooLarge: '文件过大, 请上传小于1MB的文件。',
}
}
});
const contentTypes = [
{ label: t('text'), value: 'text' },
{ label: t('html'), value: 'html' },
{ label: t('rich text'), value: 'rich' },
]
const send = async () => {
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,
})
})
sendMailModel.value = {
fromName: "",
fromMail: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
}
} catch (error) {
message.error(error.message || "error");
} finally {
message.success(t("successSend"));
}
}
const toolbarConfig = {
excludeKeys: ["uploadVideo"]
}
const editorConfig = {
MENU_CONF: {
'uploadImage': {
async customUpload() {
message.error(t('tooLarge'))
},
maxFileSize: 1 * 1024 * 1024,
base64LimitSize: 1 * 1024 * 1024,
}
}
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor;
}
</script>
<template>
<div class="center">
<n-card :bordered="false" embedded>
<n-flex justify="end">
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
</n-flex>
<div class="left">
<n-form :model="sendMailModel">
<n-form-item :label="t('fromName')" label-placement="top">
<n-input-group>
<n-input v-model:value="sendMailModel.fromName" />
<n-input v-model:value="sendMailModel.fromMail" />
</n-input-group>
</n-form-item>
<n-form-item :label="t('toName')" label-placement="top">
<n-input-group>
<n-input v-model:value="sendMailModel.toName" />
<n-input v-model:value="sendMailModel.toMail" />
</n-input-group>
</n-form-item>
<n-form-item :label="t('subject')" label-placement="top">
<n-input v-model:value="sendMailModel.subject" />
</n-form-item>
<n-form-item :label="t('options')" label-placement="top">
<n-radio-group v-model:value="sendMailModel.contentType">
<n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value"
:label="option.label" />
</n-radio-group>
<n-button v-if="sendMailModel.contentType != 'text'" @click="isPreview = !isPreview"
style="margin-left: 10px;">
{{ isPreview ? t('edit') : t('preview') }}
</n-button>
</n-form-item>
<n-form-item :label="t('content')" label-placement="top">
<n-card :bordered="false" embedded v-if="isPreview">
<div v-html="sendMailModel.content" />
</n-card>
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
<Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig"
:editor="editorRef" mode="default" />
<Editor style="height: 500px; overflow-y: hidden;" v-model="sendMailModel.content"
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
</div>
<n-input v-else type="textarea" v-model:value="sendMailModel.content" :autosize="{
minRows: 3
}" />
</n-form-item>
</n-form>
</div>
</n-card>
</div>
</template>
<style scoped>
.n-card {
max-width: 800px;
}
.n-button {
text-align: left;
margin-right: 10px;
}
.center {
display: flex;
text-align: center;
place-items: center;
justify-content: center;
}
.left {
text-align: left;
place-items: left;
justify-content: left;
}
</style>

View File

@@ -11,6 +11,7 @@ import webhook_settings from './webhook_settings'
import mail_webhook_settings from './mail_webhook_settings'
import oauth2_settings from './oauth2_settings'
import worker_config from './worker_config'
import { sendMailbyAdmin } from './send_mail'
export const api = new Hono<HonoCustomType>()
@@ -330,3 +331,6 @@ api.post("/admin/mail_webhook/test", mail_webhook_settings.testWebhookSettings);
// worker config
api.get("/admin/worker/configs", worker_config.getConfig);
// send mail by admin
api.post("/admin/send_mail", sendMailbyAdmin);

View File

@@ -0,0 +1,22 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { sendMail } from "../mails_api/send_mail_api";
export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => {
const {
from_name, from_mail,
to_mail, to_name,
subject, content, is_html
} = await c.req.json();
await sendMail(c, from_mail, {
from_name: from_name,
to_name: to_name,
to_mail: to_mail,
subject: subject,
content: content,
is_html: is_html,
}, {
isAdmin: true
})
return c.json({ status: "ok" });
}

View File

@@ -94,6 +94,9 @@ export const sendMail = async (
reqJson: {
from_name: string, to_mail: string, to_name: string,
subject: string, content: string, is_html: boolean
},
options?: {
isAdmin?: boolean
}
): Promise<void> => {
if (!address) {
@@ -107,7 +110,7 @@ export const sendMail = async (
}
const user_role = c.get("userRolePayload");
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
if (!is_no_limit_send_balance) {
if (!is_no_limit_send_balance && !options?.isAdmin) {
// check permission
const balance = await c.env.DB.prepare(
`SELECT balance FROM address_sender
@@ -158,7 +161,7 @@ export const sendMail = async (
throw new Error("Please enable resend or verified address list")
}
// update balance
if (!sendByVerifiedAddressList && !is_no_limit_send_balance) {
if (!sendByVerifiedAddressList && !is_no_limit_send_balance && !options?.isAdmin) {
try {
const { success } = await c.env.DB.prepare(
`UPDATE address_sender SET balance = balance - 1 where address = ?`

View File

@@ -94,8 +94,8 @@ export default {
const jwt = await Jwt.sign({
user_email: email,
user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
// 90 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET, "HS256")
return c.json({

View File

@@ -193,8 +193,8 @@ export default {
const jwt = await Jwt.sign({
user_email: user_email,
user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
// 90 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET, "HS256")
return c.json({

View File

@@ -165,8 +165,8 @@ export default {
const jwt = await Jwt.sign({
user_email: email,
user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
// 90 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET, "HS256")
return c.json({