新增邮件群发和数据可视化
This commit is contained in:
@@ -18,3 +18,7 @@ app.post('/account/add', async (c) => {
|
||||
return c.json(result.ok(account));
|
||||
});
|
||||
|
||||
app.put('/account/setName', async (c) => {
|
||||
await accountService.setName(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
8
mail-worker/src/api/analysis-api.js
Normal file
8
mail-worker/src/api/analysis-api.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import app from '../hono/hono';
|
||||
import analysisService from '../service/analysis-service';
|
||||
import result from '../model/result';
|
||||
|
||||
app.get('/analysis/echarts', async (c) => {
|
||||
const data = await analysisService.echarts(c);
|
||||
return c.json(result.ok(data));
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import app from '../hono/hono';
|
||||
import initService from '../service/init-service';
|
||||
import initService from '../init/init';
|
||||
|
||||
app.get('/init/:secret', (c) => {
|
||||
return initService.init(c);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const KvConst = {
|
||||
AUTH_INFO: 'auth-uid:',
|
||||
SETTING: 'setting:',
|
||||
SEND_DAY_COUNT: 'send_day_count:'
|
||||
}
|
||||
|
||||
export default KvConst;
|
||||
|
||||
104
mail-worker/src/dao/analysis-dao.js
Normal file
104
mail-worker/src/dao/analysis-dao.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const analysisDao = {
|
||||
async numberCount(c) {
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
e.receiveTotal,
|
||||
e.sendTotal,
|
||||
e.delReceiveTotal,
|
||||
e.delSendTotal,
|
||||
e.normalReceiveTotal,
|
||||
e.normalSendTotal,
|
||||
u.userTotal,
|
||||
u.normalUserTotal,
|
||||
u.delUserTotal,
|
||||
a.accountTotal,
|
||||
a.normalAccountTotal,
|
||||
a.delAccountTotal
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
SUM(CASE WHEN type = 0 THEN 1 ELSE 0 END) AS receiveTotal,
|
||||
SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END) AS sendTotal,
|
||||
SUM(CASE WHEN type = 0 AND is_del = 1 THEN 1 ELSE 0 END) AS delReceiveTotal,
|
||||
SUM(CASE WHEN type = 1 AND is_del = 1 THEN 1 ELSE 0 END) AS delSendTotal,
|
||||
SUM(CASE WHEN type = 0 AND is_del = 0 THEN 1 ELSE 0 END) AS normalReceiveTotal,
|
||||
SUM(CASE WHEN type = 1 AND is_del = 0 THEN 1 ELSE 0 END) AS normalSendTotal
|
||||
FROM
|
||||
email
|
||||
) e
|
||||
CROSS JOIN (
|
||||
SELECT
|
||||
COUNT(*) AS userTotal,
|
||||
SUM(CASE WHEN is_del = 1 THEN 1 ELSE 0 END) AS delUserTotal,
|
||||
SUM(CASE WHEN is_del = 0 THEN 1 ELSE 0 END) AS normalUserTotal
|
||||
FROM
|
||||
user
|
||||
) u
|
||||
CROSS JOIN (
|
||||
SELECT
|
||||
COUNT(*) AS accountTotal,
|
||||
SUM(CASE WHEN is_del = 1 THEN 1 ELSE 0 END) AS delAccountTotal,
|
||||
SUM(CASE WHEN is_del = 0 THEN 1 ELSE 0 END) AS normalAccountTotal
|
||||
FROM
|
||||
account
|
||||
) a
|
||||
`).all();
|
||||
return results[0];
|
||||
},
|
||||
|
||||
async userDayCount(c) {
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
DATE(create_time,'+8 hours') AS date,
|
||||
COUNT(*) AS total
|
||||
FROM
|
||||
user
|
||||
WHERE
|
||||
DATE(create_time,'+8 hours') BETWEEN DATE('now', '-14 days', '+8 hours') AND DATE('now','-1 day','+8 hours')
|
||||
GROUP BY
|
||||
DATE(create_time,'+8 hours')
|
||||
ORDER BY
|
||||
date ASC
|
||||
`).all();
|
||||
return results;
|
||||
},
|
||||
|
||||
async receiveDayCount(c) {
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
DATE(create_time,'+8 hours') AS date,
|
||||
COUNT(*) AS total
|
||||
FROM
|
||||
email
|
||||
WHERE
|
||||
DATE(create_time,'+8 hours') BETWEEN DATE('now', '-14 days', '+8 hours') AND DATE('now','-1 day','+8 hours')
|
||||
AND type = 0
|
||||
GROUP BY
|
||||
DATE(create_time,'+8 hours')
|
||||
ORDER BY
|
||||
date ASC
|
||||
`).all();
|
||||
return results;
|
||||
},
|
||||
|
||||
async sendDayCount(c) {
|
||||
const { results } = await c.env.db.prepare(`
|
||||
SELECT
|
||||
DATE(create_time,'+8 hours') AS date,
|
||||
COUNT(*) AS total
|
||||
FROM
|
||||
email
|
||||
WHERE
|
||||
DATE(create_time,'+8 hours') BETWEEN DATE('now', '-14 days', '+8 hours') AND DATE('now','-1 day','+8 hours')
|
||||
AND type = 1
|
||||
GROUP BY
|
||||
DATE(create_time,'+8 hours')
|
||||
ORDER BY
|
||||
date ASC
|
||||
`).all();
|
||||
return results;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default analysisDao;
|
||||
@@ -28,13 +28,20 @@ export async function email(message, env, ctx) {
|
||||
|
||||
const email = await PostalMime.parse(content);
|
||||
|
||||
console.warn(email)
|
||||
|
||||
const params = {
|
||||
sendEmail: email.from.address,
|
||||
name: email.from.name,
|
||||
receiveEmail: message.to,
|
||||
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.userId,
|
||||
accountId: account.accountId,
|
||||
isDel: isDel.DELETE,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { sql } from 'drizzle-orm';
|
||||
export const account = sqliteTable('account', {
|
||||
accountId: integer('account_id').primaryKey({ autoIncrement: true }),
|
||||
email: text('email').notNull(),
|
||||
name: text('name').notNull().default(''),
|
||||
status: integer('status').default(0).notNull(),
|
||||
latestEmailTime: text('latest_email_time'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
|
||||
|
||||
@@ -4,12 +4,17 @@ export const email = sqliteTable('email', {
|
||||
emailId: integer('email_id').primaryKey({ autoIncrement: true }),
|
||||
sendEmail: text('send_email'),
|
||||
name: text('name'),
|
||||
receiveEmail: text('receive_email').notNull(),
|
||||
accountId: integer('account_id').notNull(),
|
||||
userId: integer('user_id').notNull(),
|
||||
subject: text('subject'),
|
||||
text: text('text'),
|
||||
content: text('content'),
|
||||
cc: text('cc').default('[]'),
|
||||
bcc: text('bcc').default('[]'),
|
||||
recipient: text('recipient'),
|
||||
inReplyTo: text('in_reply_to').default(''),
|
||||
relation: text('relation').default(''),
|
||||
messageId: text('message_id').default(''),
|
||||
type: integer('type').default(0).notNull(),
|
||||
status: integer('status').default(0).notNull(),
|
||||
resendEmailId: text('resend_email_id'),
|
||||
|
||||
@@ -15,4 +15,5 @@ import '../api/my-api';
|
||||
import '../api/role-api'
|
||||
import '../api/sys-email-api'
|
||||
import '../api/init-api'
|
||||
import '../api/analysis-api'
|
||||
export default app;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import settingService from './setting-service';
|
||||
|
||||
const initService = {
|
||||
import settingService from '../service/setting-service';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
const init = {
|
||||
async init(c) {
|
||||
|
||||
const secret = c.req.param('secret');
|
||||
@@ -10,12 +10,53 @@ const initService = {
|
||||
}
|
||||
|
||||
await this.intDB(c);
|
||||
await this.v1DB(c);
|
||||
await this.v1_1DB(c);
|
||||
await this.v1_2DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text('初始化成功');
|
||||
},
|
||||
|
||||
async v1DB(c) {
|
||||
async v1_2DB(c){
|
||||
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
`ALTER TABLE email ADD COLUMN recipient TEXT NOT NULL DEFAULT '[]';`,
|
||||
`ALTER TABLE email ADD COLUMN cc TEXT NOT NULL DEFAULT '[]';`,
|
||||
`ALTER TABLE email ADD COLUMN bcc TEXT NOT NULL DEFAULT '[]';`,
|
||||
`ALTER TABLE email ADD COLUMN message_id TEXT NOT NULL DEFAULT '';`,
|
||||
`ALTER TABLE email ADD COLUMN in_reply_to TEXT NOT NULL DEFAULT '';`,
|
||||
`ALTER TABLE email ADD COLUMN relation TEXT NOT NULL DEFAULT '';`
|
||||
];
|
||||
|
||||
for (let sql of ADD_COLUMN_SQL_LIST) {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.receiveEmailToRecipient(c);
|
||||
await this.initAccountName(c);
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO perm (perm_id, name, perm_key, pid, type, sort) VALUES
|
||||
(31,'分析页', NULL, 0, 1, 2.1),
|
||||
(32,'数据查看', 'analysis:query', 31, 2, 1)`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过数据,原因:${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_account_email ON account (email)`).run();
|
||||
await c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email ON user (email)`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过添加唯一邮箱索引,原因:${e.message}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async v1_1DB(c) {
|
||||
// 添加字段
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
`ALTER TABLE email ADD COLUMN type INTEGER NOT NULL DEFAULT 0;`,
|
||||
@@ -81,7 +122,6 @@ const initService = {
|
||||
(11, '用户删除', 'user:delete', 6, 2, 7),
|
||||
(12, '用户收藏', 'user:star', 6, 2, 5),
|
||||
(13, '权限控制', '', 0, 1, 5),
|
||||
(30, '身份添加', 'role:add', 13, 2, -1),
|
||||
(14, '身份查看', 'role:query', 13, 2, 0),
|
||||
(15, '身份修改', 'role:set', 13, 2, 1),
|
||||
(16, '身份删除', 'role:delete', 13, 2, 2),
|
||||
@@ -97,7 +137,8 @@ const initService = {
|
||||
(26, '发件重置', 'user:reset-send', 6, 2, 6),
|
||||
(27, '邮件列表', '', 0, 1, 4),
|
||||
(28, '邮件查看', 'sys-email:query', 27, 2, 0),
|
||||
(29, '邮件删除', 'sys-email:delete', 27, 2, 0)
|
||||
(29, '邮件删除', 'sys-email:delete', 27, 2, 0),
|
||||
(30, '身份添加', 'role:add', 13, 2, -1)
|
||||
`).run();
|
||||
}
|
||||
|
||||
@@ -163,7 +204,6 @@ const initService = {
|
||||
email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
send_email TEXT,
|
||||
name TEXT,
|
||||
receive_email TEXT NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
subject TEXT,
|
||||
@@ -247,7 +287,55 @@ const initService = {
|
||||
SELECT 0, 0, 0, 1, 'Cloud 邮箱', 0, 1, 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM setting)
|
||||
`).run();
|
||||
},
|
||||
|
||||
async receiveEmailToRecipient(c) {
|
||||
|
||||
const receiveEmailColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('email') WHERE name = 'receive_email' limit 1`).first();
|
||||
|
||||
if (!receiveEmailColumn) {
|
||||
return
|
||||
}
|
||||
|
||||
const queryList = []
|
||||
const {results} = await c.env.db.prepare('SELECT receive_email,email_id FROM email').all();
|
||||
results.forEach(emailRow => {
|
||||
const recipient = {}
|
||||
recipient.address = emailRow.receive_email
|
||||
recipient.name = ''
|
||||
const recipientStr = JSON.stringify([recipient]);
|
||||
const sql = c.env.db.prepare('UPDATE email SET recipient = ? WHERE email_id = ?').bind(recipientStr,emailRow.email_id);
|
||||
queryList.push(sql)
|
||||
})
|
||||
|
||||
queryList.push(c.env.db.prepare("ALTER TABLE email DROP COLUMN receive_email"));
|
||||
|
||||
await c.env.db.batch(queryList);
|
||||
},
|
||||
|
||||
|
||||
async initAccountName(c) {
|
||||
|
||||
const nameColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('account') WHERE name = 'name' limit 1`).first();
|
||||
|
||||
if (nameColumn) {
|
||||
return
|
||||
}
|
||||
|
||||
const queryList = []
|
||||
|
||||
queryList.push(c.env.db.prepare(`ALTER TABLE account ADD COLUMN name TEXT NOT NULL DEFAULT ''`));
|
||||
|
||||
const {results} = await c.env.db.prepare(`SELECT account_id, email FROM account`).all();
|
||||
|
||||
results.forEach(accountRow => {
|
||||
const name = emailUtils.getName(accountRow.email);
|
||||
const sql = c.env.db.prepare('UPDATE account SET name = ? WHERE account_id = ?').bind(name,accountRow.account_id);
|
||||
queryList.push(sql)
|
||||
})
|
||||
|
||||
await c.env.db.batch(queryList);
|
||||
}
|
||||
};
|
||||
|
||||
export default initService;
|
||||
export default init;
|
||||
@@ -65,7 +65,8 @@ const premKey = {
|
||||
'sys-email:delete': ['/sys-email/delete'],
|
||||
'setting:query': [],
|
||||
'setting:set': ['/setting/set', '/setting/setBackground'],
|
||||
'setting:clean': ['/setting/physicsDeleteAll']
|
||||
'setting:clean': ['/setting/physicsDeleteAll'],
|
||||
'analysis:query': ['/analysis/echarts']
|
||||
};
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import userService from './user-service';
|
||||
import emailService from './email-service';
|
||||
import orm from '../entity/orm';
|
||||
import account from '../entity/account';
|
||||
import { and, asc, eq, gt, inArray, count, like } from 'drizzle-orm';
|
||||
import { and, asc, eq, gt, inArray, count, sql } from 'drizzle-orm';
|
||||
import { isDel } from '../const/entity-const';
|
||||
import settingService from './setting-service';
|
||||
import turnstileService from './turnstile-service';
|
||||
@@ -33,7 +33,7 @@ const accountService = {
|
||||
throw new BizError('不存在的邮箱域名');
|
||||
}
|
||||
|
||||
const accountRow = await this.selectByEmailIncludeDel(c, email);
|
||||
const accountRow = await this.selectByEmailIncludeDelNoCase(c, email);
|
||||
|
||||
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
@@ -55,9 +55,16 @@ const accountService = {
|
||||
await turnstileService.verify(c, token);
|
||||
}
|
||||
|
||||
return orm(c).insert(account).values({ email: email, userId: userId }).returning().get();
|
||||
return orm(c).insert(account).values({ email: email, userId: userId, name: emailUtils.getName(email) }).returning().get();
|
||||
},
|
||||
|
||||
selectByEmailIncludeDelNoCase(c, email) {
|
||||
return orm(c)
|
||||
.select()
|
||||
.from(account)
|
||||
.where(sql`${account.email} COLLATE NOCASE = ${email}`)
|
||||
.get();
|
||||
},
|
||||
selectByEmailIncludeDel(c, email) {
|
||||
return orm(c).select().from(account).where(eq(account.email, email)).get();
|
||||
},
|
||||
@@ -170,6 +177,11 @@ const accountService = {
|
||||
|
||||
async restoreByUserId(c, userId) {
|
||||
await orm(c).update(account).set({isDel: isDel.NORMAL}).where(eq(account.userId, userId)).run();
|
||||
},
|
||||
|
||||
async setName(c, params, userId) {
|
||||
const { name, accountId } = params
|
||||
await orm(c).update(account).set({name}).where(and(eq(account.userId, userId),eq(account.accountId, accountId))).run();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
88
mail-worker/src/service/analysis-service.js
Normal file
88
mail-worker/src/service/analysis-service.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import analysisDao from '../dao/analysis-dao';
|
||||
import orm from '../entity/orm';
|
||||
import email from '../entity/email';
|
||||
import { desc, count, eq } from 'drizzle-orm';
|
||||
import { emailConst } from '../const/entity-const';
|
||||
import kvConst from '../const/kv-const';
|
||||
import dayjs from 'dayjs';
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const analysisService = {
|
||||
|
||||
async echarts(c) {
|
||||
|
||||
|
||||
const [
|
||||
numberCount,
|
||||
nameRatio,
|
||||
sendEmailRatio,
|
||||
userDayCountRaw,
|
||||
receiveDayCountRaw,
|
||||
sendDayCountRaw,
|
||||
daySendTotalRaw
|
||||
] = await Promise.all([
|
||||
analysisDao.numberCount(c),
|
||||
|
||||
orm(c)
|
||||
.select({ name: email.name, total: count() })
|
||||
.from(email)
|
||||
.where(eq(email.type, emailConst.type.RECEIVE))
|
||||
.groupBy(email.name)
|
||||
.orderBy(desc(count()))
|
||||
.limit(6),
|
||||
|
||||
orm(c)
|
||||
.select({ email: email.sendEmail, total: count() })
|
||||
.from(email)
|
||||
.where(eq(email.type, emailConst.type.RECEIVE))
|
||||
.groupBy(email.sendEmail)
|
||||
.orderBy(desc(count()))
|
||||
.limit(6),
|
||||
|
||||
analysisDao.userDayCount(c),
|
||||
analysisDao.receiveDayCount(c),
|
||||
analysisDao.sendDayCount(c),
|
||||
|
||||
c.env.kv.get(kvConst.SEND_DAY_COUNT + dayjs().format('YYYY-MM-DD')),
|
||||
]);
|
||||
|
||||
|
||||
const userDayCount = this.filterEmptyDay(userDayCountRaw);
|
||||
const receiveDayCount = this.filterEmptyDay(receiveDayCountRaw);
|
||||
const sendDayCount = this.filterEmptyDay(sendDayCountRaw);
|
||||
|
||||
const daySendTotal = daySendTotalRaw || 0;
|
||||
|
||||
return {
|
||||
numberCount,
|
||||
userDayCount,
|
||||
receiveRatio: {
|
||||
nameRatio,
|
||||
sendEmailRatio
|
||||
},
|
||||
emailDayCount: {
|
||||
receiveDayCount,
|
||||
sendDayCount
|
||||
},
|
||||
daySendTotal: Number(daySendTotal)
|
||||
};
|
||||
},
|
||||
|
||||
filterEmptyDay(data) {
|
||||
const today = dayjs().tz('Asia/Shanghai').subtract(1, 'day');
|
||||
const previousDays = Array.from({ length: 15 }, (_, i) => {
|
||||
return today.subtract(i, 'day').format('YYYY-MM-DD');
|
||||
}).reverse();
|
||||
|
||||
return previousDays.map(day => {
|
||||
const index = data.findIndex(item => item.date === day)
|
||||
const total = index > - 1 ? data[index].total : 0
|
||||
return {date: day,total}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default analysisService
|
||||
@@ -15,6 +15,9 @@ import roleService from './role-service';
|
||||
import user from '../entity/user';
|
||||
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 = {
|
||||
|
||||
@@ -146,11 +149,10 @@ const emailService = {
|
||||
|
||||
async send(c, params, userId) {
|
||||
|
||||
let { accountId, name, receiveEmail, text, content, subject, attachments } = params;
|
||||
let { accountId, name, sendType, emailId, receiveEmail, manyType, text, content, subject, attachments } = params;
|
||||
|
||||
const { resendTokens, r2Domain, send } = await settingService.query(c);
|
||||
|
||||
|
||||
const { attDataList, html } = await attService.toImageUrlHtml(c, content, r2Domain);
|
||||
|
||||
if (attDataList.length > 0 && !r2Domain) {
|
||||
@@ -173,13 +175,26 @@ const emailService = {
|
||||
throw new BizError('邮箱发送功能已停用', 403);
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && manyType === 'divide') {
|
||||
throw new BizError('分别发送暂时不支持附件');
|
||||
}
|
||||
|
||||
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const roleRow = await roleService.selectById(c, userRow.type);
|
||||
|
||||
if (userRow.sendCount >= roleRow.sendCount && c.env.admin !== userRow.email) {
|
||||
if (roleRow.sendType === 'day') throw new BizError('已到达每日发送数量限制', 403);
|
||||
if (roleRow.sendType === 'count') throw new BizError('已到达发送数量限制', 403);
|
||||
if (c.env.admin !== userRow.email && roleRow.sendCount) {
|
||||
|
||||
if (userRow.sendCount >= roleRow.sendCount) {
|
||||
if (roleRow.sendType === 'day') throw new BizError('已到达每日发送次数限制', 403);
|
||||
if (roleRow.sendType === 'count') throw new BizError('已到达发送次数限制', 403);
|
||||
}
|
||||
|
||||
if (userRow.sendCount + receiveEmail.length > roleRow.sendCount) {
|
||||
if (roleRow.sendType === 'day') throw new BizError('剩余每日发送次数不足', 403);
|
||||
if (roleRow.sendType === 'count') throw new BizError('剩余发送次数不足', 403);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -203,53 +218,161 @@ const emailService = {
|
||||
name = emailUtils.getName(accountRow.email);
|
||||
}
|
||||
|
||||
|
||||
let emailRow = {
|
||||
messageId: null
|
||||
};
|
||||
|
||||
if (sendType === 'reply') {
|
||||
|
||||
emailRow = await this.selectById(c, emailId);
|
||||
|
||||
if (!emailRow) {
|
||||
throw new BizError('邮件不存在无法回复');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let resendResult = null;
|
||||
|
||||
const resend = new Resend(resendToken);
|
||||
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: `${name} <${accountRow.email}>`,
|
||||
to: [receiveEmail],
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
attachments: attachments
|
||||
});
|
||||
if (manyType === 'divide') {
|
||||
|
||||
let sendFormList = [];
|
||||
|
||||
receiveEmail.forEach(email => {
|
||||
const sendForm = {
|
||||
from: `${name} <${accountRow.email}>`,
|
||||
to: [email],
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html
|
||||
};
|
||||
|
||||
if (sendType === 'reply') {
|
||||
sendForm.headers = {
|
||||
'in-reply-to': emailRow.messageId,
|
||||
'references': emailRow.messageId
|
||||
};
|
||||
}
|
||||
|
||||
sendFormList.push(sendForm);
|
||||
});
|
||||
|
||||
resendResult = await resend.batch.send(sendFormList);
|
||||
|
||||
} else {
|
||||
|
||||
const sendForm = {
|
||||
from: `${name} <${accountRow.email}>`,
|
||||
to: [...receiveEmail],
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
attachments: attachments
|
||||
};
|
||||
|
||||
if (sendType === 'reply') {
|
||||
sendForm.headers = {
|
||||
'in-reply-to': emailRow.messageId,
|
||||
'references': emailRow.messageId
|
||||
};
|
||||
}
|
||||
|
||||
resendResult = await resend.emails.send(sendForm);
|
||||
|
||||
}
|
||||
|
||||
const { data, error } = resendResult;
|
||||
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw new BizError(error.message);
|
||||
}
|
||||
|
||||
|
||||
const emailData = {};
|
||||
emailData.sendEmail = accountRow.email;
|
||||
emailData.name = name;
|
||||
emailData.subject = subject;
|
||||
emailData.receiveEmail = receiveEmail;
|
||||
emailData.content = html;
|
||||
emailData.text = text;
|
||||
emailData.accountId = accountId;
|
||||
emailData.type = emailConst.type.SEND;
|
||||
emailData.userId = userId;
|
||||
emailData.status = emailConst.status.SENT;
|
||||
emailData.resendEmailId = data.id;
|
||||
|
||||
const emailRow = await orm(c).insert(email).values(emailData).returning().get();
|
||||
const emailDataList = [];
|
||||
|
||||
if (attDataList.length > 0) {
|
||||
await attService.saveArticleAtt(c, attDataList, userId, accountId, emailRow.emailId);
|
||||
if (manyType === 'divide') {
|
||||
|
||||
receiveEmail.forEach((item, index) => {
|
||||
const emailDataItem = { ...emailData };
|
||||
emailDataItem.resendEmailId = data.data[index].id;
|
||||
emailDataItem.recipient = JSON.stringify([{ address: item, name: '' }]);
|
||||
emailDataList.push(emailDataItem);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
emailData.resendEmailId = data.id;
|
||||
|
||||
const recipient = [];
|
||||
|
||||
receiveEmail.forEach(item => {
|
||||
recipient.push({ address: item, name: '' });
|
||||
});
|
||||
|
||||
emailData.recipient = JSON.stringify(recipient);
|
||||
|
||||
emailDataList.push(emailData);
|
||||
}
|
||||
|
||||
if (sendType === 'reply') {
|
||||
emailDataList.forEach(emailData => {
|
||||
emailData.inReplyTo = emailRow.messageId;
|
||||
emailData.relation = emailRow.messageId;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (roleRow.sendCount) {
|
||||
await userService.incrUserService(c, 1, userId);
|
||||
await userService.incrUserSendCount(c, receiveEmail.length, userId);
|
||||
}
|
||||
|
||||
if (attachments?.length > 0 && c.env.r2) {
|
||||
await attService.saveSendAtt(c, attachments, userId, accountId, emailRow.emailId);
|
||||
const emailRowList = await Promise.all(
|
||||
emailDataList.map(async (emailData) => {
|
||||
const emailRow = await orm(c).insert(email).values(emailData).returning().get();
|
||||
|
||||
if (attDataList.length > 0) {
|
||||
await attService.saveArticleAtt(c, attDataList, userId, accountId, emailRow.emailId);
|
||||
}
|
||||
|
||||
if (attachments?.length > 0 && c.env.r2) {
|
||||
await attService.saveSendAtt(c, attachments, userId, accountId, emailRow.emailId);
|
||||
}
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, [emailRow.emailId]);
|
||||
emailRow.attList = attsList;
|
||||
|
||||
return emailRow;
|
||||
})
|
||||
);
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, [emailRow.emailId]);
|
||||
emailRow.attList = attsList;
|
||||
|
||||
return emailRow;
|
||||
return emailRowList;
|
||||
},
|
||||
|
||||
selectById(c, emailId) {
|
||||
@@ -398,7 +521,7 @@ const emailService = {
|
||||
conditions.push(like(email.subject, `${subject}%`));
|
||||
}
|
||||
|
||||
conditions.push(ne(email.status, emailConst.status.SAVING))
|
||||
conditions.push(ne(email.status, emailConst.status.SAVING));
|
||||
|
||||
const countConditions = [...conditions];
|
||||
|
||||
@@ -447,7 +570,10 @@ const emailService = {
|
||||
},
|
||||
|
||||
async completeReceive(c, emailId) {
|
||||
await orm(c).update(email).set({ isDel: isDel.NORMAL, status: emailConst.status.RECEIVE }).where(eq(email.emailId, emailId)).run();
|
||||
await orm(c).update(email).set({
|
||||
isDel: isDel.NORMAL,
|
||||
status: emailConst.status.RECEIVE
|
||||
}).where(eq(email.emailId, emailId)).run();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const loginService = {
|
||||
throw new BizError('非法邮箱域名');
|
||||
}
|
||||
|
||||
const accountRow = await accountService.selectByEmailIncludeDel(c, email);
|
||||
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
|
||||
|
||||
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
@@ -60,7 +60,7 @@ const loginService = {
|
||||
|
||||
await userService.updateUserInfo(c, userId, true);
|
||||
|
||||
await accountService.insert(c, { userId: userId, email });
|
||||
await accountService.insert(c, { userId: userId, email, name: emailUtils.getName(email) });
|
||||
},
|
||||
|
||||
async login(c, params) {
|
||||
@@ -110,7 +110,6 @@ const loginService = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
await userService.updateUserInfo(c, userRow.userId);
|
||||
|
||||
await c.env.kv.put(KvConst.AUTH_INFO + userRow.userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
|
||||
|
||||
@@ -33,6 +33,7 @@ const userService = {
|
||||
user.sendCount = userRow.sendCount;
|
||||
user.email = userRow.email;
|
||||
user.accountId = account.accountId;
|
||||
user.name = account.name;
|
||||
user.permKeys = permKeys;
|
||||
user.role = roleRow
|
||||
|
||||
@@ -304,7 +305,7 @@ const userService = {
|
||||
|
||||
},
|
||||
|
||||
async incrUserService(c, quantity, userId) {
|
||||
async incrUserSendCount(c, quantity, userId) {
|
||||
await orm(c).update(user).set({
|
||||
sendCount: sql`${user.sendCount}
|
||||
+
|
||||
@@ -352,7 +353,7 @@ const userService = {
|
||||
|
||||
const userId = await userService.insert(c, { email, password: hash, salt, type });
|
||||
|
||||
await accountService.insert(c, { userId: userId, email, type });
|
||||
await accountService.insert(c, { userId: userId, email, type, name: emailUtils.getName(email) });
|
||||
},
|
||||
|
||||
async resetDaySendCount(c) {
|
||||
|
||||
Reference in New Issue
Block a user