mirror of
https://github.com/schroinerxy/cloud-mail.git
synced 2026-06-21 19:35:50 +08:00
新增TG邮件查看HTML
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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} <${params.sendEmail}>
|
||||
<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(',');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function emailMsgTemplate(email) {
|
||||
return `<b>${email.subject}</b>
|
||||
|
||||
发件人:${email.name} <${email.sendEmail}>
|
||||
收件人:\u200B${email.toEmail}`
|
||||
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
Reference in New Issue
Block a user