新增公告弹窗和无人收件开关

This commit is contained in:
eoao
2025-08-03 21:03:54 +08:00
parent 3a40cfc63b
commit 658c1776d5
43 changed files with 857 additions and 474 deletions

View File

@@ -103,6 +103,10 @@ export const settingConst = {
ruleType: {
ALL: 0,
RULE: 1
},
noRecipient: {
OPEN: 0,
CLOSE: 1,
}
}

View File

@@ -11,6 +11,7 @@ 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';
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -28,7 +29,8 @@ export async function email(message, env, ctx) {
forwardEmail,
ruleEmail,
ruleType,
r2Domain
r2Domain,
noRecipient
} = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) {
@@ -47,7 +49,11 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content);
const account = await accountService.selectByEmailIncludeDelNoCase({ env: env }, message.to);
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
return;
}
if (account && account.email !== env.admin) {
@@ -61,9 +67,9 @@ export async function email(message, env, ctx) {
for (const item of banEmail) {
if (item.startsWith('*@')) {
if (verifyUtils.isDomain(item)) {
const banDomain = emailUtils.getDomain(item.toLowerCase());
const banDomain = item.toLowerCase();
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase());
if (banDomain === receiveDomain) {

View File

@@ -25,5 +25,14 @@ export const setting = sqliteTable('setting', {
ruleType: integer('rule_type').default(0).notNull(),
loginOpacity: integer('login_opacity').default(0.88),
resendTokens: text('resend_tokens').default("{}").notNull(),
noticeTitle: text('notice_title').default('').notNull(),
noticeContent: text('notice_content').default('').notNull(),
noticeType: text('notice_type').default('').notNull(),
noticeDuration: integer('notice_duration').default(0).notNull(),
noticePosition: text('notice_position').default('').notNull(),
noticeOffset: integer('notice_offset').default(0).notNull(),
noticeWidth: integer('notice_width').default(400).notNull(),
notice: integer('notice').default(0).notNull(),
noRecipient: integer('no_recipient').default(1).notNull()
});
export default setting

View File

@@ -61,38 +61,39 @@ const en = {
JWTMismatch: 'JWT secret mismatch',
perms: {
"邮件": "Email",
"邮件发送": "Send email",
"邮件删除": "Delete email",
"邮件发送": "Send Email",
"邮件删除": "Delete Email",
"邮箱侧栏": "Account",
"邮箱查看": "View account",
"邮箱添加": "Add account",
"邮箱删除": "Delete account",
"邮箱查看": "View Account",
"邮箱添加": "Add Account",
"邮箱删除": "Delete Account",
"个人设置": "Settings",
"用户注销": "Delete user",
"用户注销": "Delete User",
"分析页": "Analytics",
"数据查看": "View data",
"用户信息": "All users",
"用户查看": "View user",
"用户添加": "Add user",
"密码修改": "Change password",
"状态修改": "Change status",
"权限修改": "Change role",
"用户删除": "Delete user",
"邮件列表": "All mail",
"邮件查看": "View email",
"数据查看": "View Data",
"用户信息": "All Users",
"用户查看": "View User",
"用户添加": "Add User",
"密码修改": "Change Password",
"状态修改": "Change Status",
"权限修改": "Change Role",
"用户删除": "Delete User",
"邮件列表": "All Mail",
"邮件查看": "View Email",
"权限控制": "Role",
"身份查看": "View role",
"身份修改": "Change role",
"身份删除": "Delete role",
"注册密钥": "Invite code",
"密钥查看": "View code",
"密钥添加": "Add code",
"密钥删除": "Delete code",
"系统设置": "System settings",
"设置查看": "View settings",
"设置修改": "Change settings",
"物理清空": "Physical purge",
"发件重置": "Reset send count"
"身份添加": "Add Role",
"身份查看": "View Role",
"身份修改": "Change Role",
"身份删除": "Delete Role",
"注册密钥": "Invite Code",
"密钥查看": "View Code",
"密钥添加": "Add Code",
"密钥删除": "Delete Code",
"系统设置": "System Settings",
"设置查看": "View Settings",
"设置修改": "Change Settings",
"物理清空": "Physical Purge",
"发件重置": "Reset Send Count"
}
};

View File

@@ -82,6 +82,7 @@ const zh = {
"邮件查看": "邮件查看",
"权限控制": "权限控制",
"身份查看": "身份查看",
"身份添加": "身份添加",
"身份修改": "身份修改",
"身份删除": "身份删除",
"注册密钥": "注册密钥",

View File

@@ -2,13 +2,14 @@ import settingService from '../service/setting-service';
import emailUtils from '../utils/email-utils';
import {emailConst} from "../const/entity-const";
import { t } from '../i18n/i18n'
const init = {
async init(c) {
const secret = c.req.param('secret');
if (secret !== c.env.jwt_secret) {
return c.text('jwt_secret 不匹配');
return c.text(t('JWTMismatch'));
}
await this.intDB(c);
@@ -25,6 +26,19 @@ const init = {
async v1_6DB(c) {
const noticeContent = '<div style="color: teal;margin-bottom: 5px;">欢迎使用 Cloud Mail 🎉 </div >\n' +
'本项目仅供学习交流,禁止用于违法业务\n' +
'<br>\n' +
'请遵守当地法规,作者不承担任何法律责任\n' +
'<div style="display: flex;gap: 18px;margin-top: 10px;">\n' +
'<a href="https://github.com/eoao/cloud-mail" target="_blank" >\n' +
'<img src="https://api.iconify.design/codicon:github-inverted.svg" alt="GitHub" width="25" height="25" />\n' +
'</a>\n' +
'<a href="https://t.me/cloud_mail_tg" target="_blank" >\n' +
'<img src="https://api.iconify.design/logos:telegram.svg" alt="GitHub" width="25" height="25" />\n' +
'</a>\n' +
'</div>\n'
const ADD_COLUMN_SQL_LIST = [
`ALTER TABLE setting ADD COLUMN reg_verify_count INTEGER NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN add_verify_count INTEGER NOT NULL DEFAULT 1;`,
@@ -34,16 +48,42 @@ const init = {
count INTEGER NOT NULL DEFAULT 1,
type INTEGER NOT NULL DEFAULT 0,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
)`
)`,
`ALTER TABLE setting ADD COLUMN notice_title TEXT NOT NULL DEFAULT '公告';`,
`ALTER TABLE setting ADD COLUMN notice_content TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN notice_type TEXT NOT NULL DEFAULT 'none';`,
`ALTER TABLE setting ADD COLUMN notice_duration INTEGER NOT NULL DEFAULT 0;`,
`ALTER TABLE setting ADD COLUMN notice_offset INTEGER NOT NULL DEFAULT 0;`,
`ALTER TABLE setting ADD COLUMN notice_position TEXT NOT NULL DEFAULT 'top-right';`,
`ALTER TABLE setting ADD COLUMN notice_width INTEGER NOT NULL DEFAULT 340;`,
`ALTER TABLE setting ADD COLUMN notice INTEGER NOT NULL DEFAULT 0;`,
`ALTER TABLE setting ADD COLUMN no_recipient INTEGER NOT NULL DEFAULT 1;`,
`UPDATE role SET avail_domain = '';`,
`UPDATE role SET ban_email = '';`,
`CREATE INDEX IF NOT EXISTS idx_email_user_id_account_id ON email(user_id, account_id);`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`过字段添加,原因:${e.message}`);
console.warn(`过字段,原因:${e.message}`);
}
});
await Promise.all(promises);
await c.env.db.prepare(`UPDATE setting SET notice_content = ? WHERE notice_content = '';`).bind(noticeContent).run();
try {
await c.env.db.batch([
c.env.db.prepare(`DROP INDEX IF EXISTS idx_account_email`),
c.env.db.prepare(`DROP INDEX IF EXISTS idx_user_email`),
c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_account_email_nocase ON account (email COLLATE NOCASE)`),
c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email_nocase ON user (email COLLATE NOCASE)`)
]);
} catch (e) {
console.error(e.message)
}
},
async v1_5DB(c) {
@@ -97,13 +137,15 @@ const init = {
`ALTER TABLE user ADD COLUMN reg_key_id INTEGER NOT NULL DEFAULT 0;`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
});
await Promise.all(promises);
},
@@ -123,13 +165,15 @@ const init = {
`ALTER TABLE setting ADD COLUMN rule_type INTEGER NOT NULL DEFAULT 0;`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
});
await Promise.all(promises);
const nameColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('email') WHERE name = 'to_email' limit 1`).first();
@@ -158,13 +202,15 @@ const init = {
`ALTER TABLE email ADD COLUMN relation TEXT NOT NULL DEFAULT '';`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
});
await Promise.all(promises);
await this.receiveEmailToRecipient(c);
await this.initAccountName(c);
@@ -178,13 +224,6 @@ const init = {
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) {
@@ -215,13 +254,15 @@ const init = {
`ALTER TABLE attachments ADD COLUMN type INTEGER NOT NULL DEFAULT 0;`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
});
await Promise.all(promises);
// 创建 perm 表并初始化
await c.env.db.prepare(`
@@ -298,7 +339,7 @@ const init = {
INSERT INTO role (
role_id, name, key, create_time, sort, description, user_id, is_default, send_count, send_type, account_count
) VALUES (
1, '普通用户', NULL, '0000-00-00 00:00:00', 0, '只有普通使用权限', 0, 1, NULL, 'count', 10
1, '普通用户', NULL, '0000-00-00 00:00:00', 0, '只有普通使用权限', 0, 1, NULL, 'ban', 10
)
`).run();
}
@@ -323,7 +364,8 @@ const init = {
(104, 1, 24),
(105, 1, 4),
(106, 1, 5),
(107, 1, 1)
(107, 1, 1),
(108, 1, 3)
`).run();
}
},
@@ -468,5 +510,4 @@ const init = {
await c.env.db.batch(queryList);
}
};
export default init;

View File

@@ -40,7 +40,7 @@ const accountService = {
}
let accountRow = await this.selectByEmailIncludeDelNoCase(c, email);
let accountRow = await this.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError(t('isDelAccount'));
@@ -92,23 +92,8 @@ const accountService = {
return accountRow;
},
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();
},
selectByEmail(c, email) {
return orm(c).select().from(account).where(
and(
eq(account.email, email),
eq(account.isDel, isDel.NORMAL)))
.get();
return orm(c).select().from(account).where(sql`${account.email} COLLATE NOCASE = ${email}`).get();
},
list(c, params, userId) {

View File

@@ -58,9 +58,9 @@ const emailService = {
)
.where(
and(
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
eq(email.accountId, accountId),
eq(email.userId, userId),
eq(email.accountId, accountId),
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
eq(email.type, type),
eq(email.isDel, isDel.NORMAL)
)
@@ -280,7 +280,7 @@ const emailService = {
if (error) {
throw new BizError(error.error);
throw new BizError(error.message);
}
html = this.imgReplace(html, null, r2Domain);

View File

@@ -68,7 +68,7 @@ const loginService = {
regKeyId = result?.regKeyId
}
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
const accountRow = await accountService.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError(t('isDelUser'));

View File

@@ -23,11 +23,7 @@ const roleService = {
let roleRow = await orm(c).select().from(role).where(eq(role.name, name)).get();
if (roleRow) {
throw new BizError(t('roleNameExist'));
}
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
const notEmailIndex = banEmail.findIndex(item => (!verifyUtils.isEmail(item) && !verifyUtils.isDomain(item)))
if (notEmailIndex > -1) {
throw new BizError(t('notEmail'));
@@ -76,7 +72,7 @@ const roleService = {
delete params.isDefault
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
const notEmailIndex = banEmail.findIndex(item => (!verifyUtils.isEmail(item) && !verifyUtils.isDomain(item)))
if (notEmailIndex > -1) {
throw new BizError(t('notEmail'));
@@ -168,7 +164,7 @@ const roleService = {
const availIndex = availDomain.findIndex(item => {
const domain = emailUtils.getDomain(email.toLowerCase());
const availDomainItem = emailUtils.getDomain(item.toLowerCase());
const availDomainItem = item.toLowerCase();
console.log(domain,availDomainItem)
return domain === availDomainItem
})

View File

@@ -139,7 +139,15 @@ const settingService = {
domainList:settingRow.domainList,
regKey: settingRow.regKey,
regVerifyOpen: settingRow.regVerifyOpen,
addVerifyOpen: settingRow.addVerifyOpen
addVerifyOpen: settingRow.addVerifyOpen,
noticeTitle: settingRow.noticeTitle,
noticeContent: settingRow.noticeContent,
noticeType: settingRow.noticeType,
noticeDuration: settingRow.noticeDuration,
noticePosition: settingRow.noticePosition,
noticeWidth: settingRow.noticeWidth,
noticeOffset: settingRow.noticeOffset,
notice: settingRow.notice,
};
}
};

View File

@@ -71,7 +71,7 @@ const userService = {
},
selectByEmailIncludeDel(c, email) {
return orm(c).select().from(user).where(eq(user.email, email)).get();
return orm(c).select().from(user).where(sql`${user.email} COLLATE NOCASE = ${email}`).get();
},
selectById(c, userId) {
@@ -355,6 +355,8 @@ const userService = {
const userId = await userService.insert(c, { email, password: hash, salt, type });
await userService.updateUserInfo(c, userId, true);
await accountService.insert(c, { userId: userId, email, type, name: emailUtils.getName(email) });
},

View File

@@ -1,6 +1,9 @@
const verifyUtils = {
isEmail(str) {
return /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(str);
},
isDomain(str) {
return /^(?!:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(str);
}
}