新增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

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;