新增tg和第三方邮件转发

This commit is contained in:
eoao
2025-06-28 13:35:37 +08:00
parent 010e0451ab
commit 136c0496b8
25 changed files with 948 additions and 457 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BVIJB-AL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DKdj6vty.css">
<script type="module" crossorigin src="/assets/index-Cgh0xJS2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cobuvpco.css">
</head>
<body>
<div id="loading-first">

View File

@@ -12,6 +12,11 @@ app.get('/setting/query', async (c) => {
return c.json(result.ok(setting));
});
app.get('/setting/websiteConfig', async (c) => {
const setting = await settingService.websiteConfig(c);
return c.json(result.ok(setting));
})
app.put('/setting/setBackground', async (c) => {
const key = await settingService.setBackground(c, await c.req.json());
return c.json(result.ok(key));

View File

@@ -78,6 +78,18 @@ export const settingConst = {
addEmailVerify: {
OPEN: 0,
CLOSE: 1,
},
forwardStatus: {
OPEN: 0,
CLOSE: 1,
},
tgBotStatus: {
OPEN: 0,
CLOSE: 1,
},
ruleType: {
ALL: 0,
RULE: 1
}
}

View File

@@ -5,13 +5,30 @@ import settingService from '../service/setting-service';
import attService from '../service/att-service';
import constant from '../const/constant';
import fileUtils from '../utils/file-utils';
import {attConst, emailConst, isDel} from '../const/entity-const';
import { attConst, emailConst, isDel, 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'
dayjs.extend(utc)
dayjs.extend(timezone)
export async function email(message, env, ctx) {
try {
if (!await settingService.isReceive({ env })) {
const {
receive,
tgBotToken,
tgChatId,
tgBotStatus,
forwardStatus,
forwardEmail,
ruleEmail,
ruleType
} = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) {
return;
}
@@ -28,14 +45,18 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content);
const toName = email.to.find(item => item.address === message.to)?.name || '';
const params = {
toEmail: message.to,
toName: toName,
sendEmail: email.from.address,
name: email.from.name,
subject: email.subject,
content: email.html,
text: email.text,
cc: email.cc ? JSON.stringify(email.cc) : '[]',
bcc:email.bcc ? JSON.stringify(email.bcc) : '[]',
bcc: email.bcc ? JSON.stringify(email.bcc) : '[]',
recipient: JSON.stringify(email.to),
inReplyTo: email.inReplyTo,
relation: email.references,
@@ -49,32 +70,96 @@ export async function email(message, env, ctx) {
const attachments = [];
const cidAttachments = [];
for(let item of email.attachments) {
for (let item of email.attachments) {
let attachment = { ...item };
attachment.key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(attachment.content) + fileUtils.getExtFileName(item.filename);
attachment.size = item.content.length ?? item.content.byteLength;
attachments.push(attachment);
if (attachment.contentId) {
cidAttachments.push(attachment)
cidAttachments.push(attachment);
}
}
const emailRow = await emailService.receive({ env }, params, cidAttachments);
let emailRow = await emailService.receive({ env }, params, cidAttachments);
attachments.forEach(attachment => {
attachment.emailId = emailRow.emailId;
attachment.userId = emailRow.userId;
attachment.accountId = emailRow.accountId;
attachment.type = attachment.contentId ? attConst.type.EMBED : attConst.type.ATT
})
attachment.type = attachment.contentId ? attConst.type.EMBED : attConst.type.ATT;
});
if (attachments.length > 0) {
await attService.addAtt({ env }, attachments);
}
await emailService.completeReceive({ env },account ? emailConst.status.RECEIVE : emailConst.status.NOONE, emailRow.emailId);
emailRow = await emailService.completeReceive({ env }, account ? emailConst.status.RECEIVE : emailConst.status.NOONE, emailRow.emailId);
if (ruleType === settingConst.ruleType.RULE) {
const emails = ruleEmail.split(',');
if (!emails.includes(message.to)) {
return;
}
}
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')}
${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);
}
}));
}
if (forwardStatus === settingConst.forwardStatus.OPEN && forwardEmail) {
const emails = forwardEmail.split(',');
await Promise.all(emails.map(async email => {
try {
await message.forward(email);
} catch (e) {
console.error(`转发邮箱 ${email} 失败:`, e);
}
}));
}
} catch (e) {
console.error('邮件接收异常: ', e);
}
}

View File

@@ -12,6 +12,8 @@ export const email = sqliteTable('email', {
cc: text('cc').default('[]'),
bcc: text('bcc').default('[]'),
recipient: text('recipient'),
toEmail: text('to_email').default('').notNull(),
toName: text('to_name').default('').notNull(),
inReplyTo: text('in_reply_to').default(''),
relation: text('relation').default(''),
messageId: text('message_id').default(''),

View File

@@ -13,7 +13,14 @@ export const setting = sqliteTable('setting', {
secretKey: text('secret_key'),
siteKey: text('site_key'),
background: text('background'),
loginOpacity: integer('login_opacity').default(0.9),
tgBotToken: text('tg_bot_token').default('').notNull(),
tgChatId: text('tg_chat_id').default('').notNull(),
tgBotStatus: integer('tg_bot_status').default(1).notNull(),
forwardEmail: text('forward_email').default('').notNull(),
forwardStatus: integer('forward_status').default(1).notNull(),
ruleEmail: text('rule_email').default('').notNull(),
ruleType: integer('rule_type').default(0).notNull(),
loginOpacity: integer('login_opacity').default(0.88),
resendTokens: text('resend_tokens').default("{}").notNull(),
});
export default setting

View File

@@ -12,10 +12,47 @@ const init = {
await this.intDB(c);
await this.v1_1DB(c);
await this.v1_2DB(c);
await this.v1_3DB(c);
await settingService.refresh(c);
return c.text('初始化成功');
},
async v1_3DB(c) {
const ADD_COLUMN_SQL_LIST = [
`ALTER TABLE setting ADD COLUMN tg_bot_token TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN tg_chat_id TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN tg_bot_status INTEGER NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN forward_email TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN forward_status INTEGER TIME NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN rule_email TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN rule_type INTEGER NOT NULL DEFAULT 0;`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
const nameColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('email') WHERE name = 'to_email' limit 1`).first();
if (nameColumn) {
return
}
const queryList = []
queryList.push(c.env.db.prepare(`ALTER TABLE email ADD COLUMN to_email TEXT NOT NULL DEFAULT ''`));
queryList.push(c.env.db.prepare(`ALTER TABLE email ADD COLUMN to_name TEXT NOT NULL DEFAULT ''`));
queryList.push(c.env.db.prepare(`UPDATE email SET to_email = json_extract(recipient, '$[0].address'), to_name = json_extract(recipient, '$[0].name')`));
await c.env.db.batch(queryList);
},
async v1_2DB(c){
const ADD_COLUMN_SQL_LIST = [

View File

@@ -10,8 +10,8 @@ import app from '../hono/hono';
const exclude = [
'/login',
'/register',
'/setting/query',
'/file',
'/setting/websiteConfig',
'/webhooks',
'/init'
];

View File

@@ -17,7 +17,6 @@ import account from '../entity/account';
import starService from './star-service';
import dayjs from 'dayjs';
import kvConst from '../const/kv-const';
import constant from '../const/constant';
const emailService = {
@@ -514,7 +513,7 @@ const emailService = {
}
if (accountEmail) {
conditions.push(like(account.email, `${accountEmail}%`));
conditions.push(like(email.toEmail, `${accountEmail}%`));
}
if (name) {
@@ -535,16 +534,14 @@ const emailService = {
conditions.push(lt(email.emailId, emailId));
}
const query = orm(c).select({ ...email, userEmail: user.email, accountEmail: account.email })
const query = orm(c).select({ ...email, userEmail: user.email })
.from(email)
.leftJoin(user, eq(email.userId, user.userId))
.leftJoin(account, eq(email.accountId, account.accountId))
.where(and(...conditions));
const queryCount = orm(c).select({ total: count() })
.from(email)
.leftJoin(user, eq(email.userId, user.userId))
.leftJoin(account, eq(email.accountId, account.accountId))
.where(and(...countConditions));
if (timeSort) {
@@ -574,10 +571,10 @@ const emailService = {
},
async completeReceive(c, status, emailId) {
await orm(c).update(email).set({
return await orm(c).update(email).set({
isDel: isDel.NORMAL,
status: status
}).where(eq(email.emailId, emailId)).run();
}).where(eq(email.emailId, emailId)).returning().get();
}
};

View File

@@ -7,7 +7,6 @@ import r2Service from './r2-service';
import emailService from './email-service';
import accountService from './account-service';
import userService from './user-service';
import starService from './star-service';
import constant from '../const/constant';
import BizError from '../error/biz-error';
@@ -32,20 +31,20 @@ const settingService = {
async get(c) {
const settingRow = await this.query(c);
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******`: null ;
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null;
Object.keys(settingRow.resendTokens).forEach(key => {
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;
});
return settingRow
return settingRow;
},
async set(c, params) {
const settingData = await this.query(c)
let resendTokens = {...settingData.resendTokens,...params.resendTokens}
const settingData = await this.query(c);
let resendTokens = { ...settingData.resendTokens, ...params.resendTokens };
Object.keys(resendTokens).forEach(domain => {
if(!resendTokens[domain]) delete resendTokens[domain]
})
params.resendTokens = JSON.stringify(resendTokens)
if (!resendTokens[domain]) delete resendTokens[domain];
});
params.resendTokens = JSON.stringify(resendTokens);
await orm(c).update(setting).set({ ...params }).returning().get();
await this.refresh(c);
},
@@ -109,6 +108,25 @@ const settingService = {
await emailService.physicsDeleteAll(c);
await accountService.physicsDeleteAll(c);
await userService.physicsDeleteAll(c);
},
async websiteConfig(c) {
const settingRow = await this.get(c);
return {
register: settingRow.register,
title: settingRow.title,
manyEmail: settingRow.manyEmail,
addEmail: settingRow.addEmail,
autoRefreshTime: settingRow.autoRefreshTime,
addEmailVerify: settingRow.addEmailVerify,
registerVerify: settingRow.registerVerify,
send: settingRow.send,
r2Domain: settingRow.r2Domain,
siteKey: settingRow.siteKey,
background: settingRow.background,
loginOpacity: settingRow.loginOpacity,
domainList:settingRow.domainList
};
}
};

View File

@@ -1,15 +1,24 @@
import { parseHTML } from 'linkedom';
const emailUtils = {
getDomain(email) {
if (typeof email !== 'string') return ''
const parts = email.split('@')
return parts.length === 2 ? parts[1] : ''
if (typeof email !== 'string') return '';
const parts = email.split('@');
return parts.length === 2 ? parts[1] : '';
},
getName(email) {
if (typeof email !== 'string') return ''
const parts = email.trim().split('@')
return parts.length === 2 ? parts[0] : ''
}
}
if (typeof email !== 'string') return '';
const parts = email.trim().split('@');
return parts.length === 2 ? parts[0] : '';
},
export default emailUtils
htmlToText(content) {
const { document } = parseHTML(content);
document.querySelectorAll('style, script, title').forEach(el => el.remove());
return document.documentElement.innerText;
}
};
export default emailUtils;