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
This commit is contained in:
Dream Hunter
2026-09-07 00:32:52 +08:00
committed by GitHub
parent 4ddd502a60
commit 0a0a56e653
11 changed files with 335 additions and 13 deletions
+1 -1
View File
@@ -16,4 +16,4 @@ COPY ${WRANGLER_TOML} wrangler.toml
EXPOSE 8787 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"]
+1 -1
View File
@@ -42,7 +42,7 @@ services:
dockerfile: e2e/Dockerfile.worker dockerfile: e2e/Dockerfile.worker
ports: ports:
- "8790:8790" - "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: volumes:
- ./fixtures/wrangler.toml.e2e.env-off:/app/worker/wrangler.toml:ro - ./fixtures/wrangler.toml.e2e.env-off:/app/worker/wrangler.toml:ro
depends_on: depends_on:
+1
View File
@@ -13,6 +13,7 @@ JWT_SECRET = "e2e-test-secret-key-env-off"
BLACK_LIST = "" BLACK_LIST = ""
ENABLE_USER_CREATE_EMAIL = true ENABLE_USER_CREATE_EMAIL = true
ENABLE_USER_DELETE_EMAIL = false ENABLE_USER_DELETE_EMAIL = false
DISABLE_ADDRESS_UPDATED_AT = true
ENABLE_REDEEM_CODE = false ENABLE_REDEEM_CODE = false
ENABLE_AUTO_REPLY = true ENABLE_AUTO_REPLY = true
DEFAULT_SEND_BALANCE = 10 DEFAULT_SEND_BALANCE = 10
+315 -1
View File
@@ -1,6 +1,8 @@
import { test, expect } from '@playwright/test'; import { test, expect, type APIRequestContext } from '@playwright/test';
import { import {
WORKER_URL, WORKER_URL,
WORKER_URL_ENV_OFF,
onMailpitMessage,
createTestAddress, createTestAddress,
deleteAddress, deleteAddress,
hashPassword, 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<string, unknown>;
async function call(request: APIRequestContext, path: string,
options: Parameters<APIRequestContext['fetch']>[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<string, string>) {
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);
}
});
});
}
+1 -1
View File
@@ -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` | | `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` | | `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_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"]` |
+1 -1
View File
@@ -38,7 +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` | | `DISABLE_ADDRESS_UPDATED_AT` | 文本/JSON | 默认 `false`。设为 `true`停止单地址及用户批量的主动保活刷新,并禁用内置手动和定时不活跃地址清理。地址初始时间、密码操作及其他清理规则不变 | `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"]` |
+1 -2
View File
@@ -99,9 +99,8 @@ export default {
const { cleanType, cleanDays } = await c.req.json(); const { cleanType, cleanDays } = await c.req.json();
try { try {
const success = await cleanup(c, cleanType, cleanDays); const success = await cleanup(c, cleanType, cleanDays);
// Report disabled cleanup as forbidden rather than successful.
if (!success) { if (!success) {
return c.text(msgs.InactiveAddressCleanupDisabledMsg, 403); return c.text(msgs.CleanupFailedMsg, 500);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
+11 -3
View File
@@ -6,7 +6,7 @@ const seedMail = async (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.E2E_TEST_MODE)) { if (!getBooleanValue(c.env.E2E_TEST_MODE)) {
return c.text("Not available", 404); 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) { if (!address || !raw) {
return c.text("address and raw are required", 400); return c.text("address and raw are required", 400);
} }
@@ -16,11 +16,19 @@ const seedMail = async (c: Context<HonoCustomType>) => {
if (message_id && message_id.length > 255) { if (message_id && message_id.length > 255) {
return c.text("message_id too long", 400); 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 || `<e2e-${Date.now()}@test>`; const msgId = message_id || `<e2e-${Date.now()}@test>`;
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`INSERT INTO raw_mails (message_id, source, address, raw, created_at)` `INSERT INTO raw_mails (message_id, source, address, raw, created_at)`
+ ` VALUES (?, ?, ?, ?, datetime('now'))` + ` VALUES (?, ?, ?, ?, COALESCE(?, datetime('now')))`
).bind(msgId, source || address, address, raw).run(); ).bind(msgId, source || address, address, raw, created_at ?? null).run();
return c.json({ success }); return c.json({ success });
}; };
+1 -1
View File
@@ -88,7 +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", 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", 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 -1
View File
@@ -86,7 +86,7 @@ export type LocaleMessages = {
EnableSendMailForDomainMsg: string EnableSendMailForDomainMsg: string
InvalidCleanupConfigMsg: string InvalidCleanupConfigMsg: string
InvalidCleanTypeMsg: string InvalidCleanTypeMsg: string
InactiveAddressCleanupDisabledMsg: string CleanupFailedMsg: string
EnableKVForMailVerifyMsg: string EnableKVForMailVerifyMsg: string
VerifyMailDomainInvalidMsg: string VerifyMailDomainInvalidMsg: string
InvalidMaxAddressCountMsg: string InvalidMaxAddressCountMsg: string
+1 -1
View File
@@ -88,7 +88,7 @@ const messages: LocaleMessages = {
EnableSendMailForDomainMsg: "请先为此域名启用 SEND_MAIL", EnableSendMailForDomainMsg: "请先为此域名启用 SEND_MAIL",
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays", InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
InvalidCleanTypeMsg: "无效的 cleanType", InvalidCleanTypeMsg: "无效的 cleanType",
InactiveAddressCleanupDisabledMsg: "禁用地址活跃时间更新,不能按不活跃时间清理地址", CleanupFailedMsg: "清理失败,请检查清理配置;禁用地址活跃时间更新时,无法按不活跃时间清理",
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV", EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在", VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在",
InvalidMaxAddressCountMsg: "无效的 maxAddressCount", InvalidMaxAddressCountMsg: "无效的 maxAddressCount",