Files
cloud-mail/mail-worker/src/email/email.js
T
2025-09-26 17:55:38 +08:00

249 lines
6.2 KiB
JavaScript

import PostalMime from 'postal-mime';
import emailService from '../service/email-service';
import accountService from '../service/account-service';
import settingService from '../service/setting-service';
import attService from '../service/att-service';
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';
dayjs.extend(utc);
dayjs.extend(timezone);
export async function email(message, env, ctx) {
try {
const {
receive,
tgBotToken,
tgChatId,
tgBotStatus,
forwardStatus,
forwardEmail,
ruleEmail,
ruleType,
r2Domain,
noRecipient
} = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) {
message.setReject('Service suspended');
return;
}
const reader = message.raw.getReader();
let content = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
content += new TextDecoder().decode(value);
}
const email = await PostalMime.parse(content);
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
message.setReject('Recipient not found');
return;
}
if (account && account.email !== env.admin) {
let { banEmail, banEmailType, availDomain } = await roleService.selectByUserId({ env: env }, account.userId);
if (!roleService.hasAvailDomainPerm(availDomain, message.to)) {
message.setReject('Mailbox disabled');
return;
}
banEmail = banEmail.split(',').filter(item => item !== '');
if (banEmail.includes('*')) {
if (!banEmailHandler(banEmailType, message, email)) return;
}
for (const item of banEmail) {
if (verifyUtils.isDomain(item)) {
const banDomain = item.toLowerCase();
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase());
if (banDomain === receiveDomain) {
if (!banEmailHandler(banEmailType, message, email)) return;
}
} else {
if (item.toLowerCase() === email.from.address.toLowerCase()) {
if (!banEmailHandler(banEmailType, message, email)) return;
}
}
}
}
if (!email.to) {
email.to = [{ address: message.to, name: emailUtils.getName(message.to)}]
}
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 || emailUtils.getName(email.from.address),
subject: email.subject,
content: email.html,
text: email.text,
cc: email.cc ? JSON.stringify(email.cc) : '[]',
bcc: email.bcc ? JSON.stringify(email.bcc) : '[]',
recipient: JSON.stringify(email.to),
inReplyTo: email.inReplyTo,
relation: email.references,
messageId: email.messageId,
userId: account ? account.userId : 0,
accountId: account ? account.accountId : 0,
isDel: isDel.DELETE,
status: emailConst.status.SAVING
};
const attachments = [];
const cidAttachments = [];
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);
}
}
let emailRow = await emailService.receive({ env }, params, cidAttachments, r2Domain);
attachments.forEach(attachment => {
attachment.emailId = emailRow.emailId;
attachment.userId = emailRow.userId;
attachment.accountId = emailRow.accountId;
});
try {
if (attachments.length > 0 && await r2Service.hasOSS({ env })) {
await attService.addAtt({ env }, attachments);
}
} catch (e) {
console.error(e);
}
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')}
${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);
}
}));
}
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);
}
}
function banEmailHandler(banEmailType, message, email) {
if (banEmailType === roleConst.banEmailType.ALL) {
message.setReject('Mailbox disabled');
return false;
}
if (banEmailType === roleConst.banEmailType.CONTENT) {
email.html = 'The content has been deleted';
email.text = 'The content has been deleted';
email.attachments = [];
}
return true;
}