mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-15 04:16:30 +08:00
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:
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"]` |
|
||||||
|
|||||||
@@ -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"]` |
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user