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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,9 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BwB6muO3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEDjT-8v.css">
<script type="module" crossorigin src="/assets/index-DQO7jFFS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BP2DuLPL.css">
</head>
<body>
<div id="loading-first">

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);
}
}

View File

@@ -1,6 +1,6 @@
name = "cloud-mail"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]
@@ -33,4 +33,4 @@ crons = ["0 16 * * *"] #定时任务每天晚上12点执行
#orm_log = false
#domain = [] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
#admin = "" #管理员的邮箱 示例: admin@example.com
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串

View File

@@ -1,6 +1,6 @@
name = "cloud-mail-dev"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]
@@ -32,7 +32,7 @@ run_worker_first = true
crons = ["0 16 * * *"]
[vars]
orm_log = false
orm_log = true
domain = ["example.com", "example2.com", "example3.com", "example4.com"]
admin = "admin@example.com"
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"

View File

@@ -1,6 +1,6 @@
name = "cloud-mail-test"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]

View File

@@ -1,6 +1,6 @@
name = "cloud-mail"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]