feat: add address activity disable switch (#1138)

* feat: add address activity disable switch

* style: separate address activity disable guards

* docs: clarify address activity switch behavior
This commit is contained in:
Dream Hunter
2026-09-06 23:08:41 +08:00
committed by GitHub
parent 66ab5a05df
commit 406d1956b8
11 changed files with 23 additions and 1 deletions
+1
View File
@@ -10,6 +10,7 @@
### Features ### Features
- feat: |Worker| 新增 `DISABLE_ADDRESS_UPDATED_AT`,可关闭单地址及用户批量的主动保活刷新,并禁止内置手动及定时不活跃地址清理,降低 D1 写入量
- feat: |Frontend| 新增 `VITE_DEFAULT_LANG` 构建变量,并支持通过 `index.html` 运行时配置覆盖前端设置 - feat: |Frontend| 新增 `VITE_DEFAULT_LANG` 构建变量,并支持通过 `index.html` 运行时配置覆盖前端设置
- feat: |兑换码| 新增角色、发信额度及专属邮箱兑换与管理,完善并发保护和表单提示 - feat: |兑换码| 新增角色、发信额度及专属邮箱兑换与管理,完善并发保护和表单提示
- feat: |邮件| 新增可选的已读/未读状态,支持点击邮件自动已读和手动切换状态 - feat: |邮件| 新增可选的已读/未读状态,支持点击邮件自动已读和手动切换状态
+1
View File
@@ -10,6 +10,7 @@
### Features ### Features
- feat: |Worker| Add `DISABLE_ADDRESS_UPDATED_AT` to disable individual and user-wide address activity keep-alive updates and built-in manual/scheduled inactive-address cleanup, reducing D1 writes
- feat: |Frontend| Add the `VITE_DEFAULT_LANG` build variable and support overriding frontend settings through runtime configuration in `index.html` - feat: |Frontend| Add the `VITE_DEFAULT_LANG` build variable and support overriding frontend settings through runtime configuration in `index.html`
- feat: |Redemption Codes| Add role, sending-credit and custom-mailbox redemption with Admin management, concurrency protection and form validation - feat: |Redemption Codes| Add role, sending-credit and custom-mailbox redemption with Admin management, concurrency protection and form validation
- feat: |Mail| Add optional read/unread status with click-to-read and manual status switching - feat: |Mail| Add optional read/unread status with click-to-read and manual status switching
@@ -38,6 +38,7 @@ When `ADMIN_API_IP_WHITELIST` is unset or empty, source IPs are not restricted.
| `MIN_ADDRESS_LEN` | Number | Minimum length of `email address` name | `1` | | `MIN_ADDRESS_LEN` | Number | Minimum length of `email address` name | `1` |
| `MAX_ADDRESS_LEN` | Number | Maximum length of `email address` name | `30` | | `MAX_ADDRESS_LEN` | Number | Maximum length of `email address` name | `30` |
| `DISABLE_CUSTOM_ADDRESS_NAME` | Text/JSON | Disable custom email address names, if set to true, users cannot enter custom names and they will be auto-generated | `true` | | `DISABLE_CUSTOM_ADDRESS_NAME` | Text/JSON | Disable custom email address names, if set to true, users cannot enter custom names and they will be auto-generated | `true` |
| `DISABLE_ADDRESS_UPDATED_AT` | Text/JSON | Defaults to `false`. Set to `true` to skip individual and bulk activity timestamp updates triggered by user settings, mailbox access, sending mail, and other address activity. Initial timestamps for new addresses and password generation/change/reset behavior, including their `updated_at` updates, remain unchanged. Manual `/admin/cleanup` requests with `cleanType=inactiveAddress` return `403`; scheduled tasks skip that type and continue other cleanup. Cleanup by creation time, other cleanup types, and administrator-defined SQL are unaffected. Set back to `false` to restore normal behavior without backfilling activity from the disabled period | `true` |
| `ADDRESS_CHECK_REGEX` | Text | Regular expression for `email address` name, used for validation only | `^(?!.*admin).*` | | `ADDRESS_CHECK_REGEX` | Text | Regular expression for `email address` name, used for validation only | `^(?!.*admin).*` |
| `ADDRESS_REGEX` | Text | Regular expression to replace illegal symbols in `email address` name, symbols not in the regex will be replaced. Default is `[^a-z0-9]` if not set. Use with caution as some symbols may prevent email reception | `[^a-z0-9]` | | `ADDRESS_REGEX` | Text | Regular expression to replace illegal symbols in `email address` name, symbols not in the regex will be replaced. Default is `[^a-z0-9]` if not set. Use with caution as some symbols may prevent email reception | `[^a-z0-9]` |
| `DEFAULT_DOMAINS` | JSON | Default domains available to users (not logged in or users without assigned roles) | `["awsl.uk", "dreamhunter2333.xyz"]` | | `DEFAULT_DOMAINS` | JSON | Default domains available to users (not logged in or users without assigned roles) | `["awsl.uk", "dreamhunter2333.xyz"]` |
@@ -38,6 +38,7 @@
| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` | | `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` |
| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` | | `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` |
| `DISABLE_CUSTOM_ADDRESS_NAME` | 文本/JSON | 禁用自定义邮箱地址名称,如果设置为 true,则用户无法输入自定义邮箱名称,将由后台自动生成 | `true` | | `DISABLE_CUSTOM_ADDRESS_NAME` | 文本/JSON | 禁用自定义邮箱地址名称,如果设置为 true,则用户无法输入自定义邮箱名称,将由后台自动生成 | `true` |
| `DISABLE_ADDRESS_UPDATED_AT` | 文本/JSON | 默认 `false`。设为 `true` 时跳过用户设置、邮箱访问、发信等触发的单地址及批量活跃时间更新。创建地址的初始时间、生成/修改/重置密码及其 `updated_at` 更新不变。手动 `/admin/cleanup``cleanType=inactiveAddress` 返回 `403`,定时任务跳过该项并继续其他清理;按创建时间清理、其他清理类型及管理员自定义 SQL 不受影响。改回 `false` 恢复原有行为,不补齐禁用期间的活跃记录 | `true` |
| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` | | `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` |
| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` | | `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` |
| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` | | `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` |
+5 -1
View File
@@ -98,7 +98,11 @@ export default {
const msgs = i18n.getMessagesbyContext(c); const msgs = i18n.getMessagesbyContext(c);
const { cleanType, cleanDays } = await c.req.json(); const { cleanType, cleanDays } = await c.req.json();
try { try {
await cleanup(c, cleanType, cleanDays); const success = await cleanup(c, cleanType, cleanDays);
// Report disabled cleanup as forbidden rather than successful.
if (!success) {
return c.text(msgs.InactiveAddressCleanupDisabledMsg, 403);
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return c.text(`${msgs.OperationFailedMsg}: ${(error as Error).message}`, 500) return c.text(`${msgs.OperationFailedMsg}: ${(error as Error).message}`, 500)
+8
View File
@@ -246,6 +246,8 @@ export function updateAddressUpdatedAt(
if (!address) { if (!address) {
return; return;
} }
// Skip activity timestamp writes when activity tracking is disabled.
if (getBooleanValue(c.env.DISABLE_ADDRESS_UPDATED_AT)) return;
// update address updated_at asynchronously // update address updated_at asynchronously
c.executionCtx.waitUntil((async () => { c.executionCtx.waitUntil((async () => {
try { try {
@@ -268,6 +270,8 @@ export function updateUserAddressesUpdatedAt(
if (!userId) { if (!userId) {
return; return;
} }
// Apply the same activity tracking switch to bulk updates.
if (getBooleanValue(c.env.DISABLE_ADDRESS_UPDATED_AT)) return;
c.executionCtx.waitUntil((async () => { c.executionCtx.waitUntil((async () => {
try { try {
await c.env.DB.prepare( await c.env.DB.prepare(
@@ -494,6 +498,10 @@ export const cleanup = async (
cleanType: string | undefined | null, cleanType: string | undefined | null,
cleanDays: number | undefined | null cleanDays: number | undefined | null
): Promise<boolean> => { ): Promise<boolean> => {
// Frozen activity timestamps cannot reliably identify inactive addresses.
if (cleanType === "inactiveAddress" && getBooleanValue(c.env.DISABLE_ADDRESS_UPDATED_AT)) {
return false;
}
const msgs = i18n.getMessagesbyContext(c); const msgs = i18n.getMessagesbyContext(c);
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) { if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
throw new Error(msgs.InvalidCleanupConfigMsg) throw new Error(msgs.InvalidCleanupConfigMsg)
+1
View File
@@ -88,6 +88,7 @@ const messages: LocaleMessages = {
EnableSendMailForDomainMsg: "Please enable SEND_MAIL for this domain first", EnableSendMailForDomainMsg: "Please enable SEND_MAIL for this domain first",
InvalidCleanupConfigMsg: "Invalid cleanType or cleanDays", InvalidCleanupConfigMsg: "Invalid cleanType or cleanDays",
InvalidCleanTypeMsg: "Invalid cleanType", InvalidCleanTypeMsg: "Invalid cleanType",
InactiveAddressCleanupDisabledMsg: "Address activity updates are disabled; cleanup by inactivity is not allowed",
EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify", EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify",
VerifyMailDomainInvalidMsg: "VerifyMailSender domain must be in", VerifyMailDomainInvalidMsg: "VerifyMailSender domain must be in",
InvalidMaxAddressCountMsg: "Invalid maxAddressCount", InvalidMaxAddressCountMsg: "Invalid maxAddressCount",
+1
View File
@@ -86,6 +86,7 @@ export type LocaleMessages = {
EnableSendMailForDomainMsg: string EnableSendMailForDomainMsg: string
InvalidCleanupConfigMsg: string InvalidCleanupConfigMsg: string
InvalidCleanTypeMsg: string InvalidCleanTypeMsg: string
InactiveAddressCleanupDisabledMsg: string
EnableKVForMailVerifyMsg: string EnableKVForMailVerifyMsg: string
VerifyMailDomainInvalidMsg: string VerifyMailDomainInvalidMsg: string
InvalidMaxAddressCountMsg: string InvalidMaxAddressCountMsg: string
+1
View File
@@ -88,6 +88,7 @@ const messages: LocaleMessages = {
EnableSendMailForDomainMsg: "请先为此域名启用 SEND_MAIL", EnableSendMailForDomainMsg: "请先为此域名启用 SEND_MAIL",
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays", InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
InvalidCleanTypeMsg: "无效的 cleanType", InvalidCleanTypeMsg: "无效的 cleanType",
InactiveAddressCleanupDisabledMsg: "已禁用地址活跃时间更新,不能按不活跃时间清理地址",
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV", EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在", VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在",
InvalidMaxAddressCountMsg: "无效的 maxAddressCount", InvalidMaxAddressCountMsg: "无效的 maxAddressCount",
+1
View File
@@ -42,6 +42,7 @@ type Bindings = {
RANDOM_SUBDOMAIN_DOMAINS: string | string[] | undefined RANDOM_SUBDOMAIN_DOMAINS: string | string[] | undefined
RANDOM_SUBDOMAIN_LENGTH: string | number | undefined RANDOM_SUBDOMAIN_LENGTH: string | number | undefined
DISABLE_CUSTOM_ADDRESS_NAME: string | boolean | undefined DISABLE_CUSTOM_ADDRESS_NAME: string | boolean | undefined
DISABLE_ADDRESS_UPDATED_AT: string | boolean | undefined
CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST: string | boolean | undefined CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST: string | boolean | undefined
ADMIN_USER_ROLE: string | undefined ADMIN_USER_ROLE: string | undefined
USER_DEFAULT_ROLE: string | UserRole | undefined USER_DEFAULT_ROLE: string | UserRole | undefined
+2
View File
@@ -41,6 +41,8 @@ PREFIX = "tmp"
# MAX_ADDRESS_LEN = 30 # MAX_ADDRESS_LEN = 30
# Disable custom email address name, if set true, users cannot input custom email name, will auto generate # Disable custom email address name, if set true, users cannot input custom email name, will auto generate
# DISABLE_CUSTOM_ADDRESS_NAME = true # DISABLE_CUSTOM_ADDRESS_NAME = true
# Disable address activity keep-alive updates and built-in manual/scheduled cleanup by inactivity. Defaults to false.
# DISABLE_ADDRESS_UPDATED_AT = true
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES # IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES
# PASSWORDS = ["123", "456"] # PASSWORDS = ["123", "456"]
# For admin panel # For admin panel