import { test, expect, APIRequestContext } from '@playwright/test'; import { WORKER_URL, WORKER_URL_SEND_MAIL_DOMAIN, createTestAddress, deleteAddress, deleteAllMailpitMessages, requestSendAccess, onMailpitMessage, } from '../../fixtures/test-helpers'; const ADMIN_PASSWORD = 'e2e-admin-pass'; const ADMIN_HEADERS = { 'x-admin-auth': ADMIN_PASSWORD }; const DEFAULT_ACCOUNT_SETTINGS = { blockList: [], sendBlockList: [], verifiedAddressList: [], fromBlockList: [], noLimitSendAddressList: [], emailRuleSettings: {}, addressCreationSettings: {}, }; const DISABLED_LIMIT_CONFIG = { dailyEnabled: false, monthlyEnabled: false, dailyLimit: null as number | null, monthlyLimit: null as number | null, }; async function saveLimitConfig( request: APIRequestContext, sendMailLimitConfig: Record ) { return request.post(`${WORKER_URL}/admin/account_settings`, { headers: ADMIN_HEADERS, data: { ...DEFAULT_ACCOUNT_SETTINGS, sendMailLimitConfig }, }); } async function resetLimitConfig(request: APIRequestContext) { const res = await saveLimitConfig(request, DISABLED_LIMIT_CONFIG); expect(res.ok()).toBe(true); } async function sendOneMail( request: APIRequestContext, jwt: string, tag: string, opts: { expectDelivery?: boolean; lang?: string } = {} ) { const { expectDelivery = true, lang } = opts; const subject = `limit-${tag}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const headers: Record = { Authorization: `Bearer ${jwt}` }; if (lang) headers['x-lang'] = lang; let listener: ReturnType | undefined; if (expectDelivery) { listener = onMailpitMessage((m) => m.Subject === subject); await listener.ready; } const res = await request.post(`${WORKER_URL}/api/send_mail`, { headers, data: { from_name: 'Limit E2E', to_name: 'Recipient', to_mail: 'recipient@test.example.com', subject, content: `Limit test body ${tag}`, is_html: false, }, }); return { res, listener, subject }; } async function probeLimitBaseline( request: APIRequestContext, jwt: string, config: { dailyEnabled: boolean; monthlyEnabled: boolean; dailyLimit: number | null; monthlyLimit: number | null; }, subjectPrefix: string, maxProbeLimit: number = 50 ): Promise { for (let limit = 1; limit <= maxProbeLimit; limit++) { const save = await saveLimitConfig(request, { ...config, dailyLimit: config.dailyEnabled ? limit : null, monthlyLimit: config.monthlyEnabled ? limit : null, }); expect(save.ok()).toBe(true); const probe = await request.post(`${WORKER_URL}/api/send_mail`, { headers: { Authorization: `Bearer ${jwt}` }, data: { from_name: 'probe', to_name: '', to_mail: 'recipient@test.example.com', subject: `${subjectPrefix}-${limit}-${Date.now()}`, content: 'probe', is_html: false, }, }); if (probe.ok()) { return limit; } } throw new Error(`Failed to probe send mail limit baseline within ${maxProbeLimit}`); } async function probeDailyBaseline( request: APIRequestContext, jwt: string ): Promise { return probeLimitBaseline(request, jwt, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: 1, monthlyLimit: null, }, 'probe-daily'); } async function probeMonthlyBaseline( request: APIRequestContext, jwt: string ): Promise { return probeLimitBaseline(request, jwt, { dailyEnabled: false, monthlyEnabled: true, dailyLimit: null, monthlyLimit: 1, }, 'probe-monthly'); } test.describe('Send Mail Limit', () => { test.beforeEach(async ({ request }) => { await deleteAllMailpitMessages(request); await resetLimitConfig(request); }); test.afterEach(async ({ request }) => { await resetLimitConfig(request); }); test('save + read roundtrip preserves all fields', async ({ request }) => { const config = { dailyEnabled: true, monthlyEnabled: true, dailyLimit: 7, monthlyLimit: 1234, }; const save = await saveLimitConfig(request, config); expect(save.ok()).toBe(true); const read = await request.get(`${WORKER_URL}/admin/account_settings`, { headers: ADMIN_HEADERS, }); expect(read.ok()).toBe(true); const body = await read.json(); expect(body.sendMailLimitConfig).toEqual(config); }); test('disabled flags coerce numeric limits to null', async ({ request }) => { const save = await saveLimitConfig(request, { dailyEnabled: false, monthlyEnabled: false, dailyLimit: 10, monthlyLimit: 20, }); expect(save.ok()).toBe(true); const read = await request.get(`${WORKER_URL}/admin/account_settings`, { headers: ADMIN_HEADERS, }); const body = await read.json(); expect(body.sendMailLimitConfig).toEqual(DISABLED_LIMIT_CONFIG); }); test('minus one is accepted as unlimited', async ({ request }) => { const config = { dailyEnabled: true, monthlyEnabled: true, dailyLimit: -1, monthlyLimit: -1, }; const save = await saveLimitConfig(request, config); expect(save.ok()).toBe(true); const read = await request.get(`${WORKER_URL}/admin/account_settings`, { headers: ADMIN_HEADERS, }); const body = await read.json(); expect(body.sendMailLimitConfig).toEqual(config); }); test('invalid payloads rejected with 400', async ({ request }) => { const cases: Array> = [ { dailyEnabled: 'yes', monthlyEnabled: false, dailyLimit: null, monthlyLimit: null }, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: -2, monthlyLimit: null }, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: 1.5, monthlyLimit: null }, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: null, monthlyLimit: null }, { dailyEnabled: false, monthlyEnabled: true, dailyLimit: null, monthlyLimit: null }, ]; for (const bad of cases) { const res = await saveLimitConfig(request, bad); expect(res.status(), `payload: ${JSON.stringify(bad)}`).toBe(400); } }); test('disabled limit allows unlimited sends', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-off'); await requestSendAccess(request, jwt); await resetLimitConfig(request); for (let i = 0; i < 3; i++) { const { res, listener } = await sendOneMail(request, jwt, `off${i}`); expect(res.ok()).toBe(true); await listener!.message; } await deleteAddress(request, jwt); }); test('zero limit blocks sending immediately', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-zero'); await requestSendAccess(request, jwt); const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: 0, monthlyLimit: null, }); expect(save.ok()).toBe(true); const { res } = await sendOneMail(request, jwt, 'zero', { expectDelivery: false, }); expect(res.ok()).toBe(false); const text = await res.text(); expect(text).toContain('Server daily send quota has been reached'); await deleteAddress(request, jwt); }); test('daily limit blocks once reached and returns English message', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-daily'); await requestSendAccess(request, jwt); const baseline = await probeDailyBaseline(request, jwt); const allowed = 2; const limit = baseline + allowed; const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: limit, monthlyLimit: null, }); expect(save.ok()).toBe(true); for (let i = 0; i < allowed; i++) { const { res, listener } = await sendOneMail(request, jwt, `d${i}`); expect(res.ok(), `send #${i} should succeed`).toBe(true); await listener!.message; } const { res: blocked } = await sendOneMail(request, jwt, 'd-over', { expectDelivery: false, }); expect(blocked.ok()).toBe(false); const text = await blocked.text(); expect(text).toContain('Server daily send quota has been reached'); await deleteAddress(request, jwt); }); test('monthly limit blocks once reached', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-monthly'); await requestSendAccess(request, jwt); const baseline = await probeMonthlyBaseline(request, jwt); const allowed = 2; const limit = baseline + allowed; const save = await saveLimitConfig(request, { dailyEnabled: false, monthlyEnabled: true, dailyLimit: null, monthlyLimit: limit, }); expect(save.ok()).toBe(true); for (let i = 0; i < allowed; i++) { const { res, listener } = await sendOneMail(request, jwt, `m${i}`); expect(res.ok(), `send #${i} should succeed`).toBe(true); await listener!.message; } const { res: blocked } = await sendOneMail(request, jwt, 'm-over', { expectDelivery: false, }); expect(blocked.ok()).toBe(false); const text = await blocked.text(); expect(text).toContain('Server monthly send quota has been reached'); await deleteAddress(request, jwt); }); test('zh-lang header returns Chinese daily limit message', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-zh'); await requestSendAccess(request, jwt); const baseline = await probeDailyBaseline(request, jwt); const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: baseline, monthlyLimit: null, }); expect(save.ok()).toBe(true); const { res } = await sendOneMail(request, jwt, 'zh-over', { expectDelivery: false, lang: 'zh', }); expect(res.ok()).toBe(false); const text = await res.text(); expect(text).toContain('服务器今日发信次数已达上限'); await deleteAddress(request, jwt); }); test('validation failures (missing subject) do not consume quota', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-noconsume'); await requestSendAccess(request, jwt); const baseline = await probeDailyBaseline(request, jwt); const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: baseline + 1, monthlyLimit: null, }); expect(save.ok()).toBe(true); // Empty subject → rejected by validation BEFORE the counter increments. const badRes = await request.post(`${WORKER_URL}/api/send_mail`, { headers: { Authorization: `Bearer ${jwt}` }, data: { from_name: '', to_name: '', to_mail: 'recipient@test.example.com', subject: '', content: 'no subject', is_html: false, }, }); expect(badRes.ok()).toBe(false); const { res, listener } = await sendOneMail(request, jwt, 'after-bad'); expect(res.ok()).toBe(true); await listener!.message; await deleteAddress(request, jwt); }); test('both daily + monthly enabled: tighter daily limit wins', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-both'); await requestSendAccess(request, jwt); const baseline = await probeDailyBaseline(request, jwt); const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: true, dailyLimit: baseline, monthlyLimit: baseline + 10_000, }); expect(save.ok()).toBe(true); const { res } = await sendOneMail(request, jwt, 'both-over', { expectDelivery: false, }); expect(res.ok()).toBe(false); const text = await res.text(); expect(text).toContain('Server daily send quota has been reached'); await deleteAddress(request, jwt); }); test('minus one means unlimited at runtime', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-unlimited'); await requestSendAccess(request, jwt); const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: true, dailyLimit: -1, monthlyLimit: -1, }); expect(save.ok()).toBe(true); for (let i = 0; i < 3; i++) { const { res, listener } = await sendOneMail(request, jwt, `unl${i}`); expect(res.ok(), `send #${i} should succeed`).toBe(true); await listener!.message; } await deleteAddress(request, jwt); }); test('/admin/send_mail_by_binding returns 400 when SEND_MAIL binding is missing', async ({ request }) => { const res = await request.post(`${WORKER_URL}/admin/send_mail_by_binding`, { headers: ADMIN_HEADERS, data: { from: 'admin@test.example.com', to: ['recipient@test.example.com'], subject: 'no-binding', text: 'body', }, }); expect(res.status()).toBe(400); }); test('/admin/send_mail_by_binding returns 200 when domain is allowed', async ({ request }) => { const res = await request.post(`${WORKER_URL_SEND_MAIL_DOMAIN}/admin/send_mail_by_binding`, { headers: ADMIN_HEADERS, data: { from: 'admin@test.example.com', to: ['recipient@test.example.com'], subject: `send-mail-domain-ok-${Date.now()}`, text: 'body', }, }); expect(res.ok()).toBe(true); expect(await res.json()).toEqual({ status: 'ok' }); }); test('/admin/send_mail_by_binding returns 400 when domain is not allowed', async ({ request }) => { const res = await request.post(`${WORKER_URL_SEND_MAIL_DOMAIN}/admin/send_mail_by_binding`, { headers: ADMIN_HEADERS, data: { from: 'admin@blocked.example.com', to: ['recipient@test.example.com'], subject: `send-mail-domain-blocked-${Date.now()}`, text: 'body', }, }); expect(res.status()).toBe(400); expect(await res.text()).toContain('Please enable SEND_MAIL for this domain first'); }); test('daily and monthly counters both increment on successful send', async ({ request }) => { const { jwt } = await createTestAddress(request, 'limit-both-inc'); await requestSendAccess(request, jwt); const dailyBaseline = await probeDailyBaseline(request, jwt); const monthlyBaseline = await probeMonthlyBaseline(request, jwt); // Give plenty of headroom so sends succeed. const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: true, dailyLimit: dailyBaseline + 10, monthlyLimit: monthlyBaseline + 10, }); expect(save.ok()).toBe(true); const { res, listener } = await sendOneMail(request, jwt, 'inc'); expect(res.ok()).toBe(true); await listener!.message; // Re-probe to confirm both counters moved forward after the successful send. const dailyAfter = await probeDailyBaseline(request, jwt); expect(dailyAfter).toBeGreaterThanOrEqual(dailyBaseline + 1); const monthlyAfter = await probeMonthlyBaseline(request, jwt); expect(monthlyAfter).toBeGreaterThanOrEqual(monthlyBaseline + 1); await deleteAddress(request, jwt); }); test('admin /admin/send_mail also respects daily limit', async ({ request }) => { const { jwt, address } = await createTestAddress(request, 'limit-admin'); await requestSendAccess(request, jwt); // Probe via a user-facing send to establish baseline. const baseline = await probeDailyBaseline(request, jwt); const save = await saveLimitConfig(request, { dailyEnabled: true, monthlyEnabled: false, dailyLimit: baseline, monthlyLimit: null, }); expect(save.ok()).toBe(true); const res = await request.post(`${WORKER_URL}/admin/send_mail`, { headers: ADMIN_HEADERS, data: { from_name: '', from_mail: address, to_name: '', to_mail: 'recipient@test.example.com', subject: `admin-over-${Date.now()}`, content: 'admin blocked body', is_html: false, }, }); expect(res.ok()).toBe(false); const text = await res.text(); expect(text).toContain('Server daily send quota has been reached'); await deleteAddress(request, jwt); }); });