feat: add empty address cleanup feature (#765)

* feat: add empty address cleanup feature

Add functionality to clean up email addresses that have never received any emails and were created more than N days ago.

Changes:
- Add emptyAddress cleanup type to backend cleanup logic
- Add enableEmptyAddressAutoCleanup and cleanEmptyAddressDays to CleanupSettings model
- Add scheduled task support for auto-cleanup of empty addresses
- Add UI controls in Maintenance page for manual and auto cleanup
- Add i18n support (English and Chinese translations)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: update dependencies

Update package.json and lock files across frontend, worker, pages, and vitepress-docs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update CHANGELOG for empty address cleanup feature

Add entry for new maintenance page feature to clean up email addresses with no emails older than N days

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2025-11-13 17:57:35 +08:00
committed by GitHub
co-authored by Claude
parent 088bf3eefe
commit 113f9ad66b
12 changed files with 1237 additions and 1265 deletions
+2
View File
@@ -3,6 +3,8 @@
## v1.1.0(main) ## v1.1.0(main)
- feat: |Admin| 维护页面增加清理 n 天前空邮件的邮箱地址功能
## v1.0.7 ## v1.0.7
- feat: |Admin| 新增 IP 黑名单功能,用于限制访问频率较高的 API - feat: |Admin| 新增 IP 黑名单功能,用于限制访问频率较高的 API
+1 -1
View File
@@ -49,7 +49,7 @@
"vite-plugin-wasm": "^3.5.0", "vite-plugin-wasm": "^3.5.0",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"workbox-window": "^7.3.0", "workbox-window": "^7.3.0",
"wrangler": "^4.46.0" "wrangler": "^4.47.0"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+332 -346
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -19,6 +19,8 @@ const cleanupModel = ref({
cleanInactiveAddressDays: 30, cleanInactiveAddressDays: 30,
enableUnboundAddressAutoCleanup: false, enableUnboundAddressAutoCleanup: false,
cleanUnboundAddressDays: 30, cleanUnboundAddressDays: 30,
enableEmptyAddressAutoCleanup: false,
cleanEmptyAddressDays: 30,
}) })
const { t } = useI18n({ const { t } = useI18n({
@@ -31,6 +33,7 @@ const { t } = useI18n({
addressCreateLabel: "Cleanup the address created before n days", addressCreateLabel: "Cleanup the address created before n days",
inactiveAddressLabel: "Cleanup the inactive address before n days", inactiveAddressLabel: "Cleanup the inactive address before n days",
unboundAddressLabel: "Cleanup the unbound address before n days", unboundAddressLabel: "Cleanup the unbound address before n days",
emptyAddressLabel: "Cleanup the empty address before n days",
cleanupNow: "Cleanup now", cleanupNow: "Cleanup now",
autoCleanup: "Auto cleanup", autoCleanup: "Auto cleanup",
cleanupSuccess: "Cleanup success", cleanupSuccess: "Cleanup success",
@@ -45,6 +48,7 @@ const { t } = useI18n({
addressCreateLabel: "清理 n 天前创建的地址", addressCreateLabel: "清理 n 天前创建的地址",
inactiveAddressLabel: "清理 n 天前的未活跃地址", inactiveAddressLabel: "清理 n 天前的未活跃地址",
unboundAddressLabel: "清理 n 天前的未绑定用户地址", unboundAddressLabel: "清理 n 天前的未绑定用户地址",
emptyAddressLabel: "清理 n 天前空邮件的邮箱地址",
autoCleanup: "自动清理", autoCleanup: "自动清理",
cleanupSuccess: "清理成功", cleanupSuccess: "清理成功",
cleanupNow: "立即清理", cleanupNow: "立即清理",
@@ -177,6 +181,18 @@ onMounted(async () => {
{{ t('cleanupNow') }} {{ t('cleanupNow') }}
</n-button> </n-button>
</n-form-item-row> </n-form-item-row>
<n-form-item-row :label="t('emptyAddressLabel')">
<n-checkbox v-model:checked="cleanupModel.enableEmptyAddressAutoCleanup">
{{ t('autoCleanup') }}
</n-checkbox>
<n-input-number v-model:value="cleanupModel.cleanEmptyAddressDays" :placeholder="t('tip')" />
<n-button @click="cleanup('emptyAddress', cleanupModel.cleanEmptyAddressDays)">
<template #icon>
<n-icon :component="CleaningServicesFilled" />
</template>
{{ t('cleanupNow') }}
</n-button>
</n-form-item-row>
</n-form> </n-form>
</n-card> </n-card>
</div> </div>
+1 -1
View File
@@ -11,7 +11,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"wrangler": "^4.46.0" "wrangler": "^4.47.0"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+2 -2
View File
@@ -4,9 +4,9 @@
"version": "1.1.0", "version": "1.1.0",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.0", "@types/node": "^24.10.1",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"wrangler": "^4.46.0" "wrangler": "^4.47.0"
}, },
"scripts": { "scripts": {
"dev": "vitepress dev docs", "dev": "vitepress dev docs",
+357 -381
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -11,26 +11,26 @@
"build": "wrangler deploy --dry-run --outdir dist --minify" "build": "wrangler deploy --dry-run --outdir dist --minify"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20251107.0", "@cloudflare/workers-types": "^4.20251111.0",
"@eslint/js": "9.18.0", "@eslint/js": "9.18.0",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@types/node": "^22.19.0", "@types/node": "^22.19.1",
"eslint": "9.18.0", "eslint": "9.18.0",
"globals": "^15.15.0", "globals": "^15.15.0",
"typescript-eslint": "^8.46.3", "typescript-eslint": "^8.46.4",
"wrangler": "^4.46.0" "wrangler": "^4.47.0"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.888.0", "@aws-sdk/client-s3": "3.888.0",
"@aws-sdk/s3-request-presigner": "3.888.0", "@aws-sdk/s3-request-presigner": "3.888.0",
"@simplewebauthn/server": "10.0.1", "@simplewebauthn/server": "10.0.1",
"hono": "^4.10.4", "hono": "^4.10.5",
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",
"mimetext": "^3.0.27", "mimetext": "^3.0.27",
"postal-mime": "^2.6.0", "postal-mime": "^2.6.0",
"resend": "^4.8.0", "resend": "^4.8.0",
"telegraf": "4.16.3", "telegraf": "4.16.3",
"worker-mailer": "^1.1.5" "worker-mailer": "^1.2.0"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
+504 -528
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -273,6 +273,13 @@ export const cleanup = async (
DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')` DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')`
).run(); ).run();
break; break;
case "emptyAddress":
// Delete addresses that have no emails and were created more than N days ago
await batchDeleteAddressWithData(
c,
`name NOT IN (SELECT DISTINCT address FROM raw_mails WHERE address IS NOT NULL) AND created_at < datetime('now', '-${cleanDays} day')`
)
break;
default: default:
throw new Error("Invalid cleanType") throw new Error("Invalid cleanType")
} }
+2
View File
@@ -48,6 +48,8 @@ export type CleanupSettings = {
cleanInactiveAddressDays: number; cleanInactiveAddressDays: number;
enableUnboundAddressAutoCleanup: boolean | undefined; enableUnboundAddressAutoCleanup: boolean | undefined;
cleanUnboundAddressDays: number; cleanUnboundAddressDays: number;
enableEmptyAddressAutoCleanup: boolean | undefined;
cleanEmptyAddressDays: number;
} }
export class GeoData { export class GeoData {
+7
View File
@@ -57,4 +57,11 @@ export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any)
autoCleanupSetting.cleanUnboundAddressDays autoCleanupSetting.cleanUnboundAddressDays
); );
} }
if (autoCleanupSetting.enableEmptyAddressAutoCleanup) {
await cleanup(
{ env: env, } as Context<HonoCustomType>,
"emptyAddress",
autoCleanupSetting.cleanEmptyAddressDays
);
}
} }