mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-06 16:07:14 +08:00
feat: support send mail from admin portal(no balance limit) (#524)
This commit is contained in:
+3
-1
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
# main(v0.8.2)
|
# main(v0.8.2)
|
||||||
|
|
||||||
- feat: |Doc| 修复文档中的一些错误
|
- fix: |Doc| 修复文档中的一些错误
|
||||||
|
- fix: |Github Action| 修复 frontend 部署分支错误的问题
|
||||||
|
- fix: admin 发送邮件功能
|
||||||
|
|
||||||
# v0.8.2
|
# v0.8.2
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import Telegram from './admin/Telegram.vue';
|
|||||||
import Webhook from './admin/Webhook.vue';
|
import Webhook from './admin/Webhook.vue';
|
||||||
import MailWebhook from './admin/MailWebhook.vue';
|
import MailWebhook from './admin/MailWebhook.vue';
|
||||||
import WorkerConfig from './admin/WorkerConfig.vue';
|
import WorkerConfig from './admin/WorkerConfig.vue';
|
||||||
|
import SendMail from './admin/SendMail.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
adminAuth, showAdminAuth, adminTab, loading,
|
adminAuth, showAdminAuth, adminTab, loading,
|
||||||
@@ -45,6 +46,7 @@ const { t } = useI18n({
|
|||||||
accessHeader: 'Admin Password',
|
accessHeader: 'Admin Password',
|
||||||
accessTip: 'Please enter the admin password',
|
accessTip: 'Please enter the admin password',
|
||||||
mails: 'Emails',
|
mails: 'Emails',
|
||||||
|
sendMail: 'Send Mail',
|
||||||
qucickSetup: 'Quick Setup',
|
qucickSetup: 'Quick Setup',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
account_create: 'Create Account',
|
account_create: 'Create Account',
|
||||||
@@ -70,6 +72,7 @@ const { t } = useI18n({
|
|||||||
accessHeader: 'Admin 密码',
|
accessHeader: 'Admin 密码',
|
||||||
accessTip: '请输入 Admin 密码',
|
accessTip: '请输入 Admin 密码',
|
||||||
mails: '邮件',
|
mails: '邮件',
|
||||||
|
sendMail: '发送邮件',
|
||||||
qucickSetup: '快速设置',
|
qucickSetup: '快速设置',
|
||||||
account: '账号',
|
account: '账号',
|
||||||
account_create: '创建账号',
|
account_create: '创建账号',
|
||||||
@@ -172,6 +175,9 @@ onMounted(async () => {
|
|||||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||||
<SendBox />
|
<SendBox />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="sendMail" :tab="t('sendMail')">
|
||||||
|
<SendMail />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
|
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
|
||||||
<MailWebhook />
|
<MailWebhook />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -11,6 +11,7 @@ import webhook_settings from './webhook_settings'
|
|||||||
import mail_webhook_settings from './mail_webhook_settings'
|
import mail_webhook_settings from './mail_webhook_settings'
|
||||||
import oauth2_settings from './oauth2_settings'
|
import oauth2_settings from './oauth2_settings'
|
||||||
import worker_config from './worker_config'
|
import worker_config from './worker_config'
|
||||||
|
import { sendMailbyAdmin } from './send_mail'
|
||||||
|
|
||||||
export const api = new Hono<HonoCustomType>()
|
export const api = new Hono<HonoCustomType>()
|
||||||
|
|
||||||
@@ -330,3 +331,6 @@ api.post("/admin/mail_webhook/test", mail_webhook_settings.testWebhookSettings);
|
|||||||
|
|
||||||
// worker config
|
// worker config
|
||||||
api.get("/admin/worker/configs", worker_config.getConfig);
|
api.get("/admin/worker/configs", worker_config.getConfig);
|
||||||
|
|
||||||
|
// send mail by admin
|
||||||
|
api.post("/admin/send_mail", sendMailbyAdmin);
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
}
|
||||||
@@ -94,6 +94,9 @@ export const sendMail = async (
|
|||||||
reqJson: {
|
reqJson: {
|
||||||
from_name: string, to_mail: string, to_name: string,
|
from_name: string, to_mail: string, to_name: string,
|
||||||
subject: string, content: string, is_html: boolean
|
subject: string, content: string, is_html: boolean
|
||||||
|
},
|
||||||
|
options?: {
|
||||||
|
isAdmin?: boolean
|
||||||
}
|
}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
@@ -107,7 +110,7 @@ export const sendMail = async (
|
|||||||
}
|
}
|
||||||
const user_role = c.get("userRolePayload");
|
const user_role = c.get("userRolePayload");
|
||||||
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
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
|
// check permission
|
||||||
const balance = await c.env.DB.prepare(
|
const balance = await c.env.DB.prepare(
|
||||||
`SELECT balance FROM address_sender
|
`SELECT balance FROM address_sender
|
||||||
@@ -158,7 +161,7 @@ export const sendMail = async (
|
|||||||
throw new Error("Please enable resend or verified address list")
|
throw new Error("Please enable resend or verified address list")
|
||||||
}
|
}
|
||||||
// update balance
|
// update balance
|
||||||
if (!sendByVerifiedAddressList && !is_no_limit_send_balance) {
|
if (!sendByVerifiedAddressList && !is_no_limit_send_balance && !options?.isAdmin) {
|
||||||
try {
|
try {
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`UPDATE address_sender SET balance = balance - 1 where address = ?`
|
`UPDATE address_sender SET balance = balance - 1 where address = ?`
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ export default {
|
|||||||
const jwt = await Jwt.sign({
|
const jwt = await Jwt.sign({
|
||||||
user_email: email,
|
user_email: email,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
// 30 days expire in seconds
|
// 90 days expire in seconds
|
||||||
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
}, c.env.JWT_SECRET, "HS256")
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
return c.json({
|
return c.json({
|
||||||
|
|||||||
@@ -193,8 +193,8 @@ export default {
|
|||||||
const jwt = await Jwt.sign({
|
const jwt = await Jwt.sign({
|
||||||
user_email: user_email,
|
user_email: user_email,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
// 30 days expire in seconds
|
// 90 days expire in seconds
|
||||||
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
}, c.env.JWT_SECRET, "HS256")
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
return c.json({
|
return c.json({
|
||||||
|
|||||||
@@ -165,8 +165,8 @@ export default {
|
|||||||
const jwt = await Jwt.sign({
|
const jwt = await Jwt.sign({
|
||||||
user_email: email,
|
user_email: email,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
// 30 days expire in seconds
|
// 90 days expire in seconds
|
||||||
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
}, c.env.JWT_SECRET, "HS256")
|
}, c.env.JWT_SECRET, "HS256")
|
||||||
return c.json({
|
return c.json({
|
||||||
|
|||||||
Reference in New Issue
Block a user