1142 lines
30 KiB
JavaScript
1142 lines
30 KiB
JavaScript
import orm from '../entity/orm';
|
||
import email from '../entity/email';
|
||
import { emailListColumns, emailBriefColumns, EMAIL_LIST_TEXT_LEN } from '../lib/email-list-columns';
|
||
import { attConst, emailConst, isDel, settingConst } from '../const/entity-const';
|
||
import { and, desc, eq, gt, inArray, notInArray, lt, count, asc, sql, ne, or, like, lte, gte } from 'drizzle-orm';
|
||
import { star } from '../entity/star';
|
||
import settingService from './setting-service';
|
||
import accountService from './account-service';
|
||
import BizError from '../error/biz-error';
|
||
import emailUtils from '../utils/email-utils';
|
||
import fileUtils from '../utils/file-utils';
|
||
import { Resend } from 'resend';
|
||
import attService from './att-service';
|
||
import { parseHTML } from 'linkedom';
|
||
import userService from './user-service';
|
||
import roleService from './role-service';
|
||
import user from '../entity/user';
|
||
import starService from './star-service';
|
||
import dayjs from 'dayjs';
|
||
import kvConst from '../const/kv-const';
|
||
import { t } from '../i18n/i18n'
|
||
import domainUtils from '../utils/domain-uitls';
|
||
import account from "../entity/account";
|
||
import { att } from '../entity/att';
|
||
import telegramService from './telegram-service';
|
||
|
||
const emailService = {
|
||
|
||
async list(c, params, userId) {
|
||
|
||
let { emailId, type, accountId, size, timeSort, allReceive, full } = params;
|
||
|
||
size = Number(size);
|
||
type = Number(type);
|
||
emailId = Number(emailId) || 0;
|
||
timeSort = Number(timeSort);
|
||
accountId = Number(accountId);
|
||
allReceive = Number(allReceive);
|
||
full = Number(full);
|
||
|
||
if (isNaN(type)) {
|
||
type = 0;
|
||
}
|
||
|
||
if (isNaN(accountId)) {
|
||
throw new BizError(t('emptyAccountId'));
|
||
}
|
||
|
||
if (isNaN(size)) {
|
||
size = 10;
|
||
}
|
||
|
||
if (isNaN(full)) {
|
||
full = 1;
|
||
}
|
||
|
||
full = full === 1;
|
||
|
||
if (size > 50) {
|
||
size = 50;
|
||
}
|
||
|
||
if (isNaN(allReceive)) {
|
||
let accountRow = await accountService.selectById(c, accountId);
|
||
allReceive = accountRow.allReceive;
|
||
}
|
||
|
||
const filters = this.emailListFilters({ userId, accountId, type, allReceive, emailId, timeSort });
|
||
const countFilters = this.emailListFilters({ userId, accountId, type, allReceive, withCursor: false });
|
||
const columns = full ? emailListColumns : emailBriefColumns;
|
||
|
||
const query = orm(c)
|
||
.select({
|
||
...columns,
|
||
starId: star.starId
|
||
})
|
||
.from(email)
|
||
.leftJoin(
|
||
star,
|
||
and(
|
||
eq(star.emailId, email.emailId),
|
||
eq(star.userId, userId)
|
||
)
|
||
)
|
||
.innerJoin(
|
||
account,
|
||
eq(account.accountId, email.accountId)
|
||
)
|
||
.where(and(...filters));
|
||
|
||
if (timeSort) {
|
||
query.orderBy(asc(email.emailId));
|
||
} else {
|
||
query.orderBy(desc(email.emailId));
|
||
}
|
||
|
||
const listQuery = query.limit(size).all();
|
||
|
||
const totalQuery = orm(c).select({ total: count() }).from(email)
|
||
.innerJoin(
|
||
account,
|
||
eq(account.accountId, email.accountId)
|
||
)
|
||
.where(and(...countFilters))
|
||
.get();
|
||
|
||
const latestEmailQuery = orm(c).select({
|
||
emailId: email.emailId,
|
||
accountId: email.accountId,
|
||
userId: email.userId,
|
||
}).from(email).where(
|
||
and(
|
||
eq(email.userId, userId),
|
||
eq(email.type, type),
|
||
eq(email.isDel, isDel.NORMAL),
|
||
allReceive ? undefined : eq(email.accountId, accountId)
|
||
))
|
||
.orderBy(desc(email.emailId)).limit(1).get();
|
||
|
||
let [list, totalRow, latestEmail] = await Promise.all([listQuery, totalQuery, latestEmailQuery]);
|
||
|
||
list = list.map(item => ({
|
||
...item,
|
||
isStar: item.starId != null ? 1 : 0
|
||
}));
|
||
|
||
if (full) {
|
||
await this.emailAddAtt(c, list);
|
||
} else {
|
||
this.applyListText(list);
|
||
}
|
||
|
||
if (!latestEmail) {
|
||
latestEmail = {
|
||
emailId: 0,
|
||
accountId: accountId,
|
||
userId: userId,
|
||
}
|
||
}
|
||
|
||
return { list, total: totalRow.total, latestEmail };
|
||
},
|
||
|
||
toListText(item) {
|
||
const raw = emailUtils.formatText(item.text) || emailUtils.htmlToText(item.content);
|
||
return raw.replace(/\s+/g, ' ').trim().slice(0, EMAIL_LIST_TEXT_LEN);
|
||
},
|
||
|
||
applyListText(list) {
|
||
for (const item of list) {
|
||
item.text = this.toListText(item);
|
||
delete item.content;
|
||
}
|
||
return list;
|
||
},
|
||
|
||
emailListFilters({ userId, accountId, type, allReceive, emailId, timeSort, withCursor = true }) {
|
||
const conditions = [
|
||
eq(email.userId, userId),
|
||
eq(email.type, type),
|
||
eq(email.isDel, isDel.NORMAL),
|
||
eq(account.isDel, isDel.NORMAL),
|
||
];
|
||
if (!allReceive) {
|
||
conditions.push(eq(email.accountId, accountId));
|
||
}
|
||
if (withCursor && emailId) {
|
||
conditions.push(timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId));
|
||
}
|
||
return conditions;
|
||
},
|
||
|
||
allEmailListFilters({ emailId, name, subject, accountEmail, userEmail, type, timeSort, withCursor = true }) {
|
||
const conditions = [];
|
||
|
||
if (type === 'send') {
|
||
conditions.push(eq(email.type, emailConst.type.SEND));
|
||
}
|
||
|
||
if (type === 'receive') {
|
||
conditions.push(eq(email.type, emailConst.type.RECEIVE));
|
||
}
|
||
|
||
if (type === 'delete') {
|
||
conditions.push(eq(email.isDel, isDel.DELETE));
|
||
}
|
||
|
||
if (type === 'noone') {
|
||
conditions.push(eq(email.status, emailConst.status.NOONE));
|
||
}
|
||
|
||
if (userEmail) {
|
||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${userEmail + '%'}`);
|
||
}
|
||
|
||
if (accountEmail) {
|
||
conditions.push(
|
||
or(
|
||
sql`${email.toEmail} COLLATE NOCASE LIKE ${accountEmail + '%'}`,
|
||
sql`${email.sendEmail} COLLATE NOCASE LIKE ${accountEmail + '%'}`,
|
||
)
|
||
);
|
||
}
|
||
|
||
if (name) {
|
||
conditions.push(sql`${email.name} COLLATE NOCASE LIKE ${name + '%'}`);
|
||
}
|
||
|
||
if (subject) {
|
||
conditions.push(sql`${email.subject} COLLATE NOCASE LIKE ${subject + '%'}`);
|
||
}
|
||
|
||
if (withCursor && emailId) {
|
||
conditions.push(timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId));
|
||
}
|
||
|
||
return conditions;
|
||
},
|
||
|
||
async delete(c, params, userId) {
|
||
const { emailIds } = params;
|
||
const emailIdList = emailIds.split(',').map(Number);
|
||
const { syncDelete } = await settingService.query(c);
|
||
|
||
if (syncDelete === settingConst.syncDelete.OPEN) {
|
||
const owned = await orm(c).select({ emailId: email.emailId }).from(email)
|
||
.where(and(eq(email.userId, userId), inArray(email.emailId, emailIdList)))
|
||
.all();
|
||
const ownedIds = owned.map(row => row.emailId);
|
||
if (ownedIds.length) {
|
||
await this.physicsDelete(c, { emailIds: ownedIds.join(',') });
|
||
}
|
||
return;
|
||
}
|
||
|
||
await orm(c).update(email).set({ isDel: isDel.DELETE }).where(
|
||
and(
|
||
eq(email.userId, userId),
|
||
inArray(email.emailId, emailIdList)))
|
||
.run();
|
||
},
|
||
|
||
receive(c, params, cidAttList, r2domain) {
|
||
params.content = this.imgReplace(params.content, cidAttList, r2domain)
|
||
return orm(c).insert(email).values({ ...params }).returning().get();
|
||
},
|
||
|
||
//邮件发送
|
||
async send(c, params, userId) {
|
||
|
||
let {
|
||
accountId, //发送账号id
|
||
name, //发件人名字
|
||
sendType, //发件类型
|
||
emailId, //邮件id,如果是回复邮件会带
|
||
receiveEmail, //收件人邮箱
|
||
text, //邮件纯文本
|
||
content, //邮件内容
|
||
subject, //邮件标题
|
||
attachments = [] //附件
|
||
} = params;
|
||
|
||
const { resendTokens, r2Domain, send, domainList } = await settingService.query(c);
|
||
|
||
let { imageDataList, html } = await attService.toImageUrlHtml(c, content);
|
||
|
||
//判断是否关闭发件功能
|
||
if (send === settingConst.send.CLOSE) {
|
||
throw new BizError(t('disabledSend'), 403);
|
||
}
|
||
|
||
const userRow = await userService.selectById(c, userId);
|
||
const roleRow = await roleService.selectById(c, userRow.type);
|
||
|
||
//判断接收方是不是全部为站内邮箱
|
||
const allInternal = receiveEmail.every(email => {
|
||
const domain = '@' + emailUtils.getDomain(email);
|
||
return domainList.includes(domain);
|
||
});
|
||
|
||
if (c.env.admin !== userRow.email) {
|
||
|
||
//发件被禁用
|
||
if (roleRow.sendType === 'ban') {
|
||
throw new BizError(t('bannedSend'), 403);
|
||
}
|
||
|
||
//发件被禁用
|
||
if (roleRow.sendType === 'internal' && !allInternal) {
|
||
throw new BizError(t('onlyInternalSend'), 403);
|
||
}
|
||
|
||
}
|
||
|
||
//如果不是管理员,权限设置了发送次数
|
||
if (c.env.admin !== userRow.email && roleRow.sendCount) {
|
||
|
||
if (userRow.sendCount >= roleRow.sendCount) {
|
||
if (roleRow.sendType === 'day') throw new BizError(t('daySendLimit'), 403);
|
||
if (roleRow.sendType === 'count') throw new BizError(t('totalSendLimit'), 403);
|
||
}
|
||
|
||
if (userRow.sendCount + receiveEmail.length > roleRow.sendCount) {
|
||
if (roleRow.sendType === 'day') throw new BizError(t('daySendLack'), 403);
|
||
if (roleRow.sendType === 'count') throw new BizError(t('totalSendLack'), 403);
|
||
}
|
||
|
||
}
|
||
|
||
const accountRow = await accountService.selectById(c, accountId);
|
||
|
||
if (!accountRow) {
|
||
throw new BizError(t('senderAccountNotExist'));
|
||
}
|
||
|
||
if (accountRow.userId !== userId) {
|
||
throw new BizError(t('sendEmailNotCurUser'));
|
||
}
|
||
|
||
if (c.env.admin !== userRow.email) {
|
||
//用户没有这个域名的使用权限
|
||
if(!roleService.hasAvailDomainPerm(roleRow.availDomain, accountRow.email)) {
|
||
throw new BizError(t('noDomainPermSend'),403)
|
||
}
|
||
|
||
}
|
||
|
||
const domain = emailUtils.getDomain(accountRow.email);
|
||
const resendToken = resendTokens[domain];
|
||
const useCloudflareEmail = !!c.env.email;
|
||
|
||
//如果接收方存在站外邮箱,又没有发信服务
|
||
if (!useCloudflareEmail && !resendToken && !allInternal) {
|
||
throw new BizError(t('noSendProvider'));
|
||
}
|
||
|
||
//没有发件人名字自动截取
|
||
if (!name) {
|
||
name = emailUtils.getName(accountRow.email);
|
||
}
|
||
|
||
let emailRow = {
|
||
messageId: null
|
||
};
|
||
|
||
//如果是回复邮件
|
||
if (sendType === 'reply') {
|
||
|
||
emailRow = await this.selectById(c, emailId);
|
||
|
||
if (!emailRow) {
|
||
throw new BizError(t('notExistEmailReply'));
|
||
}
|
||
|
||
}
|
||
|
||
let sendResult = {};
|
||
|
||
//存在站外邮箱时,如果配置了 Cloudflare Email Service 就优先使用,否则使用 Resend
|
||
if (!allInternal) {
|
||
|
||
if (useCloudflareEmail) {
|
||
sendResult = await this.sendByCloudflareEmail(c, {
|
||
name,
|
||
accountEmail: accountRow.email,
|
||
receiveEmail,
|
||
subject,
|
||
text,
|
||
html,
|
||
attachments: [...imageDataList, ...attachments],
|
||
sendType,
|
||
messageId: emailRow.messageId
|
||
});
|
||
} else {
|
||
sendResult = await this.sendByResend(resendToken, {
|
||
name,
|
||
accountEmail: accountRow.email,
|
||
receiveEmail,
|
||
subject,
|
||
text,
|
||
html,
|
||
attachments: [...imageDataList, ...attachments],
|
||
sendType,
|
||
messageId: emailRow.messageId
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
const { data, error } = sendResult;
|
||
|
||
|
||
if (error) {
|
||
throw new BizError(error.message);
|
||
}
|
||
|
||
imageDataList = imageDataList.map(item => ({...item, contentId: `<${item.contentId}>`}))
|
||
|
||
//把图片标签cid标签切换会通用url
|
||
html = this.imgReplace(html, imageDataList, r2Domain);
|
||
|
||
//封装数据保存到数据库
|
||
const emailData = {};
|
||
emailData.sendEmail = accountRow.email;
|
||
emailData.name = name;
|
||
emailData.subject = subject;
|
||
emailData.content = html;
|
||
emailData.text = text;
|
||
emailData.accountId = accountId;
|
||
emailData.status = useCloudflareEmail ? emailConst.status.DELIVERED : emailConst.status.SENT;
|
||
emailData.type = emailConst.type.SEND;
|
||
emailData.userId = userId;
|
||
emailData.resendEmailId = data?.id;
|
||
|
||
const recipient = [];
|
||
|
||
receiveEmail.forEach(item => {
|
||
recipient.push({ address: item, name: '' });
|
||
});
|
||
|
||
emailData.recipient = JSON.stringify(recipient);
|
||
|
||
if (sendType === 'reply') {
|
||
emailData.inReplyTo = emailRow.messageId;
|
||
emailData.relation = emailRow.messageId;
|
||
}
|
||
|
||
//如果权限有发送次数增加用户发送次数
|
||
if (roleRow.sendCount && roleRow.sendType !== 'internal') {
|
||
await userService.incrUserSendCount(c, receiveEmail.length, userId);
|
||
}
|
||
|
||
//保存到数据库并返回结果
|
||
const emailResult = await orm(c).insert(email).values(emailData).returning().get();
|
||
|
||
//保存内嵌附件
|
||
if (imageDataList.length > 0) {
|
||
if (imageDataList.length > 10) {
|
||
throw new BizError(t('imageAttLimit'));
|
||
}
|
||
await attService.saveArticleAtt(c, imageDataList, userId, accountId, emailResult.emailId);
|
||
}
|
||
|
||
//保存普通附件
|
||
if (attachments?.length > 0) {
|
||
if (attachments.length > 10) {
|
||
throw new BizError(t('attLimit'));
|
||
}
|
||
await attService.saveSendAtt(c, attachments, userId, accountId, emailResult.emailId);
|
||
}
|
||
|
||
const attList = await attService.selectByEmailIds(c, [emailResult.emailId]);
|
||
emailResult.attList = attList;
|
||
|
||
//如果全是站内接收方,直接写入数据库
|
||
if (allInternal) {
|
||
await this.HandleOnSiteEmail(c, receiveEmail, emailResult, attList);
|
||
}
|
||
|
||
const dateStr = dayjs().format('YYYY-MM-DD');
|
||
let daySendTotal = await c.env.kv.get(kvConst.SEND_DAY_COUNT + dateStr);
|
||
|
||
//记录每天发件次数统计
|
||
if (!daySendTotal) {
|
||
await c.env.kv.put(kvConst.SEND_DAY_COUNT + dateStr, JSON.stringify(receiveEmail.length), { expirationTtl: 60 * 60 * 24 });
|
||
} else {
|
||
daySendTotal = Number(daySendTotal) + receiveEmail.length
|
||
await c.env.kv.put(kvConst.SEND_DAY_COUNT + dateStr, JSON.stringify(daySendTotal), { expirationTtl: 60 * 60 * 24 });
|
||
}
|
||
|
||
return [ emailResult ];
|
||
},
|
||
|
||
async sendByCloudflareEmail(c, params) {
|
||
const sendForm = {
|
||
from: { email: params.accountEmail, name: params.name },
|
||
to: [...params.receiveEmail],
|
||
subject: params.subject
|
||
};
|
||
|
||
if (params.text) {
|
||
sendForm.text = params.text;
|
||
}
|
||
|
||
if (params.html) {
|
||
sendForm.html = params.html;
|
||
}
|
||
|
||
const attachments = await this.toCloudflareAttachments(params.attachments);
|
||
if (attachments.length > 0) {
|
||
sendForm.attachments = attachments;
|
||
}
|
||
|
||
if (params.sendType === 'reply' && params.messageId) {
|
||
sendForm.headers = {
|
||
'in-reply-to': params.messageId,
|
||
'references': params.messageId
|
||
};
|
||
}
|
||
|
||
const result = await c.env.email.send(sendForm);
|
||
|
||
return {
|
||
data: {
|
||
id: result.messageId
|
||
}
|
||
};
|
||
},
|
||
|
||
async sendByResend(resendToken, params) {
|
||
const resend = new Resend(resendToken);
|
||
|
||
const sendForm = {
|
||
from: `${params.name} <${params.accountEmail}>`,
|
||
to: [...params.receiveEmail],
|
||
subject: params.subject,
|
||
text: params.text,
|
||
html: params.html,
|
||
attachments: await this.toResendAttachments(params.attachments)
|
||
};
|
||
|
||
if (params.sendType === 'reply') {
|
||
sendForm.headers = {
|
||
'in-reply-to': params.messageId,
|
||
'references': params.messageId
|
||
};
|
||
}
|
||
|
||
return await resend.emails.send(sendForm);
|
||
},
|
||
|
||
async toCloudflareAttachments(attachments) {
|
||
const arrayBufferAttachments = await this.toArrayBufferAttachments(attachments);
|
||
|
||
return arrayBufferAttachments.map(attachment => {
|
||
const item = {
|
||
content: attachment.content,
|
||
filename: attachment.filename,
|
||
type: attachment.mimeType || attachment.contentType || attachment.type || 'application/octet-stream',
|
||
disposition: attachment.contentId ? 'inline' : 'attachment'
|
||
};
|
||
|
||
if (attachment.contentId) {
|
||
item.contentId = attachment.contentId.replace(/^<|>$/g, '');
|
||
}
|
||
|
||
return item;
|
||
});
|
||
},
|
||
|
||
async toResendAttachments(attachments = []) {
|
||
const result = [];
|
||
|
||
for (const attachment of attachments) {
|
||
const content = await this.toAttachmentBase64(attachment);
|
||
if (!content) {
|
||
continue;
|
||
}
|
||
|
||
result.push({
|
||
...attachment,
|
||
content,
|
||
contentType: attachment.contentType || attachment.mimeType || attachment.type || 'application/octet-stream'
|
||
});
|
||
}
|
||
|
||
return result;
|
||
},
|
||
|
||
async toArrayBufferAttachments(attachments = []) {
|
||
const result = [];
|
||
|
||
for (const attachment of attachments) {
|
||
const content = await this.toAttachmentArrayBuffer(attachment);
|
||
if (!content) {
|
||
continue;
|
||
}
|
||
|
||
result.push({ ...attachment, content });
|
||
}
|
||
|
||
return result;
|
||
},
|
||
|
||
async toAttachmentBase64(attachment) {
|
||
let content = attachment.content;
|
||
|
||
if (!content) {
|
||
return null;
|
||
}
|
||
|
||
if (typeof content === 'string') {
|
||
if (content.startsWith('data:')) {
|
||
content = content.split(',')[1] || content;
|
||
}
|
||
return content.replace(/\s+/g, '');
|
||
}
|
||
|
||
const arrayBuffer = await this.toAttachmentArrayBuffer(attachment);
|
||
if (!arrayBuffer) {
|
||
return null;
|
||
}
|
||
|
||
const bytes = new Uint8Array(arrayBuffer);
|
||
let binary = '';
|
||
|
||
for (let i = 0; i < bytes.length; i += 0x8000) {
|
||
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||
}
|
||
|
||
return btoa(binary);
|
||
},
|
||
|
||
async toAttachmentArrayBuffer(attachment) {
|
||
let content = attachment.content;
|
||
|
||
if (!content) {
|
||
return null;
|
||
}
|
||
|
||
if (content instanceof ArrayBuffer) {
|
||
return content;
|
||
}
|
||
|
||
if (content instanceof Uint8Array) {
|
||
return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
||
}
|
||
|
||
if (typeof content === 'string') {
|
||
if (content.startsWith('data:')) {
|
||
content = content.split(',')[1] || content;
|
||
}
|
||
return fileUtils.base64ToUint8Array(content.replace(/\s+/g, '')).buffer;
|
||
}
|
||
|
||
return content;
|
||
},
|
||
|
||
//处理站内邮件发送
|
||
async HandleOnSiteEmail(c, receiveEmail, sendEmailData, attList) {
|
||
|
||
const { noRecipient } = await settingService.query(c);
|
||
|
||
//查询所有收件人账号信息
|
||
let accountList = await orm(c).select().from(account).where(inArray(account.email, receiveEmail)).all();
|
||
|
||
// 对于含+未精确匹配的收件人,获取基础地址账号
|
||
const plusEmails = receiveEmail.filter(
|
||
e => e.includes('+') && !accountList.some(a => a.email === e)
|
||
);
|
||
const baseAccounts = [];
|
||
if (plusEmails.length > 0) {
|
||
const baseEmails = [...new Set(
|
||
plusEmails.map(e => emailUtils.getBaseEmail(e)).filter(Boolean)
|
||
)];
|
||
const existing = new Set(accountList.map(a => a.email));
|
||
const needed = baseEmails.filter(e => !existing.has(e));
|
||
if (needed.length > 0) {
|
||
const rows = await orm(c).select().from(account)
|
||
.where(inArray(account.email, needed)).all();
|
||
baseAccounts.push(...rows);
|
||
}
|
||
}
|
||
|
||
// 合并精确匹配和基础地址匹配的账号用于权限查询
|
||
const allAccounts = [...accountList, ...baseAccounts];
|
||
|
||
//查询所有收件人权限身份
|
||
const userIds = allAccounts.map(accountRow => accountRow.userId);
|
||
let roleList = await roleService.selectByUserIds(c, userIds);
|
||
|
||
//封装数据库准备保存到数据库
|
||
const emailDataList = [];
|
||
|
||
for (const email of receiveEmail) {
|
||
|
||
//把发件人邮件改成收件
|
||
const emailValues = {...sendEmailData}
|
||
emailValues.status = emailConst.status.RECEIVE;
|
||
emailValues.type = emailConst.type.RECEIVE;
|
||
emailValues.toEmail = email;
|
||
emailValues.toName = emailUtils.getName(email);
|
||
emailValues.emailId = null;
|
||
|
||
let accountRow = allAccounts.find(accountRow => accountRow.email === email);
|
||
|
||
// 精确匹配不到时回退到主地址(去掉 +tag)
|
||
if (!accountRow && email.includes('+')) {
|
||
const baseEmail = emailUtils.getBaseEmail(email);
|
||
accountRow = allAccounts.find(accountRow => accountRow.email === baseEmail);
|
||
}
|
||
|
||
//如果收件人存在就把邮件信息改成收件人的
|
||
if (accountRow) {
|
||
|
||
//设置给收件人保存
|
||
emailValues.userId = accountRow.userId;
|
||
emailValues.accountId = accountRow.accountId;
|
||
emailValues.type = emailConst.type.RECEIVE;
|
||
emailValues.status = emailConst.status.RECEIVE;
|
||
|
||
const roleRow = roleList.find(roleRow => roleRow.userId === accountRow.userId);
|
||
|
||
let { banEmail, availDomain } = roleRow;
|
||
|
||
//如果收件人没有这个域名的使用权限和有邮件拦截,就把邮件改为拒收状态
|
||
if (email !== c.env.admin) {
|
||
|
||
if (!roleService.hasAvailDomainPerm(availDomain, email)) {
|
||
emailValues.status = emailConst.status.BOUNCED;
|
||
emailValues.message = `The recipient <${email}> is not authorized to use this domain.`;
|
||
} else if(roleService.isBanEmail(banEmail, sendEmailData.sendEmail)) {
|
||
emailValues.status = emailConst.status.BOUNCED;
|
||
emailValues.message = `The recipient <${email}> is disabled from receiving emails.`;
|
||
}
|
||
|
||
}
|
||
|
||
emailDataList.push(emailValues);
|
||
|
||
} else {
|
||
|
||
//设置无收件人邮件信息
|
||
emailValues.userId = 0;
|
||
emailValues.accountId = 0;
|
||
emailValues.type = emailConst.type.RECEIVE;
|
||
emailValues.status = emailConst.status.NOONE;
|
||
|
||
//如果无人收件关闭改为拒收
|
||
if (noRecipient === settingConst.noRecipient.CLOSE) {
|
||
emailValues.status = emailConst.status.BOUNCED;
|
||
emailValues.message = `Recipient not found: <${email}>`;
|
||
}
|
||
|
||
emailDataList.push(emailValues);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
//保存邮件
|
||
const receiveEmailList = emailDataList.filter(emailRow => emailRow.status === emailConst.status.RECEIVE || emailRow.status === emailConst.status.NOONE);
|
||
|
||
for (const emailData of receiveEmailList) {
|
||
|
||
const emailRow = await orm(c).insert(email).values(emailData).returning().get();
|
||
|
||
//设置附件保存
|
||
for (const attRow of attList) {
|
||
const attValues = {...attRow};
|
||
attValues.emailId = emailRow.emailId;
|
||
attValues.accountId = emailRow.accountId;
|
||
attValues.userId = emailRow.userId;
|
||
attValues.attId = null;
|
||
await orm(c).insert(att).values(attValues).run();
|
||
}
|
||
|
||
}
|
||
|
||
const bouncedEmail = emailDataList.find(emailRow => emailRow.status === emailConst.status.BOUNCED);
|
||
|
||
|
||
let status = emailConst.status.DELIVERED;
|
||
let message = ''
|
||
//如果有拒收邮件,就把发件人的邮件改成拒收
|
||
if (bouncedEmail) {
|
||
const messageJson = { message: bouncedEmail.message };
|
||
message = JSON.stringify(messageJson);
|
||
status = emailConst.status.BOUNCED;
|
||
}
|
||
|
||
await orm(c).update(email).set({ status, message: message }).where(eq(email.emailId, sendEmailData.emailId)).run();
|
||
|
||
},
|
||
|
||
imgReplace(content, cidAttList, r2domain) {
|
||
|
||
if (!content) {
|
||
return ''
|
||
}
|
||
|
||
const { document } = parseHTML(content);
|
||
|
||
const images = Array.from(document.querySelectorAll('img'));
|
||
|
||
const useAtts = []
|
||
|
||
for (const img of images) {
|
||
|
||
const src = img.getAttribute('src');
|
||
if (src && src.startsWith('cid:') && cidAttList) {
|
||
|
||
const cid = src.replace(/^cid:/, '');
|
||
const attCidIndex = cidAttList.findIndex(cidAtt => cidAtt.contentId.replace(/^<|>$/g, '') === cid);
|
||
|
||
if (attCidIndex > -1) {
|
||
const cidAtt = cidAttList[attCidIndex];
|
||
img.setAttribute('src', '{{domain}}' + cidAtt.key);
|
||
useAtts.push(cidAtt)
|
||
}
|
||
|
||
}
|
||
|
||
r2domain = domainUtils.toOssDomain(r2domain)
|
||
|
||
if (src && src.startsWith(r2domain + '/')) {
|
||
img.setAttribute('src', src.replace(r2domain + '/', '{{domain}}'));
|
||
}
|
||
|
||
}
|
||
|
||
useAtts.forEach(att => {
|
||
att.type = attConst.type.EMBED
|
||
})
|
||
|
||
return document.toString();
|
||
},
|
||
|
||
selectById(c, emailId) {
|
||
return orm(c).select().from(email).where(
|
||
and(eq(email.emailId, emailId),
|
||
eq(email.isDel, isDel.NORMAL)))
|
||
.get();
|
||
},
|
||
|
||
async latest(c, params, userId) {
|
||
let { emailId, accountId, allReceive } = params;
|
||
allReceive = Number(allReceive);
|
||
|
||
if (isNaN(allReceive)) {
|
||
let accountRow = await accountService.selectById(c, accountId);
|
||
allReceive = accountRow.allReceive;
|
||
}
|
||
|
||
let list = await orm(c).select({ ...emailBriefColumns }).from(email)
|
||
.innerJoin(
|
||
account,
|
||
eq(account.accountId, email.accountId)
|
||
)
|
||
.where(
|
||
and(
|
||
gt(email.emailId, emailId),
|
||
eq(email.userId, userId),
|
||
eq(email.isDel, isDel.NORMAL),
|
||
eq(account.isDel, isDel.NORMAL),
|
||
allReceive ? undefined : eq(email.accountId, accountId),
|
||
eq(email.type, emailConst.type.RECEIVE)
|
||
))
|
||
.orderBy(desc(email.emailId))
|
||
.limit(20);
|
||
|
||
return this.applyListText(list);
|
||
},
|
||
|
||
async physicsDelete(c, params) {
|
||
let { emailIds } = params;
|
||
emailIds = emailIds.split(',').map(Number);
|
||
await attService.removeByEmailIds(c, emailIds);
|
||
await starService.removeByEmailIds(c, emailIds);
|
||
await orm(c).delete(email).where(inArray(email.emailId, emailIds)).run();
|
||
},
|
||
|
||
async physicsDeleteUserIds(c, userIds) {
|
||
await attService.removeByUserIds(c, userIds);
|
||
await orm(c).delete(email).where(inArray(email.userId, userIds)).run();
|
||
},
|
||
|
||
updateEmailStatus(c, params) {
|
||
const { status, resendEmailId, message } = params;
|
||
return orm(c).update(email).set({
|
||
status: status,
|
||
message: message
|
||
}).where(eq(email.resendEmailId, resendEmailId)).returning().get();
|
||
},
|
||
|
||
async selectUserEmailCountList(c, userIds, type, del = isDel.NORMAL) {
|
||
const result = await orm(c)
|
||
.select({
|
||
userId: email.userId,
|
||
count: count(email.emailId)
|
||
})
|
||
.from(email)
|
||
.where(and(
|
||
inArray(email.userId, userIds),
|
||
eq(email.type, type),
|
||
eq(email.isDel, del),
|
||
ne(email.status, emailConst.status.SAVING),
|
||
))
|
||
.groupBy(email.userId);
|
||
return result;
|
||
},
|
||
|
||
async allList(c, params) {
|
||
|
||
let { emailId, size, name, subject, accountEmail, userEmail, type, timeSort, full } = params;
|
||
|
||
size = Number(size);
|
||
emailId = Number(emailId) || 0;
|
||
timeSort = Number(timeSort);
|
||
full = Number(full);
|
||
|
||
if (type === undefined) {
|
||
type = 'receive';
|
||
}
|
||
|
||
if (isNaN(size)) {
|
||
size = 10;
|
||
}
|
||
|
||
if (size > 50) {
|
||
size = 50;
|
||
}
|
||
|
||
if (isNaN(full)) {
|
||
full = 1;
|
||
}
|
||
|
||
full = full === 1;
|
||
|
||
const filters = this.allEmailListFilters({ emailId, name, subject, accountEmail, userEmail, type, timeSort });
|
||
const countFilters = this.allEmailListFilters({ emailId, name, subject, accountEmail, userEmail, type, timeSort, withCursor: false });
|
||
const columns = full ? emailListColumns : emailBriefColumns;
|
||
|
||
const query = orm(c).select({ ...columns, userEmail: user.email })
|
||
.from(email)
|
||
.leftJoin(user, eq(email.userId, user.userId))
|
||
.where(and(...filters));
|
||
|
||
// count 不搜用户时无需 join user
|
||
const queryCount = userEmail
|
||
? orm(c).select({ total: count() })
|
||
.from(email)
|
||
.leftJoin(user, eq(email.userId, user.userId))
|
||
.where(and(...countFilters))
|
||
: orm(c).select({ total: count() })
|
||
.from(email)
|
||
.where(and(...countFilters));
|
||
|
||
if (timeSort) {
|
||
query.orderBy(asc(email.emailId));
|
||
} else {
|
||
query.orderBy(desc(email.emailId));
|
||
}
|
||
|
||
const listQuery = query.limit(size).all();
|
||
const totalQuery = queryCount.get();
|
||
const latestEmailQuery = orm(c).select({
|
||
emailId: email.emailId,
|
||
accountId: email.accountId,
|
||
userId: email.userId,
|
||
}).from(email)
|
||
.where(eq(email.type, emailConst.type.RECEIVE))
|
||
.orderBy(desc(email.emailId)).limit(1).get();
|
||
|
||
let [list, totalRow, latestEmail] = await Promise.all([listQuery, totalQuery, latestEmailQuery]);
|
||
|
||
if (full) {
|
||
await this.emailAddAtt(c, list);
|
||
} else {
|
||
this.applyListText(list);
|
||
}
|
||
|
||
if (!latestEmail) {
|
||
latestEmail = {
|
||
emailId: 0,
|
||
accountId: 0,
|
||
userId: 0,
|
||
}
|
||
}
|
||
|
||
return { list: list, total: totalRow.total, latestEmail };
|
||
},
|
||
|
||
async allEmailLatest(c, params) {
|
||
|
||
const { emailId } = params;
|
||
|
||
let list = await orm(c).select({ ...emailBriefColumns, userEmail: user.email }).from(email)
|
||
.leftJoin(user, eq(email.userId, user.userId))
|
||
.where(
|
||
and(
|
||
gt(email.emailId, emailId),
|
||
eq(email.type, emailConst.type.RECEIVE)
|
||
))
|
||
.orderBy(desc(email.emailId))
|
||
.limit(20);
|
||
|
||
return this.applyListText(list);
|
||
},
|
||
|
||
async emailAddAtt(c, list) {
|
||
|
||
const emailIds = list.map(item => item.emailId);
|
||
|
||
if (emailIds.length > 0) {
|
||
|
||
const attList = await attService.selectByEmailIds(c, emailIds);
|
||
|
||
list.forEach(emailRow => {
|
||
const atts = attList.filter(attRow => attRow.emailId === emailRow.emailId);
|
||
emailRow.attList = atts;
|
||
});
|
||
}
|
||
},
|
||
|
||
async restoreByUserId(c, userId) {
|
||
await orm(c).update(email).set({ isDel: isDel.NORMAL }).where(eq(email.userId, userId)).run();
|
||
},
|
||
|
||
async completeReceive(c, status, emailId) {
|
||
return await orm(c).update(email).set({
|
||
isDel: isDel.NORMAL,
|
||
status: status
|
||
}).where(eq(email.emailId, emailId)).returning().get();
|
||
},
|
||
|
||
async completeReceiveAll(c) {
|
||
// 用 EXISTS 走 status=6 部分索引 + account 主键;避免 IN (SELECT account_id FROM account) 触发全盘扫描
|
||
await c.env.db.prepare(
|
||
`UPDATE email
|
||
SET status = ${emailConst.status.RECEIVE}
|
||
WHERE status = ${emailConst.status.SAVING}
|
||
AND EXISTS (SELECT 1 FROM account WHERE account.account_id = email.account_id)`
|
||
).run();
|
||
await c.env.db.prepare(
|
||
`UPDATE email
|
||
SET status = ${emailConst.status.NOONE}
|
||
WHERE status = ${emailConst.status.SAVING}`
|
||
).run();
|
||
},
|
||
|
||
async autoClean(c) {
|
||
const { autoCleanDays, autoCleanExclude } = await settingService.query(c);
|
||
const days = Number(autoCleanDays);
|
||
|
||
if (!days || days <= 0) {
|
||
return;
|
||
}
|
||
|
||
const cutoff = dayjs().subtract(days, 'day').format('YYYY-MM-DD HH:mm:ss');
|
||
const excludeEmails = String(autoCleanExclude || '')
|
||
.split(/[,,]/)
|
||
.map(item => item.trim())
|
||
.filter(Boolean);
|
||
|
||
let excludeUserIds = [];
|
||
if (excludeEmails.length) {
|
||
const rows = await orm(c)
|
||
.select({ userId: user.userId })
|
||
.from(user)
|
||
.where(sql`lower(${user.email}) IN (${sql.join(excludeEmails.map(email => sql`${email.toLowerCase()}`), sql`, `)})`)
|
||
.all();
|
||
excludeUserIds = rows.map(row => row.userId);
|
||
}
|
||
|
||
const batchSize = 95;
|
||
|
||
while (true) {
|
||
const conditions = [lt(email.createTime, cutoff)];
|
||
if (excludeUserIds.length) {
|
||
conditions.push(notInArray(email.userId, excludeUserIds));
|
||
}
|
||
|
||
const rows = await orm(c)
|
||
.select({ emailId: email.emailId })
|
||
.from(email)
|
||
.where(and(...conditions))
|
||
.limit(batchSize)
|
||
.all();
|
||
|
||
if (!rows.length) {
|
||
break;
|
||
}
|
||
|
||
const emailIds = rows.map(row => row.emailId);
|
||
await this.physicsDelete(c, { emailIds: emailIds.join(',') });
|
||
|
||
if (rows.length < batchSize) {
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
async batchDelete(c, params) {
|
||
let { sendName, sendEmail, toEmail, subject, startTime, endTime, type } = params
|
||
|
||
let right = type === 'left' || type === 'include'
|
||
let left = type === 'include'
|
||
|
||
const conditions = []
|
||
|
||
if (sendName) {
|
||
conditions.push(like(email.name,`${left ? '%' : ''}${sendName}${right ? '%' : ''}`))
|
||
}
|
||
|
||
if (subject) {
|
||
conditions.push(like(email.subject,`${left ? '%' : ''}${subject}${right ? '%' : ''}`))
|
||
}
|
||
|
||
if (sendEmail) {
|
||
conditions.push(like(email.sendEmail,`${left ? '%' : ''}${sendEmail}${right ? '%' : ''}`))
|
||
}
|
||
|
||
if (toEmail) {
|
||
conditions.push(like(email.toEmail,`${left ? '%' : ''}${toEmail}${right ? '%' : ''}`))
|
||
}
|
||
|
||
if (startTime && endTime) {
|
||
conditions.push(gte(email.createTime,`${startTime}`))
|
||
conditions.push(lte(email.createTime,`${endTime}`))
|
||
}
|
||
|
||
if (conditions.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const emailIdsRow = await orm(c).select({emailId: email.emailId}).from(email).where(conditions.length > 1 ? and(...conditions) : conditions[0]).all();
|
||
|
||
const emailIds = emailIdsRow.map(row => row.emailId);
|
||
|
||
if (emailIds.length === 0){
|
||
return;
|
||
}
|
||
|
||
await attService.removeByEmailIds(c, emailIds);
|
||
|
||
await orm(c).delete(email).where(conditions.length > 1 ? and(...conditions) : conditions[0]).run();
|
||
},
|
||
|
||
async physicsDeleteByAccountId(c, accountId) {
|
||
await attService.removeByAccountId(c, accountId);
|
||
await orm(c).delete(email).where(eq(email.accountId, accountId)).run();
|
||
},
|
||
|
||
async read(c, params, userId) {
|
||
const { emailIds } = params;
|
||
await orm(c).update(email).set({ unread: emailConst.unread.READ }).where(and(eq(email.userId, userId), inArray(email.emailId, emailIds)));
|
||
}
|
||
};
|
||
|
||
export default emailService;
|