新增TG邮件查看HTML

This commit is contained in:
eoao
2025-10-22 21:33:45 +08:00
parent 140d451472
commit b8b1bee015
15 changed files with 332 additions and 62 deletions
-1
View File
@@ -14,7 +14,6 @@ app.put('/role/setDefault', async (c) => {
return c.json(result.ok());
});
app.put('/role/set', async (c) => {
await roleService.setRole(c, await c.req.json());
return c.json(result.ok());
+8
View File
@@ -0,0 +1,8 @@
import app from '../hono/hono';
import telegramService from '../service/telegram-service';
app.get('/telegram/getEmail/:token', async (c) => {
const content = await telegramService.getEmailContent(c, c.req.param());
c.header('Cache-Control', 'public, max-age=604800, immutable');
return c.html(content)
});
+4 -40
View File
@@ -7,16 +7,11 @@ import constant from '../const/constant';
import fileUtils from '../utils/file-utils';
import { emailConst, isDel, roleConst, settingConst } from '../const/entity-const';
import emailUtils from '../utils/email-utils';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import roleService from '../service/role-service';
import verifyUtils from '../utils/verify-utils';
import r2Service from '../service/r2-service';
import userService from '../service/user-service';
dayjs.extend(utc);
dayjs.extend(timezone);
import telegramService from '../service/telegram-service';
export async function email(message, env, ctx) {
@@ -24,7 +19,6 @@ export async function email(message, env, ctx) {
const {
receive,
tgBotToken,
tgChatId,
tgBotStatus,
forwardStatus,
@@ -179,42 +173,12 @@ export async function email(message, env, ctx) {
}
//转发到TG
if (tgBotStatus === settingConst.tgBotStatus.OPEN && tgChatId) {
const tgMessage = `<b>${params.subject}</b>
<b>发件人:</b>${params.name} &lt;${params.sendEmail}&gt;
<b>收件人:\u200B</b>${message.to}
<b>时间:</b>${dayjs.utc(emailRow.createTime).tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm')}
${params.text || emailUtils.htmlToText(params.content) || ''}
`;
const tgChatIds = tgChatId.split(',');
await Promise.all(tgChatIds.map(async chatId => {
try {
const res = await fetch(`https://api.telegram.org/bot${tgBotToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
chat_id: chatId,
parse_mode: 'HTML',
text: tgMessage
})
});
if (!res.ok) {
console.error(`转发 Telegram 失败: chatId=${chatId}, 状态码=${res.status}`);
}
} catch (e) {
console.error(`转发 Telegram 失败: chatId=${chatId}`, e);
}
}));
await telegramService.sendEmailToBot({ env }, emailRow)
}
//转发到其他邮箱
if (forwardStatus === settingConst.forwardStatus.OPEN && forwardEmail) {
const emails = forwardEmail.split(',');
+2 -1
View File
@@ -41,6 +41,7 @@ export const setting = sqliteTable('setting', {
s3AccessKey: text('s3_access_key').default('').notNull(),
s3SecretKey: text('s3_secret_key').default('').notNull(),
kvStorage: integer('kv_storage').default(1).notNull(),
forcePathStyle: integer('force_path_style').default(1).notNull()
forcePathStyle: integer('force_path_style').default(1).notNull(),
customDomain: text('custom_domain').default('').notNull()
});
export default setting
+1
View File
@@ -18,4 +18,5 @@ import '../api/init-api'
import '../api/analysis-api'
import '../api/reg-key-api'
import '../api/public-api'
import '../api/telegram-api'
export default app;
+3 -2
View File
@@ -31,10 +31,11 @@ const init = {
try {
await c.env.db.batch([
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN force_path_style INTEGER NOT NULL DEFAULT 1;`),
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN kv_storage INTEGER NOT NULL DEFAULT 1;`)
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN kv_storage INTEGER NOT NULL DEFAULT 1;`),
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN custom_domain TEXT NOT NULL DEFAULT '';`)
]);
} catch (e) {
console.error(e.message)
console.error(e)
}
},
+5 -5
View File
@@ -15,7 +15,9 @@ const exclude = [
'/setting/websiteConfig',
'/webhooks',
'/init',
'/public/genToken'
'/public/genToken',
'/telegram',
'/test'
];
const requirePerms = [
@@ -85,10 +87,6 @@ app.use('*', async (c, next) => {
const path = c.req.path;
if (path.startsWith('/test')) {
return await next();
}
const index = exclude.findIndex(item => {
return path.startsWith(item);
});
@@ -162,7 +160,9 @@ app.use('*', async (c, next) => {
});
function permKeyToPaths(permKeys) {
const paths = [];
for (const key of permKeys) {
const routeList = premKey[key];
if (routeList && Array.isArray(routeList)) {
+109
View File
@@ -0,0 +1,109 @@
import orm from '../entity/orm';
import email from '../entity/email';
import settingService from './setting-service';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
import { eq } from 'drizzle-orm';
import jwtUtils from '../utils/jwt-utils';
import emailMsgTemplate from '../template/email-msg';
import emailTextTemplate from '../template/email-text';
import emailHtmlTemplate from '../template/email-html';
import verifyUtils from '../utils/verify-utils';
const telegramService = {
async getEmailById(c, params) {
const { emailId } = params
const emailRow = await orm(c).select().from(email).where(eq(email.emailId, emailId)).get();
if (emailRow) {
if (emailRow.content) {
return emailHtmlTemplate(emailRow.content || '')
} else {
return emailTextTemplate(emailRow.text || '')
}
} else {
return emailTextTemplate('The email does not exist')
}
},
async getEmailContent(c, params) {
const { token } = params
const result = await jwtUtils.verifyToken(c, token);
if (!result) {
return emailTextTemplate('Access denied')
}
const emailRow = await orm(c).select().from(email).where(eq(email.emailId, result.emailId)).get();
if (emailRow) {
if (emailRow.content) {
return emailHtmlTemplate(emailRow.content || '')
} else {
return emailTextTemplate(emailRow.text || '')
}
} else {
return emailTextTemplate('The email does not exist')
}
},
async sendEmailToBot(c, email) {
const { tgBotToken, tgChatId, customDomain, tgMsgTo, tgMsgFrom } = await settingService.query(c);
const tgChatIds = tgChatId.split(',');
const jwtToken = await jwtUtils.generateToken(c, { emailId: email.emailId })
const webAppUrl = verifyUtils.isDomain(customDomain) ? `https://${customDomain}/api/telegram/getEmail/${jwtToken}` : 'https://www.cloudflare.com/404'
await Promise.all(tgChatIds.map(async chatId => {
try {
const res = await fetch(`https://api.telegram.org/bot${tgBotToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
chat_id: chatId,
parse_mode: 'HTML',
text: emailMsgTemplate(email, tgMsgTo, tgMsgFrom),
reply_markup: {
inline_keyboard: [
[
{
text: '查看',
web_app: { url: webAppUrl }
}
]
]
}
})
});
if (!res.ok) {
console.error(`转发 Telegram 失败: chatId=${chatId}, 状态码=${res.status}`);
}
} catch (e) {
console.error(`转发 Telegram 失败: chatId=${chatId}`, e.message);
}
}));
}
}
export default telegramService
+139
View File
@@ -0,0 +1,139 @@
import { parseHTML } from 'linkedom';
export default function emailHtmlTemplate(html) {
const { document } = parseHTML(html);
document.querySelectorAll('script').forEach(script => script.remove());
html = document.documentElement.outerHTML;
return `<!DOCTYPE html>
<html lang='en' >
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
background: #FFF;
}
.content-box {
padding: 15px 10px;
width: 100%;
height: 100%;
overflow: auto; /* 改为 auto 允许滚动 */
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.content-html {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class='content-box'>
<div id='container' class='content-html'></div>
</div>
<script>
function renderHTML(html) {
const container = document.getElementById('container');
const shadowRoot = container.attachShadow({ mode: 'open' });
// 提取 <body> 的 style 属性
const bodyStyleRegex = /<body[^>]*style="([^"]*)"[^>]*>/i;
const bodyStyleMatch = html.match(bodyStyleRegex);
const bodyStyle = bodyStyleMatch ? bodyStyleMatch[1] : '';
// 移除 <body> 标签
const cleanedHtml = html.replace(/<\\/?body[^>]*>/gi, '');
// 渲染内容
shadowRoot.innerHTML = \`
<style>
:host {
all: initial;
width: 100%;
height: 100%;
font-family: Inter, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #13181D;
word-break: break-word;
overflow: auto; /* 添加滚动 */
}
h1, h2, h3, h4 {
font-size: 18px;
font-weight: 700;
}
p {
margin: 0;
}
a {
text-decoration: none;
color: #0E70DF;
}
.shadow-content {
background: #FFFFFF;
width: fit-content;
height: fit-content;
min-width: 100%;
\${bodyStyle ? bodyStyle : ''} /* 注入 body 的 style */
}
img:not(table img) {
max-width: 100% !important;
height: auto !important;
}
</style>
<div class="shadow-content">
\${cleanedHtml}
</div>
\`;
// 自动缩放
autoScale(shadowRoot, container);
}
function autoScale(shadowRoot, container) {
if (!shadowRoot || !container) return;
const parent = container;
const shadowContent = shadowRoot.querySelector('.shadow-content');
if (!shadowContent) return;
const parentWidth = parent.offsetWidth;
const parentHeight = parent.offsetHeight;
const childWidth = shadowContent.scrollWidth;
const childHeight = shadowContent.scrollHeight;
if (childWidth === 0 || childHeight === 0) return;
const scaleX = parentWidth / childWidth;
const scaleY = parentHeight / childHeight;
const scale = Math.min(scaleX, scaleY);
const hostElement = shadowRoot.host;
hostElement.style.zoom = scale;
}
// 使用示例
const exampleHtml = \`${html}\`;
// 渲染HTML
renderHTML(exampleHtml);
</script>
</body>
</html>`
}
+7
View File
@@ -0,0 +1,7 @@
export default function emailMsgTemplate(email) {
return `<b>${email.subject}</b>
发件人:${email.name} &lt;${email.sendEmail}&gt;
收件人:\u200B${email.toEmail}`
}
+35
View File
@@ -0,0 +1,35 @@
export default function emailTextTemplate(text) {
return `<!DOCTYPE html>
<html lang='en' >
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<style>
html {
margin: 0;
padding: 0;
background: #FFF;
}
body {
box-sizing: border-box;
margin: 0;
padding: 10px 10px;
width: 100%;
height: 100%;
overflow: auto; /* 改为 auto 允许滚动 */
}
span {
font-family: inherit;
white-space: pre-wrap;
word-break: break-word;
}
</style>
</head>
<body>
<span>${text}</span>
</body>
</html>`
}