From 0a0a56e6531706c4c0bd096cde3abc795a934179 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Mon, 7 Sep 2026 00:32:52 +0800 Subject: [PATCH] fix: clarify cleanup failures and test address activity controls (#1139) * fix: use general cleanup failure messages * fix: limit cleanup message change to false results * test: cover address activity controls in existing e2e workers * test: keep the default e2e worker entry unchanged * test: extend existing address activity e2e coverage * test: prepare historical timestamps through shared D1 files * test: assert exact cleanup validation errors * test: exclude D1 metadata when locating the test database * test: exercise address activity through real API flows * test: prepare activity fixtures with admin APIs --- e2e/Dockerfile.worker | 2 +- e2e/docker-compose.yml | 2 +- e2e/fixtures/wrangler.toml.e2e.env-off | 1 + .../api/address-activity-throttle.spec.ts | 316 +++++++++++++++++- vitepress-docs/docs/en/guide/worker-vars.md | 2 +- vitepress-docs/docs/zh/guide/worker-vars.md | 2 +- worker/src/admin_api/cleanup_api.ts | 3 +- worker/src/admin_api/e2e_test_api.ts | 14 +- worker/src/i18n/en.ts | 2 +- worker/src/i18n/type.ts | 2 +- worker/src/i18n/zh.ts | 2 +- 11 files changed, 335 insertions(+), 13 deletions(-) diff --git a/e2e/Dockerfile.worker b/e2e/Dockerfile.worker index d3177b5c..b3b81d16 100644 --- a/e2e/Dockerfile.worker +++ b/e2e/Dockerfile.worker @@ -16,4 +16,4 @@ COPY ${WRANGLER_TOML} wrangler.toml EXPOSE 8787 -CMD ["pnpm", "exec", "wrangler", "dev", "--port", "8787", "--ip", "0.0.0.0"] +CMD ["pnpm", "exec", "wrangler", "dev", "--port", "8787", "--ip", "0.0.0.0", "--test-scheduled"] diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index b801eb15..61dd2cac 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -42,7 +42,7 @@ services: dockerfile: e2e/Dockerfile.worker ports: - "8790:8790" - command: ["pnpm", "exec", "wrangler", "dev", "--port", "8790", "--ip", "0.0.0.0"] + command: ["pnpm", "exec", "wrangler", "dev", "--port", "8790", "--ip", "0.0.0.0", "--test-scheduled"] volumes: - ./fixtures/wrangler.toml.e2e.env-off:/app/worker/wrangler.toml:ro depends_on: diff --git a/e2e/fixtures/wrangler.toml.e2e.env-off b/e2e/fixtures/wrangler.toml.e2e.env-off index d84d3966..17d901d6 100644 --- a/e2e/fixtures/wrangler.toml.e2e.env-off +++ b/e2e/fixtures/wrangler.toml.e2e.env-off @@ -13,6 +13,7 @@ JWT_SECRET = "e2e-test-secret-key-env-off" BLACK_LIST = "" ENABLE_USER_CREATE_EMAIL = true ENABLE_USER_DELETE_EMAIL = false +DISABLE_ADDRESS_UPDATED_AT = true ENABLE_REDEEM_CODE = false ENABLE_AUTO_REPLY = true DEFAULT_SEND_BALANCE = 10 diff --git a/e2e/tests/api/address-activity-throttle.spec.ts b/e2e/tests/api/address-activity-throttle.spec.ts index 2e7e4c69..a3a29b56 100644 --- a/e2e/tests/api/address-activity-throttle.spec.ts +++ b/e2e/tests/api/address-activity-throttle.spec.ts @@ -1,6 +1,8 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type APIRequestContext } from '@playwright/test'; import { WORKER_URL, + WORKER_URL_ENV_OFF, + onMailpitMessage, createTestAddress, deleteAddress, hashPassword, @@ -77,3 +79,315 @@ test.describe('Address activity throttling', () => { } }); }); + +const OLD = '2020-01-01 00:00:00'; +const RECENT = new Date(Date.now() - 3_600_000).toISOString().replace('T', ' ').slice(0, 19); +type Mailbox = { address: string; address_id: number; jwt: string; password: string }; +type User = { id: number; email: string; jwt: string }; + +for (const { base, disabled } of [ + { base: WORKER_URL, disabled: false }, + { base: WORKER_URL_ENV_OFF, disabled: true }, +]) { + test.describe(`Address activity disabled: ${disabled}`, () => { + let mailboxes: Mailbox[]; + let users: User[]; + let orphanAddresses: string[]; + let originalUserSettings: Record; + + async function call(request: APIRequestContext, path: string, + options: Parameters[1] = {}, status = 200) { + const response = await request.fetch(`${base}${path}`, options); + expect(response.status(), await response.text()).toBe(status); + return response; + } + async function list(request: APIRequestContext, path: string, query: Record) { + return (await call(request, path, { params: { limit: '100', offset: '0', ...query } })).json(); + } + async function addressRow(request: APIRequestContext, mailbox: Mailbox) { + return (await list(request, '/admin/address', { query: mailbox.address })).results + .find((row: { name: string }) => row.name === mailbox.address); + } + async function newMailbox(request: APIRequestContext) { + const response = await call(request, '/admin/new_address', { + method: 'POST', data: { name: `activity${Date.now()}${mailboxes.length}`, domain: 'test.example.com' }, + }); + const mailbox: Mailbox = await response.json(); + mailboxes.push(mailbox); + return mailbox; + } + async function newUser(request: APIRequestContext) { + const email = `activity${Date.now()}${users.length}@test.example.com`; + const password = hashPassword('test-password-123'); + await call(request, '/admin/users', { method: 'POST', data: { email, password } }); + const response = await call(request, '/user_api/login', { method: 'POST', data: { email, password } }); + const { jwt } = await response.json(); + const { user_id: id } = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8')); + const user = { id, email, jwt }; + users.push(user); + return user; + } + const addressAuth = (mailbox: Mailbox) => ({ Authorization: `Bearer ${mailbox.jwt}` }); + const userAuth = (user: User) => ({ 'x-user-token': user.jwt }); + async function bind(request: APIRequestContext, mailbox: Mailbox, user: User) { + await call(request, '/admin/users/bind_address', { + method: 'POST', data: { user_id: user.id, address_id: mailbox.address_id }, + }); + } + async function seed(request: APIRequestContext, mailbox: Mailbox, updatedAt: string | null = OLD, createdAt?: string) { + await call(request, '/admin/test/seed_mail', { + method: 'POST', data: { + address: mailbox.address, raw: `From: sender@test.example.com\r\nTo: ${mailbox.address}\r\nSubject: activity\r\n\r\nBody`, + address_updated_at: updatedAt, address_created_at: createdAt, created_at: OLD, + }, + }); + expect((await addressRow(request, mailbox)).updated_at).toBe(updatedAt); + } + async function expectActivity(request: APIRequestContext, mailbox: Mailbox, previous: string | null) { + if (disabled || previous === RECENT) { + await waitForNextTimestamp(); + expect((await addressRow(request, mailbox)).updated_at).toBe(previous); + return; + } + await expect.poll(async () => (await addressRow(request, mailbox)).updated_at).not.toBe(previous); + expect((await addressRow(request, mailbox)).updated_at).toBeTruthy(); + } + async function send(request: APIRequestContext, mailbox: Mailbox, user?: User) { + const subject = `Activity ${disabled} ${Date.now()}`; + const listener = onMailpitMessage(mail => mail.Subject === subject); + await listener.ready; + const [, delivered] = await Promise.all([ + call(request, user ? `/user_api/address/${mailbox.address_id}/send_mail` : '/api/send_mail', { + method: 'POST', headers: user ? userAuth(user) : addressAuth(mailbox), + data: { to_mail: 'recipient@test.example.com', subject, content: 'Activity test', is_html: false }, + }), + listener.message, + ]); + expect(delivered.From.Address).toBe(mailbox.address); + } + async function relatedCounts(request: APIRequestContext, mailbox: Mailbox, user: User) { + const address = await addressRow(request, mailbox); + const inbox = await list(request, '/admin/mails', { address: mailbox.address }); + const sent = await list(request, '/admin/sendbox', { address: mailbox.address }); + const sender = await list(request, '/admin/address_sender', { address: mailbox.address }); + const reply = await (await call(request, '/api/auto_reply', { headers: addressAuth(mailbox) })).json(); + const bound = await (await call(request, '/user_api/bind_address', { headers: userAuth(user) })).json(); + return [Number(!!address), inbox.count, sent.count, sender.count, Number(!!reply.subject), + bound.results.filter((row: { name: string }) => row.name === mailbox.address).length]; + } + async function prepareRelated(request: APIRequestContext) { + const mailbox = await newMailbox(request); + const user = await newUser(request); + await bind(request, mailbox, user); + await call(request, '/api/auto_reply', { + method: 'POST', headers: addressAuth(mailbox), + data: { auto_reply: { subject: 'Reply', message: 'Body', enabled: false } }, + }); + await send(request, mailbox); + await seed(request, mailbox); + expect(await relatedCounts(request, mailbox, user)).toEqual([1, 1, 1, 1, 1, 1]); + return { mailbox, user }; + } + + test.beforeAll(async ({ request }) => { + expect(base).toBeTruthy(); + originalUserSettings = await (await call(request, '/admin/user_settings')).json(); + await call(request, '/admin/user_settings', { + method: 'POST', data: { ...originalUserSettings, enable: true, enableMailVerify: false }, + }); + }); + test.afterAll(async ({ request }) => { + await call(request, '/admin/user_settings', { method: 'POST', data: originalUserSettings }); + }); + test.beforeEach(() => { + expect(base).toBeTruthy(); + mailboxes = []; + users = []; + orphanAddresses = []; + }); + test.afterEach(async ({ request }) => { + for (const mailbox of mailboxes) { + const row = await addressRow(request, mailbox); + if (row) await call(request, `/admin/delete_address/${row.id}`, { method: 'DELETE' }); + } + for (const user of users) await call(request, `/admin/users/${user.id}`, { method: 'DELETE' }); + for (const address of orphanAddresses) { + const mails = await list(request, '/admin/mails', { address }); + for (const mail of mails.results) await call(request, `/admin/mails/${mail.id}`, { method: 'DELETE' }); + } + }); + + test('repeated user settings reads respect activity tracking and ownership', async ({ request }) => { + const user = await newUser(request); + const otherUser = await newUser(request); + for (const [index, previous] of [OLD, null, RECENT, OLD, OLD].entries()) { + const mailbox = await newMailbox(request); + if (index < 3) await bind(request, mailbox, user); + if (index === 4) await bind(request, mailbox, otherUser); + await seed(request, mailbox, previous); + } + await call(request, '/user_api/settings', { headers: userAuth(user) }); + await expectActivity(request, mailboxes[0], OLD); + const after = await Promise.all(mailboxes.map(mailbox => addressRow(request, mailbox))); + expect(after.map(row => row.updated_at).slice(2)).toEqual([RECENT, OLD, OLD]); + if (disabled) expect(after[1].updated_at).toBeNull(); + else expect(after[1].updated_at).toBeTruthy(); + await call(request, '/user_api/settings', { headers: userAuth(user) }); + await waitForNextTimestamp(); + expect(await Promise.all(mailboxes.map(mailbox => addressRow(request, mailbox)))).toEqual(after); + }); + + for (const path of ['/api/settings', '/api/mails?limit=20&offset=0', '/api/parsed_mails?limit=20&offset=0']) { + test(`${path} respects the switch for stale, null and recent activity`, async ({ request }) => { + const mailbox = await newMailbox(request); + const other = await newMailbox(request); + await seed(request, other); + for (const previous of [OLD, null, RECENT]) { + await seed(request, mailbox, previous); + await call(request, path, { headers: addressAuth(mailbox) }); + await expectActivity(request, mailbox, previous); + expect((await addressRow(request, other)).updated_at).toBe(OLD); + } + }); + } + + test('later inbox pages do not refresh activity', async ({ request }) => { + const mailbox = await newMailbox(request); + await seed(request, mailbox); + for (const path of ['/api/mails?limit=20&offset=20', '/api/parsed_mails?limit=20&offset=20']) { + await call(request, path, { headers: addressAuth(mailbox) }); + } + await waitForNextTimestamp(); + expect((await addressRow(request, mailbox)).updated_at).toBe(OLD); + }); + + test('incoming mail is stored and both send APIs respect activity tracking', async ({ request }) => { + const mailbox = await newMailbox(request); + const user = await newUser(request); + await bind(request, mailbox, user); + await seed(request, mailbox); + const raw = `From: sender@test.example.com\r\nTo: ${mailbox.address}\r\nSubject: incoming\r\n\r\nBody`; + const received = await call(request, '/admin/test/receive_mail', { + method: 'POST', data: { from: 'sender@test.example.com', to: mailbox.address, raw }, + }); + expect((await received.json()).success).toBe(true); + expect((await list(request, '/admin/mails', { address: mailbox.address })).count).toBe(2); + expect((await addressRow(request, mailbox)).updated_at).toBe(OLD); + for (const sender of [undefined, user]) { + await seed(request, mailbox); + await send(request, mailbox, sender); + await expectActivity(request, mailbox, OLD); + } + const sender = await list(request, '/admin/address_sender', { address: mailbox.address }); + expect(sender.results[0].balance).toBe(8); + expect((await list(request, '/admin/sendbox', { address: mailbox.address })).count).toBe(2); + }); + + test('password generation, password changes and admin resets keep working', async ({ request }) => { + const mailbox = await newMailbox(request); + expect(mailbox.password).toBeTruthy(); + expect((await addressRow(request, mailbox)).updated_at).toBeTruthy(); + await call(request, '/api/address_login', { + method: 'POST', data: { email: mailbox.address, password: hashPassword(mailbox.password) }, + }); + for (const path of ['/api/address_change_password', `/admin/address/${mailbox.address_id}/reset_password`]) { + await seed(request, mailbox); + const password = hashPassword(`password-${path}`); + await call(request, path, { + method: 'POST', headers: addressAuth(mailbox), data: { password, new_password: password }, + }); + expect((await addressRow(request, mailbox)).updated_at).not.toBe(OLD); + await call(request, '/api/address_login', { method: 'POST', data: { email: mailbox.address, password } }); + } + }); + + test('address transfer still initializes its timestamp and changes ownership', async ({ request }) => { + const mailbox = await newMailbox(request); + const user = await newUser(request); + const target = await newUser(request); + await bind(request, mailbox, user); + await seed(request, mailbox); + await call(request, '/user_api/transfer_address', { + method: 'POST', headers: userAuth(user), data: { address_id: mailbox.address_id, target_user_email: target.email }, + }); + const transferred = await addressRow(request, mailbox); + expect(transferred.id).not.toBe(mailbox.address_id); + expect(transferred.updated_at).toBeTruthy(); + expect(transferred.updated_at).not.toBe(OLD); + const bindings = await (await call(request, '/user_api/bind_address', { headers: userAuth(target) })).json(); + expect(bindings.results.map((row: { name: string }) => row.name)).toContain(mailbox.address); + }); + + test('manual inactivity cleanup protects the address and all related data when disabled', async ({ request }) => { + const { mailbox, user } = await prepareRelated(request); + for (const lang of ['en', 'zh']) { + const response = await call(request, '/admin/cleanup', { + method: 'POST', headers: { 'x-lang': lang }, data: { cleanType: 'inactiveAddress', cleanDays: 1 }, + }, disabled ? 500 : 200); + if (disabled) expect(await response.text()).toBe(lang === 'zh' + ? '清理失败,请检查清理配置;禁用地址活跃时间更新时,无法按不活跃时间清理。' + : 'Cleanup failed. Check your cleanup settings; inactive-address cleanup is unavailable when address activity updates are disabled.'); + else expect(await response.json()).toEqual({ success: true }); + expect(await relatedCounts(request, mailbox, user)).toEqual(disabled ? [1, 1, 1, 1, 1, 1] : [0, 0, 0, 0, 0, 0]); + } + }); + + test('scheduled inactivity cleanup respects the switch and continues later cleanup', async ({ request }) => { + const { mailbox, user } = await prepareRelated(request); + const created = await newMailbox(request); + await seed(request, created, RECENT, OLD); + const original = await (await call(request, '/admin/auto_cleanup')).json(); + try { + await call(request, '/admin/auto_cleanup', { method: 'POST', data: { + enableInactiveAddressAutoCleanup: true, cleanInactiveAddressDays: 1, + enableAddressAutoCleanup: true, cleanAddressDays: 1, + } }); + await call(request, '/__scheduled'); + expect(await relatedCounts(request, mailbox, user)).toEqual(disabled ? [1, 1, 1, 1, 1, 1] : [0, 0, 0, 0, 0, 0]); + expect(await addressRow(request, created)).toBeUndefined(); + } finally { + await call(request, '/admin/auto_cleanup', { method: 'POST', data: original || {} }); + } + }); + + for (const cleanType of ['addressCreated', 'unboundAddress', 'emptyAddress', 'mails', 'mails_unknow', 'sendbox']) { + test(`${cleanType} cleanup remains available`, async ({ request }) => { + const mailbox = await newMailbox(request); + await seed(request, mailbox, RECENT, OLD); + let queryAddress = mailbox.address; + if (cleanType === 'emptyAddress') await call(request, `/admin/clear_inbox/${mailbox.address_id}`, { method: 'DELETE' }); + if (cleanType === 'mails_unknow') { + queryAddress = `unknown${Date.now()}@test.example.com`; + orphanAddresses.push(queryAddress); + await call(request, '/admin/test/seed_mail', { + method: 'POST', data: { address: queryAddress, raw: 'old unknown mail', created_at: OLD }, + }); + } + if (cleanType === 'sendbox') { + await send(request, mailbox); + await waitForNextTimestamp(); + } + await call(request, '/admin/cleanup', { + method: 'POST', data: { cleanType, cleanDays: cleanType === 'sendbox' ? 0 : 1 }, + }); + if (['addressCreated', 'unboundAddress', 'emptyAddress'].includes(cleanType)) { + expect(await addressRow(request, mailbox)).toBeUndefined(); + } else { + const path = cleanType === 'sendbox' ? '/admin/sendbox' : '/admin/mails'; + expect((await list(request, path, { address: queryAddress })).count).toBe(0); + expect(await addressRow(request, mailbox)).toBeTruthy(); + } + }); + } + + test('invalid cleanup requests keep their original error details', async ({ request }) => { + for (const { data, message } of [ + { data: { cleanType: 'invalid', cleanDays: 1 }, message: 'Operation failed: Invalid cleanType' }, + { data: { cleanType: 'mails', cleanDays: -1 }, message: 'Operation failed: Invalid cleanType or cleanDays' }, + ]) { + const response = await call(request, '/admin/cleanup', { method: 'POST', data }, 500); + expect(await response.text()).toBe(message); + } + }); + }); +} diff --git a/vitepress-docs/docs/en/guide/worker-vars.md b/vitepress-docs/docs/en/guide/worker-vars.md index de32a894..d91fbdb7 100644 --- a/vitepress-docs/docs/en/guide/worker-vars.md +++ b/vitepress-docs/docs/en/guide/worker-vars.md @@ -38,7 +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` | | `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_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` | +| `DISABLE_ADDRESS_UPDATED_AT` | Text/JSON | Defaults to `false`. Set to `true` to stop individual and user-wide address activity keep-alive updates and disable built-in manual and scheduled inactive-address cleanup. Initial address timestamps, password operations, and other cleanup rules remain unchanged | `true` | | `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]` | | `DEFAULT_DOMAINS` | JSON | Default domains available to users (not logged in or users without assigned roles) | `["awsl.uk", "dreamhunter2333.xyz"]` | diff --git a/vitepress-docs/docs/zh/guide/worker-vars.md b/vitepress-docs/docs/zh/guide/worker-vars.md index f6c5dd0e..a3398229 100644 --- a/vitepress-docs/docs/zh/guide/worker-vars.md +++ b/vitepress-docs/docs/zh/guide/worker-vars.md @@ -38,7 +38,7 @@ | `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` | | `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` | | `DISABLE_CUSTOM_ADDRESS_NAME` | 文本/JSON | 禁用自定义邮箱地址名称,如果设置为 true,则用户无法输入自定义邮箱名称,将由后台自动生成 | `true` | -| `DISABLE_ADDRESS_UPDATED_AT` | 文本/JSON | 默认 `false`。设为 `true` 时跳过用户设置、邮箱访问、发信等触发的单地址及批量活跃时间更新。创建地址的初始时间、生成/修改/重置密码及其 `updated_at` 更新不变。手动 `/admin/cleanup` 的 `cleanType=inactiveAddress` 返回 `403`,定时任务跳过该项并继续其他清理;按创建时间清理、其他清理类型及管理员自定义 SQL 不受影响。改回 `false` 恢复原有行为,不补齐禁用期间的活跃记录 | `true` | +| `DISABLE_ADDRESS_UPDATED_AT` | 文本/JSON | 默认 `false`。设为 `true` 时停止单地址及用户批量的主动保活刷新,并禁用内置手动和定时不活跃地址清理。地址初始时间、密码操作及其他清理规则不变 | `true` | | `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` | | `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` | | `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` | diff --git a/worker/src/admin_api/cleanup_api.ts b/worker/src/admin_api/cleanup_api.ts index 1dbbcf74..aa92a5d2 100644 --- a/worker/src/admin_api/cleanup_api.ts +++ b/worker/src/admin_api/cleanup_api.ts @@ -99,9 +99,8 @@ export default { const { cleanType, cleanDays } = await c.req.json(); try { const success = await cleanup(c, cleanType, cleanDays); - // Report disabled cleanup as forbidden rather than successful. if (!success) { - return c.text(msgs.InactiveAddressCleanupDisabledMsg, 403); + return c.text(msgs.CleanupFailedMsg, 500); } } catch (error) { console.error(error); diff --git a/worker/src/admin_api/e2e_test_api.ts b/worker/src/admin_api/e2e_test_api.ts index 36ca7e9d..403c8ba6 100644 --- a/worker/src/admin_api/e2e_test_api.ts +++ b/worker/src/admin_api/e2e_test_api.ts @@ -6,7 +6,7 @@ const seedMail = async (c: Context) => { if (!getBooleanValue(c.env.E2E_TEST_MODE)) { return c.text("Not available", 404); } - const { address, source, raw, message_id } = await c.req.json(); + const { address, source, raw, message_id, created_at, address_updated_at, address_created_at } = await c.req.json(); if (!address || !raw) { return c.text("address and raw are required", 400); } @@ -16,11 +16,19 @@ const seedMail = async (c: Context) => { if (message_id && message_id.length > 255) { return c.text("message_id too long", 400); } + if (address_updated_at !== undefined) { + await c.env.DB.prepare(`UPDATE address SET updated_at = ? WHERE name = ?`) + .bind(address_updated_at, address).run(); + } + if (address_created_at !== undefined) { + await c.env.DB.prepare(`UPDATE address SET created_at = ? WHERE name = ?`) + .bind(address_created_at, address).run(); + } const msgId = message_id || ``; const { success } = await c.env.DB.prepare( `INSERT INTO raw_mails (message_id, source, address, raw, created_at)` - + ` VALUES (?, ?, ?, ?, datetime('now'))` - ).bind(msgId, source || address, address, raw).run(); + + ` VALUES (?, ?, ?, ?, COALESCE(?, datetime('now')))` + ).bind(msgId, source || address, address, raw, created_at ?? null).run(); return c.json({ success }); }; diff --git a/worker/src/i18n/en.ts b/worker/src/i18n/en.ts index 08395b47..941cc929 100644 --- a/worker/src/i18n/en.ts +++ b/worker/src/i18n/en.ts @@ -88,7 +88,7 @@ const messages: LocaleMessages = { EnableSendMailForDomainMsg: "Please enable SEND_MAIL for this domain first", InvalidCleanupConfigMsg: "Invalid cleanType or cleanDays", InvalidCleanTypeMsg: "Invalid cleanType", - InactiveAddressCleanupDisabledMsg: "Address activity updates are disabled; cleanup by inactivity is not allowed", + CleanupFailedMsg: "Cleanup failed. Check your cleanup settings; inactive-address cleanup is unavailable when address activity updates are disabled.", EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify", VerifyMailDomainInvalidMsg: "VerifyMailSender domain must be in", InvalidMaxAddressCountMsg: "Invalid maxAddressCount", diff --git a/worker/src/i18n/type.ts b/worker/src/i18n/type.ts index aafb1a77..de3bb92b 100644 --- a/worker/src/i18n/type.ts +++ b/worker/src/i18n/type.ts @@ -86,7 +86,7 @@ export type LocaleMessages = { EnableSendMailForDomainMsg: string InvalidCleanupConfigMsg: string InvalidCleanTypeMsg: string - InactiveAddressCleanupDisabledMsg: string + CleanupFailedMsg: string EnableKVForMailVerifyMsg: string VerifyMailDomainInvalidMsg: string InvalidMaxAddressCountMsg: string diff --git a/worker/src/i18n/zh.ts b/worker/src/i18n/zh.ts index b55582c9..06ac4960 100644 --- a/worker/src/i18n/zh.ts +++ b/worker/src/i18n/zh.ts @@ -88,7 +88,7 @@ const messages: LocaleMessages = { EnableSendMailForDomainMsg: "请先为此域名启用 SEND_MAIL", InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays", InvalidCleanTypeMsg: "无效的 cleanType", - InactiveAddressCleanupDisabledMsg: "已禁用地址活跃时间更新,不能按不活跃时间清理地址", + CleanupFailedMsg: "清理失败,请检查清理配置;禁用地址活跃时间更新时,无法按不活跃时间清理。", EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV", VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在", InvalidMaxAddressCountMsg: "无效的 maxAddressCount",