From 621476cb792ec9c324779782727cd53baaadeed4 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Thu, 15 Aug 2024 00:23:31 +0800 Subject: [PATCH] feat: update webhook to support global webhook (#407) --- frontend/src/components/MailBox.vue | 14 +- frontend/src/components/SendBox.vue | 6 +- frontend/src/components/WebhookComponent.vue | 179 ++++++++++++++++++ frontend/src/views/Admin.vue | 12 +- frontend/src/views/Index.vue | 4 +- frontend/src/views/admin/MailWebhook.vue | 30 +++ frontend/src/views/common/Login.vue | 4 +- frontend/src/views/index/SendMail.vue | 13 +- frontend/src/views/index/Webhook.vue | 127 ++----------- vitepress-docs/docs/zh/guide/ui/worker.md | 2 + worker/src/admin_api/index.ts | 11 ++ worker/src/admin_api/mail_webhook_settings.ts | 55 ++++++ worker/src/common.ts | 75 ++++++++ worker/src/constants.ts | 1 + worker/src/email/index.ts | 4 +- worker/src/mails_api/webhook_settings.ts | 78 +------- worker/src/models/index.ts | 17 ++ 17 files changed, 416 insertions(+), 216 deletions(-) create mode 100644 frontend/src/components/WebhookComponent.vue create mode 100644 frontend/src/views/admin/MailWebhook.vue create mode 100644 worker/src/admin_api/mail_webhook_settings.ts diff --git a/frontend/src/components/MailBox.vue b/frontend/src/components/MailBox.vue index efe913dd..2c719546 100644 --- a/frontend/src/components/MailBox.vue +++ b/frontend/src/components/MailBox.vue @@ -14,37 +14,37 @@ const props = defineProps({ enableUserDeleteEmail: { type: Boolean, default: false, - requried: false + required: false }, showEMailTo: { type: Boolean, default: true, - requried: false + required: false }, fetchMailData: { type: Function, default: () => { }, - requried: true + required: true }, deleteMail: { type: Function, default: () => { }, - requried: false + required: false }, showReply: { type: Boolean, default: false, - requried: false + required: false }, showSaveS3: { type: Boolean, default: false, - requried: false + required: false }, saveToS3: { type: Function, default: (mail_id, filename, blob) => { }, - requried: false + required: false }, }) diff --git a/frontend/src/components/SendBox.vue b/frontend/src/components/SendBox.vue index 8a85ff24..9bfecebb 100644 --- a/frontend/src/components/SendBox.vue +++ b/frontend/src/components/SendBox.vue @@ -12,7 +12,7 @@ const props = defineProps({ enableUserDeleteEmail: { type: Boolean, default: false, - requried: false + required: false }, showEMailFrom: { type: Boolean, @@ -21,12 +21,12 @@ const props = defineProps({ fetchMailData: { type: Function, default: () => { }, - requried: true + required: true }, deleteMail: { type: Function, default: () => { }, - requried: false + required: false }, }) diff --git a/frontend/src/components/WebhookComponent.vue b/frontend/src/components/WebhookComponent.vue new file mode 100644 index 00000000..0361ce88 --- /dev/null +++ b/frontend/src/components/WebhookComponent.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 6f2ea732..301d48f1 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -20,6 +20,7 @@ import Maintenance from './admin/Maintenance.vue'; import Appearance from './common/Appearance.vue'; import Telegram from './admin/Telegram.vue'; import Webhook from './admin/Webhook.vue'; +import MailWebhook from './admin/MailWebhook.vue'; const { adminAuth, showAdminAuth, adminTab, loading, @@ -51,12 +52,13 @@ const { t } = useI18n({ senderAccess: 'Sender Access Control', sendBox: 'Send Box', telegram: 'Telegram Bot', - webhook: 'Webhook', + webhookSettings: 'Webhook Settings', statistics: 'Statistics', maintenance: 'Maintenance', appearance: 'Appearance', about: 'About', ok: 'OK', + mailWebhook: 'Mail Webhook', }, zh: { accessHeader: 'Admin 密码', @@ -72,12 +74,13 @@ const { t } = useI18n({ senderAccess: '发件权限控制', sendBox: '发件箱', telegram: '电报机器人', - webhook: 'Webhook', + webhookSettings: 'Webhook 设置', statistics: '统计', maintenance: '维护', appearance: '外观', about: '关于', ok: '确定', + mailWebhook: '邮件 Webhook', } } }); @@ -119,7 +122,7 @@ onMounted(async () => { - + @@ -142,6 +145,9 @@ onMounted(async () => { + + + diff --git a/frontend/src/views/Index.vue b/frontend/src/views/Index.vue index 3b7b64ac..0bcd6fc9 100644 --- a/frontend/src/views/Index.vue +++ b/frontend/src/views/Index.vue @@ -29,6 +29,7 @@ const { t } = useI18n({ about: 'About', s3Attachment: 'S3 Attachment', saveToS3Success: 'save to s3 success', + webhookSettings: 'Webhook Settings', }, zh: { mailbox: '收件箱', @@ -39,6 +40,7 @@ const { t } = useI18n({ about: '关于', s3Attachment: 'S3附件', saveToS3Success: '保存到s3成功', + webhookSettings: 'Webhook 设置', } } }); @@ -102,7 +104,7 @@ const saveToS3 = async (mail_id, filename, blob) => { - + diff --git a/frontend/src/views/admin/MailWebhook.vue b/frontend/src/views/admin/MailWebhook.vue new file mode 100644 index 00000000..f3abef74 --- /dev/null +++ b/frontend/src/views/admin/MailWebhook.vue @@ -0,0 +1,30 @@ + + + diff --git a/frontend/src/views/common/Login.vue b/frontend/src/views/common/Login.vue index c7ff2ff0..77832f56 100644 --- a/frontend/src/views/common/Login.vue +++ b/frontend/src/views/common/Login.vue @@ -15,7 +15,7 @@ const props = defineProps({ bindUserAddress: { type: Function, default: async () => { await api.bindUserAddress(); }, - requried: true + required: true }, newAddressPath: { type: Function, @@ -29,7 +29,7 @@ const props = defineProps({ }), }); }, - requried: true + required: true }, }) diff --git a/frontend/src/views/index/SendMail.vue b/frontend/src/views/index/SendMail.vue index d2741767..3aaeff0c 100644 --- a/frontend/src/views/index/SendMail.vue +++ b/frontend/src/views/index/SendMail.vue @@ -151,16 +151,15 @@ onMounted(async () => { {{ t('requestAccess') }} -
- + {{ t('send_balance') }}: {{ settings.send_balance }} -
+ {{ t('send') }} -
+
@@ -232,9 +231,7 @@ onMounted(async () => { justify-content: left; } -.right { - text-align: right; - place-items: right; - justify-content: right; +.n-alert { + margin-bottom: 10px; } diff --git a/frontend/src/views/index/Webhook.vue b/frontend/src/views/index/Webhook.vue index 77117ed9..d07c5f9f 100644 --- a/frontend/src/views/index/Webhook.vue +++ b/frontend/src/views/index/Webhook.vue @@ -1,129 +1,28 @@ - - diff --git a/vitepress-docs/docs/zh/guide/ui/worker.md b/vitepress-docs/docs/zh/guide/ui/worker.md index 1f0d05de..13083026 100644 --- a/vitepress-docs/docs/zh/guide/ui/worker.md +++ b/vitepress-docs/docs/zh/guide/ui/worker.md @@ -35,6 +35,8 @@ > [!NOTE] > 注意字符串格式的变量的最外层的引号是不需要的 + > + > - 对于 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]` ![worker-var](/ui_install/worker-var.png) diff --git a/worker/src/admin_api/index.ts b/worker/src/admin_api/index.ts index 7a1c6102..e3a1b361 100644 --- a/worker/src/admin_api/index.ts +++ b/worker/src/admin_api/index.ts @@ -8,6 +8,7 @@ import { CONSTANTS } from '../constants' import cleanup_api from './cleanup_api' import admin_user_api from './admin_user_api' import webhook_settings from './webhook_settings' +import mail_webhook_settings from './mail_webhook_settings' export const api = new Hono() @@ -291,9 +292,12 @@ api.post('/admin/account_settings', async (c) => { }) }) +// cleanup api.post('/admin/cleanup', cleanup_api.cleanup) api.get('/admin/auto_cleanup', cleanup_api.getCleanup) api.post('/admin/auto_cleanup', cleanup_api.saveCleanup) + +// user settings api.get('/admin/user_settings', admin_user_api.getSetting) api.post('/admin/user_settings', admin_user_api.saveSetting) api.get('/admin/users', admin_user_api.getUsers) @@ -302,5 +306,12 @@ api.post('/admin/users', admin_user_api.createUser) api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword) api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c))) api.post('/admin/user_roles', admin_user_api.updateUserRoles) + +// webhook settings api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings); api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings); + +// mail webhook settings +api.get("/admin/mail_webhook/settings", mail_webhook_settings.getWebhookSettings); +api.post("/admin/mail_webhook/settings", mail_webhook_settings.saveWebhookSettings); +api.post("/admin/mail_webhook/test", mail_webhook_settings.testWebhookSettings); diff --git a/worker/src/admin_api/mail_webhook_settings.ts b/worker/src/admin_api/mail_webhook_settings.ts new file mode 100644 index 00000000..6c0d4710 --- /dev/null +++ b/worker/src/admin_api/mail_webhook_settings.ts @@ -0,0 +1,55 @@ +import { Context } from "hono"; +import { HonoCustomType } from "../types"; +import { CONSTANTS } from "../constants"; +import { WebhookSettings } from "../models"; +import { getBooleanValue } from "../utils"; +import { commonParseMail, sendWebhook } from "../common"; + +async function getWebhookSettings(c: Context): Promise { + if (!c.env.KV) { + return c.text("KV is not available", 400); + } + if (!getBooleanValue(c.env.ENABLE_WEBHOOK)) { + return c.text("Webhook is disabled", 403); + } + const settings = await c.env.KV.get( + CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, "json" + ) || new WebhookSettings(); + return c.json(settings); +} + +async function saveWebhookSettings(c: Context): Promise { + const settings = await c.req.json(); + await c.env.KV.put( + CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, + JSON.stringify(settings)); + return c.json({ success: true }) +} + +async function testWebhookSettings(c: Context): Promise { + const settings = await c.req.json(); + // random raw email + const raw = await c.env.DB.prepare( + `SELECT raw FROM raw_mails ORDER BY RANDOM() LIMIT 1` + ).first("raw"); + + const parsedEmail = await commonParseMail(raw); + const res = await sendWebhook(settings, { + from: parsedEmail?.sender || "test@test.com", + to: "admin@test.com", + subject: parsedEmail?.subject || "test subject", + raw: raw || "test raw email", + parsedText: parsedEmail?.text || "test parsed text", + parsedHtml: parsedEmail?.html || "test parsed html" + }); + if (!res.success) { + return c.text(res.message || "send webhook error", 400); + } + return c.json({ success: true }); +} + +export default { + getWebhookSettings, + saveWebhookSettings, + testWebhookSettings, +} diff --git a/worker/src/common.ts b/worker/src/common.ts index cdc26e41..c03b2794 100644 --- a/worker/src/common.ts +++ b/worker/src/common.ts @@ -4,6 +4,8 @@ import { Jwt } from 'hono/utils/jwt' import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains } from './utils'; import { HonoCustomType, UserRole } from './types'; import { unbindTelegramByAddress } from './telegram_api/common'; +import { CONSTANTS } from './constants'; +import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models'; const DEFAULT_NAME_REGEX = /[^a-z0-9]/g; @@ -273,3 +275,76 @@ export const getAllowDomains = async (c: Context): Promise { + // send webhook + let body = settings.body; + for (const key of Object.keys(formatMap)) { + /* eslint-disable no-useless-escape */ + body = body.replace( + new RegExp(`\\$\\{${key}\\}`, "g"), + JSON.stringify( + formatMap[key as keyof WebhookMail] + ).replace(/^"(.*)"$/, '\$1') + ); + /* eslint-enable no-useless-escape */ + } + const response = await fetch(settings.url, { + method: settings.method, + headers: JSON.parse(settings.headers), + body: body + }); + if (!response.ok) { + console.log("send webhook error", response.status, response.statusText); + return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` }; + } + return { success: true } +} + +export async function triggerWebhook( + c: Context, + address: string, + raw_mail: string +): Promise { + if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) { + return + } + const webhookList: WebhookSettings[] = [] + + // admin mail webhook + const adminMailWebhookSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, "json"); + if (adminMailWebhookSettings?.enabled) { + webhookList.push(adminMailWebhookSettings) + } + + // user mail webhook + const adminSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); + if (adminSettings?.allowList.includes(address)) { + const settings = await c.env.KV.get( + `${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json" + ); + if (settings?.enabled) { + webhookList.push(settings) + } + } + + // no webhook + if (webhookList.length === 0) { + return + } + const parsedEmail = await commonParseMail(raw_mail); + const webhookMail = { + from: parsedEmail?.sender || "", + to: address, + subject: parsedEmail?.subject || "", + raw: raw_mail, + parsedText: parsedEmail?.text || "", + parsedHtml: parsedEmail?.html || "" + } + for (const settings of webhookList) { + const res = await sendWebhook(settings, webhookMail); + if (!res.success) { + console.error(res.message); + } + } +} diff --git a/worker/src/constants.ts b/worker/src/constants.ts index a2648559..b7d7ff9c 100644 --- a/worker/src/constants.ts +++ b/worker/src/constants.ts @@ -14,4 +14,5 @@ export const CONSTANTS = { WEBHOOK_KV_SETTINGS_KEY: "temp-mail-webhook-settings", WEBHOOK_KV_USER_SETTINGS_KEY: "temp-mail-webhook-user-settings", EMAIL_KV_BLACK_LIST: "temp-mail-email-black-list", + WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY: "temp-mail-webhook-admin-mail-settings", } diff --git a/worker/src/email/index.ts b/worker/src/email/index.ts index 4fe684d4..4405f007 100644 --- a/worker/src/email/index.ts +++ b/worker/src/email/index.ts @@ -4,8 +4,8 @@ import { getEnvStringList } from "../utils"; import { sendMailToTelegram } from "../telegram_api"; import { Bindings, HonoCustomType } from "../types"; import { auto_reply } from "./auto_reply"; -import { trigerWebhook } from "../mails_api/webhook_settings"; import { isBlocked } from "./black_list"; +import { triggerWebhook } from "../common"; async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) { @@ -48,7 +48,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu // send webhook try { - await trigerWebhook( + await triggerWebhook( { env: env } as Context, message.to, rawEmail ); diff --git a/worker/src/mails_api/webhook_settings.ts b/worker/src/mails_api/webhook_settings.ts index b4743520..8322e6f1 100644 --- a/worker/src/mails_api/webhook_settings.ts +++ b/worker/src/mails_api/webhook_settings.ts @@ -1,26 +1,9 @@ import { Context } from "hono"; import { HonoCustomType } from "../types"; import { CONSTANTS } from "../constants"; -import { AdminWebhookSettings, WebhookMail } from "../models"; +import { AdminWebhookSettings, WebhookSettings } from "../models"; import { getBooleanValue } from "../utils"; -import { commonParseMail } from "../common"; - - -class WebhookSettings { - url: string = '' - method: string = 'POST' - headers: string = JSON.stringify({ - "Content-Type": "application/json" - }, null, 2) - body: string = JSON.stringify({ - "from": "${from}", - "to": "${to}", - "subject": "${subject}", - "raw": "${raw}", - "parsedText": "${parsedText}", - "parsedHtml": "${parsedHtml}", - }, null, 2) -} +import { commonParseMail, sendWebhook } from "../common"; async function getWebhookSettings(c: Context): Promise { @@ -55,63 +38,6 @@ async function saveWebhookSettings(c: Context): Promise { - // send webhook - let body = settings.body; - for (const key of Object.keys(formatMap)) { - /* eslint-disable no-useless-escape */ - body = body.replace( - new RegExp(`\\$\\{${key}\\}`, "g"), - JSON.stringify( - formatMap[key as keyof WebhookMail] - ).replace(/^"(.*)"$/, '\$1') - ); - /* eslint-enable no-useless-escape */ - } - const response = await fetch(settings.url, { - method: settings.method, - headers: JSON.parse(settings.headers), - body: body - }); - if (!response.ok) { - console.log("send webhook error", response.status, response.statusText); - return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` }; - } - return { success: true } -} - -export async function trigerWebhook( - c: Context, - address: string, - raw_mail: string -): Promise { - if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) { - return - } - const adminSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); - if (!adminSettings?.allowList.includes(address)) { - return; - } - const settings = await c.env.KV.get( - `${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json" - ); - if (!settings) { - return; - } - const parsedEmail = await commonParseMail(raw_mail); - const res = await sendWebhook(settings, { - from: parsedEmail?.sender || "", - to: address, - subject: parsedEmail?.subject || "", - raw: raw_mail, - parsedText: parsedEmail?.text || "", - parsedHtml: parsedEmail?.html || "" - }); - if (!res.success) { - console.log(res.message); - } -} - async function testWebhookSettings(c: Context): Promise { const settings = await c.req.json(); const { address } = c.get("jwtPayload"); diff --git a/worker/src/models/index.ts b/worker/src/models/index.ts index 6c5a065a..eb2d799f 100644 --- a/worker/src/models/index.ts +++ b/worker/src/models/index.ts @@ -119,3 +119,20 @@ export class UserInfo { this.userEmail = userEmail; } } + +export class WebhookSettings { + enabled: boolean = false + url: string = '' + method: string = 'POST' + headers: string = JSON.stringify({ + "Content-Type": "application/json" + }, null, 2) + body: string = JSON.stringify({ + "from": "${from}", + "to": "${to}", + "subject": "${subject}", + "raw": "${raw}", + "parsedText": "${parsedText}", + "parsedHtml": "${parsedHtml}", + }, null, 2) +}