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 = `${params.subject} 发件人:${params.name} <${params.sendEmail}> 收件人:\u200B${message.to} 时间:${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; }