fix: normalize domain casing

Fix domain casing normalization for configured domains and inbound recipient domains.
This commit is contained in:
Hging
2026-05-16 03:35:39 -07:00
committed by GitHub
parent 8324b133fb
commit add0124cfd
26 changed files with 641 additions and 86 deletions

View File

@@ -16,4 +16,116 @@ test.describe('Admin New Address', () => {
expect(body.address_id).toBeGreaterThan(0);
expect(typeof body.address_id).toBe('number');
});
test('normalizes uppercase configured prefix and domain', async ({ request }) => {
const uniqueName = `admincase${Date.now()}`;
const res = await request.post(`${WORKER_URL}/admin/new_address`, {
data: { name: uniqueName, domain: TEST_DOMAIN.toUpperCase(), enablePrefix: true },
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.address).toBe(`tmp${uniqueName}@${TEST_DOMAIN}`);
expect(body.jwt).toBeTruthy();
expect(body.address_id).toBeGreaterThan(0);
});
test('falls back to domains when default domains is empty', async ({ request }) => {
const uniqueName = `fallback${Date.now().toString(36)}`;
const res = await request.post(`${WORKER_URL}/api/new_address`, {
data: { name: uniqueName },
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.address).toBe(`tmp${uniqueName}@${TEST_DOMAIN}`);
expect(body.jwt).toBeTruthy();
expect(body.address_id).toBeGreaterThan(0);
});
test('normalizes user role domains and prefix', async ({ request }) => {
const suffix = `${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
const email = `role-case-${suffix}@${TEST_DOMAIN}`;
const password = `role-pass-${suffix}`;
const name = `rolecase${suffix}`;
const createUserRes = await request.post(`${WORKER_URL}/admin/users`, {
data: { email, password },
});
expect(createUserRes.ok()).toBe(true);
const usersRes = await request.get(`${WORKER_URL}/admin/users`, {
params: { limit: '20', offset: '0', query: email },
});
expect(usersRes.ok()).toBe(true);
const usersBody = await usersRes.json();
const user = usersBody.results.find((row: { user_email: string }) => row.user_email === email);
expect(user).toBeTruthy();
const updateRoleRes = await request.post(`${WORKER_URL}/admin/user_roles`, {
data: { user_id: user.id, role_text: 'case-role' },
});
expect(updateRoleRes.ok()).toBe(true);
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
data: { email, password },
});
expect(loginRes.ok()).toBe(true);
const { jwt: userJwt } = await loginRes.json();
const createAddressRes = await request.post(`${WORKER_URL}/api/new_address`, {
headers: { 'x-user-token': userJwt },
data: { name },
});
expect(createAddressRes.ok()).toBe(true);
const addressBody = await createAddressRes.json();
expect(addressBody.address).toBe(`role${name}@${TEST_DOMAIN}`);
expect(addressBody.jwt).toBeTruthy();
expect(addressBody.address_id).toBeGreaterThan(0);
});
test('falls back to default domains when user role domains is empty', async ({ request }) => {
const suffix = `${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
const email = `empty-role-${suffix}@${TEST_DOMAIN}`;
const password = `empty-role-pass-${suffix}`;
const name = `emptyrole${suffix}`;
const createUserRes = await request.post(`${WORKER_URL}/admin/users`, {
data: { email, password },
});
expect(createUserRes.ok()).toBe(true);
const usersRes = await request.get(`${WORKER_URL}/admin/users`, {
params: { limit: '20', offset: '0', query: email },
});
expect(usersRes.ok()).toBe(true);
const usersBody = await usersRes.json();
const user = usersBody.results.find((row: { user_email: string }) => row.user_email === email);
expect(user).toBeTruthy();
const updateRoleRes = await request.post(`${WORKER_URL}/admin/user_roles`, {
data: { user_id: user.id, role_text: 'empty-role' },
});
expect(updateRoleRes.ok()).toBe(true);
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
data: { email, password },
});
expect(loginRes.ok()).toBe(true);
const { jwt: userJwt } = await loginRes.json();
const createAddressRes = await request.post(`${WORKER_URL}/api/new_address`, {
headers: { 'x-user-token': userJwt },
data: { name },
});
expect(createAddressRes.ok()).toBe(true);
const addressBody = await createAddressRes.json();
expect(addressBody.address).toBe(`empty${name}@${TEST_DOMAIN}`);
expect(addressBody.jwt).toBeTruthy();
expect(addressBody.address_id).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,175 @@
import { test, expect } from '@playwright/test';
import type { APIRequestContext } from '@playwright/test';
import {
WORKER_URL,
TEST_DOMAIN,
createTestAddress,
deleteAddress,
} 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: {},
};
async function resetAccountSettings(request: APIRequestContext) {
const res = await request.post(`${WORKER_URL}/admin/account_settings`, {
headers: ADMIN_HEADERS,
data: DEFAULT_ACCOUNT_SETTINGS,
});
expect(res.ok()).toBe(true);
}
test.describe('Email forward domain normalization', () => {
test.afterEach(async ({ request }) => {
await resetAccountSettings(request);
});
test('normalizes uppercase forwarding rule and recipient domains', async ({ request }) => {
const { jwt, address } = await createTestAddress(request, 'forward-case');
const forwardAddress = 'forward-target@test.example.com';
try {
const saveRes = await request.post(`${WORKER_URL}/admin/account_settings`, {
headers: ADMIN_HEADERS,
data: {
...DEFAULT_ACCOUNT_SETTINGS,
emailRuleSettings: {
emailForwardingList: [{
domains: [TEST_DOMAIN.toUpperCase()],
forward: forwardAddress,
}],
},
},
});
expect(saveRes.ok()).toBe(true);
const to = address.replace(`@${TEST_DOMAIN}`, `@${TEST_DOMAIN.toUpperCase()}`);
const subject = `forward-case-${Date.now()}`;
const raw = [
`From: sender@test.example.com`,
`To: ${to}`,
`Subject: ${subject}`,
`Message-ID: <${subject}@test>`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=utf-8`,
``,
`Forward domain normalization test`,
].join('\r\n');
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
data: { from: 'sender@test.example.com', to, raw },
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.success).toBe(true);
expect(body.forwardedTo).toEqual([forwardAddress]);
const mailsRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
headers: { Authorization: `Bearer ${jwt}` },
});
expect(mailsRes.ok()).toBe(true);
const mailsBody = await mailsRes.json();
expect(mailsBody.results.some((mail: { address: string; raw: string }) => {
return mail.address === address && mail.raw.includes(subject);
})).toBe(true);
} finally {
await deleteAddress(request, jwt);
}
});
test('does not forward when only the domain suffix string matches', async ({ request }) => {
const { jwt, address } = await createTestAddress(request, 'forward-boundary');
const forwardAddress = 'forward-boundary-target@test.example.com';
try {
const saveRes = await request.post(`${WORKER_URL}/admin/account_settings`, {
headers: ADMIN_HEADERS,
data: {
...DEFAULT_ACCOUNT_SETTINGS,
emailRuleSettings: {
emailForwardingList: [{
domains: [TEST_DOMAIN],
forward: forwardAddress,
}],
},
},
});
expect(saveRes.ok()).toBe(true);
const to = address.replace(`@${TEST_DOMAIN}`, `@evil${TEST_DOMAIN}`);
const subject = `forward-boundary-${Date.now()}`;
const raw = [
`From: sender@test.example.com`,
`To: ${to}`,
`Subject: ${subject}`,
`Message-ID: <${subject}@test>`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=utf-8`,
``,
`Forward domain boundary test`,
].join('\r\n');
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
data: { from: 'sender@test.example.com', to, raw },
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.success).toBe(true);
expect(body.forwardedTo).toEqual([]);
} finally {
await deleteAddress(request, jwt);
}
});
test('keeps blank forwarding rule domain as catch-all', async ({ request }) => {
const { jwt, address } = await createTestAddress(request, 'forward-catch-all');
const forwardAddress = 'forward-catch-all-target@test.example.com';
try {
const saveRes = await request.post(`${WORKER_URL}/admin/account_settings`, {
headers: ADMIN_HEADERS,
data: {
...DEFAULT_ACCOUNT_SETTINGS,
emailRuleSettings: {
emailForwardingList: [{
domains: ['', 'not-the-domain.example.com'],
forward: forwardAddress,
}],
},
},
});
expect(saveRes.ok()).toBe(true);
const subject = `forward-catch-all-${Date.now()}`;
const raw = [
`From: sender@test.example.com`,
`To: ${address}`,
`Subject: ${subject}`,
`Message-ID: <${subject}@test>`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=utf-8`,
``,
`Forward catch-all domain test`,
].join('\r\n');
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
data: { from: 'sender@test.example.com', to: address, raw },
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.success).toBe(true);
expect(body.forwardedTo).toEqual([forwardAddress]);
} finally {
await deleteAddress(request, jwt);
}
});
});

View File

@@ -15,6 +15,7 @@ test.describe('Health & Settings', () => {
const settings = await res.json();
expect(settings.domains).toContain('test.example.com');
expect(settings.defaultDomains).toContain('test.example.com');
expect(settings.prefix).toBe('tmp');
expect(settings.enableSendMail).toBe(true);
expect(settings.enableUserCreateEmail).toBe(true);
expect(settings.enableUserDeleteEmail).toBe(true);

View File

@@ -1,4 +1,5 @@
import { test, expect, APIRequestContext } from '@playwright/test';
import { test, expect } from '@playwright/test';
import type { APIRequestContext } from '@playwright/test';
import {
WORKER_URL,
WORKER_URL_SEND_MAIL_DOMAIN,
@@ -425,11 +426,11 @@ test.describe('Send Mail Limit', () => {
expect(res.status()).toBe(400);
});
test('/admin/send_mail_by_binding returns 200 when domain is allowed', async ({ request }) => {
test('/admin/send_mail_by_binding normalizes uppercase allowed domain', 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',
from: 'admin@TEST.EXAMPLE.COM',
to: ['recipient@test.example.com'],
subject: `send-mail-domain-ok-${Date.now()}`,
text: 'body',
@@ -439,6 +440,20 @@ test.describe('Send Mail Limit', () => {
expect(await res.json()).toEqual({ status: 'ok' });
});
test('/admin/send_mail_by_binding normalizes object-form from domain', async ({ request }) => {
const res = await request.post(`${WORKER_URL_SEND_MAIL_DOMAIN}/admin/send_mail_by_binding`, {
headers: ADMIN_HEADERS,
data: {
from: { email: 'admin@TEST.EXAMPLE.COM', name: 'Admin' },
to: ['recipient@test.example.com'],
subject: `send-mail-domain-object-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,

View File

@@ -0,0 +1,118 @@
import { test, expect } from '@playwright/test';
import type { APIRequestContext } from '@playwright/test';
import http from 'node:http';
import { WORKER_URL } from '../../fixtures/test-helpers';
async function resetUserSettings(request: APIRequestContext) {
const res = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: {
enable: true,
enableMailVerify: false,
enableMailAllowList: false,
mailAllowList: [],
},
});
expect(res.ok()).toBe(true);
}
async function resetOauth2Settings(request: APIRequestContext) {
const res = await request.post(`${WORKER_URL}/admin/user_oauth2_settings`, {
data: [],
});
expect(res.ok()).toBe(true);
}
async function startOauthServer(email: string): Promise<{ server: http.Server; baseUrl: string }> {
const server = http.createServer((req, res) => {
if (req.url === '/token') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ access_token: 'token', token_type: 'Bearer' }));
return;
}
if (req.url === '/userinfo') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ email }));
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('not found');
});
await new Promise<void>((resolve) => server.listen(0, '0.0.0.0', resolve));
const addr = server.address();
if (!addr || typeof addr === 'string') throw new Error('Failed to resolve OAuth test server port');
const hostname = process.env.CI ? 'e2e-runner' : 'localhost';
return { server, baseUrl: `http://${hostname}:${addr.port}` };
}
test.describe('User domain normalization', () => {
test.afterEach(async ({ request }) => {
await resetOauth2Settings(request);
await resetUserSettings(request);
});
test('normalizes uppercase verify mail sender domain in admin settings', async ({ request }) => {
const res = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: {
enable: true,
enableMailVerify: true,
verifyMailSender: 'verify@TEST.EXAMPLE.COM',
},
});
expect(res.ok()).toBe(true);
});
test('normalizes uppercase user registration allow-list domains', async ({ request }) => {
const saveRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: {
enable: true,
enableMailVerify: false,
enableMailAllowList: true,
mailAllowList: ['TEST.EXAMPLE.COM'],
},
});
expect(saveRes.ok()).toBe(true);
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
data: {
email: `allow-list-${Date.now()}@TEST.EXAMPLE.COM`,
password: 'allow-list-password',
},
});
expect(registerRes.ok()).toBe(true);
});
test('normalizes uppercase OAuth2 allow-list domains', async ({ request }) => {
const email = `oauth-allow-${Date.now()}@TEST.EXAMPLE.COM`;
const { server, baseUrl } = await startOauthServer(email);
try {
const saveRes = await request.post(`${WORKER_URL}/admin/user_oauth2_settings`, {
data: [{
name: 'case-oauth',
clientID: 'case-client',
clientSecret: 'case-secret',
authorizationURL: `${baseUrl}/authorize`,
accessTokenURL: `${baseUrl}/token`,
accessTokenFormat: 'json',
userInfoURL: `${baseUrl}/userinfo`,
redirectURL: `${baseUrl}/callback`,
userEmailKey: 'email',
scope: 'openid email',
enableMailAllowList: true,
mailAllowList: ['TEST.EXAMPLE.COM'],
}],
});
expect(saveRes.ok()).toBe(true);
const callbackRes = await request.post(`${WORKER_URL}/user_api/oauth2/callback`, {
data: { clientID: 'case-client', code: 'case-code' },
});
expect(callbackRes.ok()).toBe(true);
const body = await callbackRes.json();
expect(body.jwt).toBeTruthy();
} finally {
server.close();
}
});
});