新增tg和第三方邮件转发
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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} <${params.sendEmail}>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(''),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -10,8 +10,8 @@ import app from '../hono/hono';
|
||||
const exclude = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/setting/query',
|
||||
'/file',
|
||||
'/setting/websiteConfig',
|
||||
'/webhooks',
|
||||
'/init'
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user