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

@@ -17,6 +17,7 @@
- fix: |Admin| 管理员重置邮箱地址密码时改为前端 SHA-256 后提交,后端只接受并存储哈希值,避免该接口继续接收明文密码
- fix: |Address| 管理员邮箱地址列表与用户绑定地址列表不再返回已存储的地址密码哈希值,避免列表接口暴露敏感字段
- fix: |Address| 统一规范化配置域名、收件地址域名与前缀的空白和大小写,覆盖 `DOMAINS``DEFAULT_DOMAINS``USER_ROLES.domains`、随机子域名、转发规则、SMTP 与 `SEND_MAIL` 域名匹配,保留转发规则空域名 catch-all 行为,并明确空 `DEFAULT_DOMAINS` / 角色域名回退到 `DOMAINS` 的行为避免大小写配置或入站收件域名导致创建、收件、转发或发信失败issue #926
- fix: |AI 提取| 将 AI 邮件识别默认 Workers AI 模型切换为支持 JSON Mode 且未弃用的 `@cf/meta/llama-3.1-8b-instruct-fast`,并在文档中补充 `@cf/zai-org/glm-4.7-flash` 结构化输出兼容性提示issue #1029
- fix: |CI| 将 GitHub Actions 与 e2e Docker 镜像统一升级到 Node.js 24适配 Wrangler 4.90.0 的运行时要求
- fix: |Frontend| 修复 iOS Safari 点击输入框时因移动端表单控件字号过小导致页面自动放大的问题

View File

@@ -17,6 +17,7 @@
- fix: |Admin| Hash address passwords in the frontend before admin reset requests, and make the backend accept and store only the hash instead of plaintext
- fix: |Address| Stop returning stored address password hashes from the admin address list and user bound-address list APIs to avoid exposing sensitive fields
- fix: |Address| Normalize whitespace and casing for configured domains, inbound recipient domains, and prefixes across `DOMAINS`, `DEFAULT_DOMAINS`, `USER_ROLES.domains`, random subdomains, forwarding rules, SMTP, and `SEND_MAIL` domain matching, preserve blank-domain catch-all forwarding rules, and clarify that empty `DEFAULT_DOMAINS` / role domains fall back to `DOMAINS`, to avoid create, receive, forward, or send failures caused by mixed-case configuration or inbound recipient domains (issue #926)
- fix: |AI Extract| Switch the default Workers AI model for AI email recognition to the JSON Mode-compatible, non-deprecated `@cf/meta/llama-3.1-8b-instruct-fast`, and document structured-output compatibility guidance for `@cf/zai-org/glm-4.7-flash` (issue #1029)
- fix: |CI| Upgrade GitHub Actions and e2e Docker images to Node.js 24 to satisfy Wrangler 4.90.0 runtime requirements
- fix: |Frontend| Prevent iOS Safari from auto-zooming the page when focusing mobile form controls with small font sizes

View File

@@ -5,9 +5,13 @@ compatibility_flags = [ "nodejs_compat" ]
keep_vars = true
[vars]
PREFIX = "tmp"
DEFAULT_DOMAINS = ["test.example.com"]
DOMAINS = ["test.example.com"]
PREFIX = "TMP"
DEFAULT_DOMAINS = []
DOMAINS = ["TEST.EXAMPLE.COM"]
USER_ROLES = [
{ domains = ["TEST.EXAMPLE.COM"], role = "case-role", prefix = "ROLE" },
{ domains = [], role = "empty-role", prefix = "EMPTY" },
]
JWT_SECRET = "e2e-test-secret-key"
BLACK_LIST = ""
ENABLE_USER_CREATE_EMAIL = true
@@ -20,7 +24,7 @@ ADMIN_PASSWORDS = '["e2e-admin-pass"]'
ENABLE_WEBHOOK = true
E2E_TEST_MODE = true
SMTP_CONFIG = """
{"test.example.com":{"host":"mailpit","port":1025,"secure":false}}
{"TEST.EXAMPLE.COM":{"host":"mailpit","port":1025,"secure":false}}
"""
[[kv_namespaces]]

View File

@@ -9,10 +9,10 @@ send_email = [
]
[vars]
PREFIX = "tmp"
DEFAULT_DOMAINS = ["test.example.com"]
DOMAINS = ["test.example.com"]
SEND_MAIL_DOMAINS = ["test.example.com"]
PREFIX = "TMP"
DEFAULT_DOMAINS = ["TEST.EXAMPLE.COM"]
DOMAINS = ["TEST.EXAMPLE.COM"]
SEND_MAIL_DOMAINS = ["TEST.EXAMPLE.COM"]
JWT_SECRET = "e2e-test-secret-key"
BLACK_LIST = ""
ENABLE_USER_CREATE_EMAIL = true
@@ -25,7 +25,7 @@ ADMIN_PASSWORDS = '["e2e-admin-pass"]'
ENABLE_WEBHOOK = true
E2E_TEST_MODE = true
SMTP_CONFIG = """
{"test.example.com":{"host":"mailpit","port":1025,"secure":false}}
{"TEST.EXAMPLE.COM":{"host":"mailpit","port":1025,"secure":false}}
"""
[[kv_namespaces]]

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();
}
});
});

View File

@@ -132,8 +132,10 @@ test.describe('IMAP Proxy', () => {
try {
const results = await client.search({ all: true });
expect(results.length).toBeGreaterThan(0);
const msg = await client.fetchOne(String(results[0]), { uid: true, flags: true }, { uid: true });
expect(msg.uid).toBe(results[0]);
const seqMsg = await client.fetchOne(String(results[0]), { uid: true, flags: true });
expect(seqMsg.uid).toBeGreaterThan(0);
const uidMsg = await client.fetchOne(String(seqMsg.uid), { uid: true, flags: true }, { uid: true });
expect(uidMsg.uid).toBe(seqMsg.uid);
} finally {
lock.release();
}

View File

@@ -48,6 +48,8 @@
| `SEND_MAIL_DOMAINS` | JSON | Restrict which sender domains can use the `SEND_MAIL` binding; when unset or empty, all domains are allowed | `["example.com", "mail.example.com"]` |
> [!NOTE]
> When `DEFAULT_DOMAINS` is unset or configured as an empty array, it falls back to `DOMAINS`.
>
> `RANDOM_SUBDOMAIN_DOMAINS` only controls automatic random subdomain generation during mailbox
> creation. It does not create Cloudflare-side subdomain routing for you.
>
@@ -137,7 +139,7 @@
> [!NOTE] USER_ROLES User Role Configuration
>
> - If `domains` is empty, `DEFAULT_DOMAINS` will be used
> - If `domains` is empty, `DEFAULT_DOMAINS` will be used; if `DEFAULT_DOMAINS` is also empty, it falls back to `DOMAINS`
> - If prefix is null, the default prefix will be used, if prefix is an empty string, no prefix will be used
>
> When deploying through UI, configure `USER_ROLES` in this format: `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`

View File

@@ -48,6 +48,8 @@
| `SEND_MAIL_DOMAINS` | JSON | 限制 `SEND_MAIL` binding 可用于哪些发件域名;留空或不配置时允许所有域名 | `["example.com", "mail.example.com"]` |
> [!NOTE]
> `DEFAULT_DOMAINS` 未配置或配置为空数组时,会回退使用 `DOMAINS`。
>
> `RANDOM_SUBDOMAIN_DOMAINS` 只负责“创建地址时自动补随机子域名”,不会自动帮你创建 Cloudflare
> 侧的子域名路由。
>
@@ -131,7 +133,7 @@
> [!NOTE] USER_ROLES 用户角色配置说明
>
> - 如果 `domains` 为空将使用 `DEFAULT_DOMAINS`
> - 如果 `domains` 为空将使用 `DEFAULT_DOMAINS`;如果 `DEFAULT_DOMAINS` 也为空,则继续回退到 `DOMAINS`
> - 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
>
> 通过用户界面部署时 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`

View File

@@ -1,7 +1,7 @@
import { Context } from 'hono';
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles, getMailDomain, includesDomain } from '../utils';
import { UserSettings, GeoData, UserInfo, RoleAddressConfig } from "../models";
import { handleListQuery } from '../common'
import UserBindAddressModule from '../user_api/bind_address';
@@ -24,9 +24,9 @@ export default {
return c.text(msgs.VerifyMailSenderNotSetMsg, 400)
}
if (settings.enableMailVerify && settings.verifyMailSender) {
const mailDomain = settings.verifyMailSender.split("@")[1];
const mailDomain = getMailDomain(settings.verifyMailSender);
const domains = getDomains(c);
if (!domains.includes(mailDomain)) {
if (!includesDomain(domains, mailDomain)) {
return c.text(`${msgs.VerifyMailDomainInvalidMsg} ${JSON.stringify(domains, null, 2)}`, 400)
}
}

View File

@@ -44,19 +44,24 @@ const receiveMail = async (c: Context<HonoCustomType>) => {
if (!headers.has('Message-ID')) headers.set('Message-ID', `<e2e-${Date.now()}@test>`);
const rawBytes = new TextEncoder().encode(raw);
const state = { rejected: undefined as string | undefined, replyCalled: false };
const state = { rejected: undefined as string | undefined, replyCalled: false, forwardedTo: [] as string[] };
const mockMessage: ForwardableEmailMessage = {
from, to, headers,
rawSize: rawBytes.byteLength,
raw: new ReadableStream({ start(ctrl) { ctrl.enqueue(rawBytes); ctrl.close(); } }),
setReject(reason: string) { state.rejected = reason; },
forward: async () => ({ messageId: '' }),
forward: async (recipient: string) => { state.forwardedTo.push(recipient); return { messageId: '' }; },
reply: async () => { state.replyCalled = true; return { messageId: '' }; },
};
const { email: emailHandler } = await import('../email');
await emailHandler(mockMessage, c.env, { waitUntil: () => {}, passThroughOnException: () => {} });
return c.json({ success: !state.rejected, replyCalled: state.replyCalled, ...(state.rejected ? { rejected: state.rejected } : {}) });
return c.json({
success: !state.rejected,
replyCalled: state.replyCalled,
forwardedTo: state.forwardedTo,
...(state.rejected ? { rejected: state.rejected } : {})
});
};
export default { seedMail, receiveMail };

View File

@@ -3,6 +3,7 @@ import { isSendMailBindingEnabled } from "../common";
import i18n from "../i18n";
import { sendMail } from "../mails_api/send_mail_api";
import { ensureSendMailLimit, increaseSendMailLimitCount } from "../mails_api/send_mail_limit_utils";
import { getMailDomain } from "../utils";
const getAdminSendMailErrorMessage = (
msgs: ReturnType<typeof i18n.getMessagesbyContext>,
@@ -68,9 +69,7 @@ export const sendMailByBindingAdmin = async (c: Context<HonoCustomType>) => {
return c.text(msgs.InvalidInputMsg, 400)
}
const fromMail = typeof from === "string" ? from : from?.email;
const mailDomain = typeof fromMail === "string" && fromMail.includes("@")
? fromMail.split("@")[1]?.trim().toLowerCase()
: null;
const mailDomain = getMailDomain(fromMail);
if (!mailDomain) {
return c.text(msgs.InvalidInputMsg, 400)
}

View File

@@ -25,7 +25,7 @@ api.get('/open_api/settings', async (c) => {
"title": c.env.TITLE,
"announcement": utils.getStringValue(c.env.ANNOUNCEMENT),
"alwaysShowAnnouncement": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
"prefix": utils.getStringValue(c.env.PREFIX),
"prefix": utils.trimLower(c.env.PREFIX),
"addressRegex": utils.getStringValue(c.env.ADDRESS_REGEX),
"minAddressLen": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
"maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),

View File

@@ -2,7 +2,7 @@ import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import { WorkerMailerOptions } from 'worker-mailer';
import { getBooleanValue, getDomains, getStringArray, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains } from './utils';
import { getBooleanValue, getDomains, getStringArray, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains, getDomainMapValue, normalizeDomains, trimLower } from './utils';
import { unbindTelegramByAddress } from './telegram_api/common';
import { CONSTANTS } from './constants';
import { AddressCreationSettings, AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
@@ -15,7 +15,7 @@ const MAX_DOMAIN_LENGTH = 253;
const DOMAIN_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const normalizeDomainValue = (domain: string): string => {
return domain.trim().toLowerCase();
return trimLower(domain);
}
const isValidDomainLabel = (label: string): boolean => {
@@ -41,7 +41,7 @@ export const isSendMailEnabled = (
// Check SMTP config for domain
const smtpConfigMap = getJsonObjectValue<Record<string, WorkerMailerOptions>>(c.env.SMTP_CONFIG);
if (smtpConfigMap && smtpConfigMap[mailDomain]) return true;
if (getDomainMapValue(smtpConfigMap, mailDomain)) return true;
// Check SEND_MAIL binding
if (isSendMailBindingEnabled(c, mailDomain)) return true;
@@ -56,8 +56,7 @@ export const isSendMailBindingEnabled = (
if (!c.env.SEND_MAIL) {
return false;
}
const sendMailDomains = getStringArray(c.env.SEND_MAIL_DOMAINS)
.map((domain) => normalizeDomainValue(domain));
const sendMailDomains = normalizeDomains(getStringArray(c.env.SEND_MAIL_DOMAINS));
if (sendMailDomains.length === 0) {
return true;
}
@@ -369,9 +368,9 @@ export const newAddress = async (
}
// create address with prefix
if (typeof addressPrefix === "string") {
name = addressPrefix.trim() + name;
name = trimLower(addressPrefix) + name;
} else if (enablePrefix) {
name = getStringValue(c.env.PREFIX).trim() + name;
name = trimLower(c.env.PREFIX) + name;
}
// check domain
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
@@ -760,13 +759,13 @@ export const commonGetUserRole = async (
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
const user = c.get("userPayload");
if (!user) {
return getStringValue(c.env.PREFIX).trim().toLowerCase();
return trimLower(c.env.PREFIX);
}
const user_role = await commonGetUserRole(c, user.user_id);
if (typeof user_role?.prefix === "string") {
return user_role.prefix.trim().toLowerCase();
return trimLower(user_role.prefix);
}
return getStringValue(c.env.PREFIX).trim().toLowerCase();
return trimLower(c.env.PREFIX);
}
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
@@ -775,7 +774,10 @@ export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<strin
return getDefaultDomains(c);
}
const user_role = await commonGetUserRole(c, user.user_id);
return user_role?.domains || getDefaultDomains(c);;
if (user_role?.domains && user_role.domains.length > 0) {
return normalizeDomains(user_role.domains);
}
return getDefaultDomains(c);
}
export async function sendWebhook(

View File

@@ -1,5 +1,5 @@
import { createMimeMessage } from "mimetext";
import { getBooleanValue } from "../utils";
import { getBooleanValue, normalizeAddressDomain } from "../utils";
/**
* Check if the sender matches the source_prefix filter.
@@ -21,14 +21,18 @@ function matchSender(from: string, sourcePrefix: string | undefined): boolean {
return from.startsWith(sourcePrefix);
}
export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise<void> => {
export const auto_reply = async (
message: ForwardableEmailMessage,
env: Bindings,
toAddress: string = normalizeAddressDomain(message.to)
): Promise<void> => {
const message_id = message.headers.get("Message-ID");
// auto reply email
if (getBooleanValue(env.ENABLE_AUTO_REPLY) && message_id) {
try {
const results = await env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? and enabled = 1`
).bind(message.to).first<Record<string, string>>();
).bind(toAddress).first<Record<string, string>>();
if (results && matchSender(message.from, results.source_prefix)) {
if (!results.subject || !results.message) {
console.log("auto-reply using defaults:", !results.subject ? "subject" : "", !results.message ? "message" : "");
@@ -50,7 +54,7 @@ export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings
} else {
const { EmailMessage } = await import('cloudflare:email');
const replyMessage = new EmailMessage(
message.to,
toAddress,
message.from,
msg.asRaw()
);

View File

@@ -1,6 +1,6 @@
import { Context } from "hono";
import { getEnvStringList, getJsonObjectValue, getJsonSetting } from "../utils";
import { getEnvStringList, getJsonObjectValue, getJsonSetting, getMailDomain, isDomainOrSubdomain, normalizeDomain } from "../utils";
import { EmailRuleSettings } from "../models";
import { CONSTANTS } from "../constants";
@@ -91,6 +91,7 @@ async function forwardByRules(
...(emailRuleSettings?.emailForwardingList || []),
];
const messageDomain = getMailDomain(message.to);
for (const rule of allRules) {
// 检查来源地址是否匹配正则
if (!matchSourcePatterns(message.from, rule.sourcePatterns, rule.sourceMatchMode)) {
@@ -100,8 +101,16 @@ async function forwardByRules(
// 检查目标地址是否匹配域名,并转发
// 保持原始逻辑:每个匹配的 domain 都会触发一次转发
if (rule.domains && rule.domains.length > 0) {
for (const domain of rule.domains) {
if (message.to.endsWith(domain) && rule.forward) {
const normalizedDomains = rule.domains.map(normalizeDomain);
if (normalizedDomains.some(domain => domain.length === 0)) {
if (rule.forward) {
await message.forward(rule.forward);
}
continue;
}
for (const normalizedDomain of normalizedDomains) {
if (isDomainOrSubdomain(messageDomain, normalizedDomain) && rule.forward) {
await message.forward(rule.forward);
}
}

View File

@@ -1,6 +1,6 @@
import { Context } from "hono";
import { getBooleanValue, getJsonSetting } from "../utils";
import { getBooleanValue, getJsonSetting, normalizeAddressDomain } from "../utils";
import { sendMailToTelegram } from "../telegram_api";
import { auto_reply } from "./auto_reply";
import { isBlocked } from "./black_list";
@@ -15,9 +15,10 @@ import { compressText } from "../gzip";
async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) {
const toAddress = normalizeAddressDomain(message.to);
if (await isBlocked(message.from, env)) {
message.setReject("Reject from address");
console.log(`Reject message from ${message.from} to ${message.to}`);
console.log(`Reject message from ${message.from} to ${toAddress}`);
return;
}
const rawEmail = await new Response(message.raw).text();
@@ -27,10 +28,10 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
// check if junk mail
try {
const is_junk = await check_if_junk_mail(env, message.to, parsedEmailContext, message.headers.get("Message-ID"));
const is_junk = await check_if_junk_mail(env, toAddress, parsedEmailContext, message.headers.get("Message-ID"));
if (is_junk) {
message.setReject("Junk mail");
console.log(`Junk mail from ${message.from} to ${message.to}`);
console.log(`Junk mail from ${message.from} to ${toAddress}`);
return;
}
} catch (error) {
@@ -45,10 +46,10 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
if (emailRuleSettings?.blockReceiveUnknowAddressEmail) {
const db_address_id = await env.DB.prepare(
`SELECT id FROM address where name = ? `
).bind(message.to).first("id");
).bind(toAddress).first("id");
if (!db_address_id) {
message.setReject("Unknown address");
console.log(`Unknown address mail from ${message.from} to ${message.to}`);
console.log(`Unknown address mail from ${message.from} to ${toAddress}`);
return;
}
}
@@ -58,7 +59,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
// remove attachment if configured or size > 2MB
try {
await remove_attachment_if_need(env, parsedEmailContext, message.from, message.to, message.rawSize);
await remove_attachment_if_need(env, parsedEmailContext, message.from, toAddress, message.rawSize);
} catch (error) {
console.error("remove attachment error", error);
}
@@ -79,7 +80,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
({ success } = await env.DB.prepare(
`INSERT INTO raw_mails (source, address, raw_blob, message_id) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to, compressed, message_id
message.from, toAddress, compressed, message_id
).run());
} catch (dbError) {
// Fallback to plaintext only if raw_blob column is missing (migration not applied)
@@ -89,7 +90,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
({ success } = await env.DB.prepare(
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to, parsedEmailContext.rawEmail, message_id
message.from, toAddress, parsedEmailContext.rawEmail, message_id
).run());
} else {
throw dbError;
@@ -99,19 +100,19 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
({ success } = await env.DB.prepare(
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to, parsedEmailContext.rawEmail, message_id
message.from, toAddress, parsedEmailContext.rawEmail, message_id
).run());
}
} else {
({ success } = await env.DB.prepare(
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to, parsedEmailContext.rawEmail, message_id
message.from, toAddress, parsedEmailContext.rawEmail, message_id
).run());
}
if (!success) {
message.setReject(`Failed save message to ${message.to}`);
console.error(`Failed save message from ${message.from} to ${message.to}`);
message.setReject(`Failed save message to ${toAddress}`);
console.error(`Failed save message from ${message.from} to ${toAddress}`);
}
}
catch (error) {
@@ -125,7 +126,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
try {
await sendMailToTelegram(
{ env: env } as Context<HonoCustomType>,
message.to, parsedEmailContext, message_id);
toAddress, parsedEmailContext, message_id);
} catch (error) {
console.error("send mail to telegram error", error);
}
@@ -134,7 +135,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
try {
await triggerWebhook(
{ env: env } as Context<HonoCustomType>,
message.to, parsedEmailContext, message_id
toAddress, parsedEmailContext, message_id
);
} catch (error) {
console.error("send webhook error", error);
@@ -146,7 +147,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
const parsedText = parsedEmail?.text ?? ""
const rpcEmail: RPCEmailMessage = {
from: message.from,
to: message.to,
to: toAddress,
rawEmail: rawEmail,
headers: message.headers
}
@@ -156,10 +157,10 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
}
// auto reply email
await auto_reply(message, env);
await auto_reply(message, env, toAddress);
// AI email content extraction
await extractEmailInfo(parsedEmailContext, env, message_id, message.to);
await extractEmailInfo(parsedEmailContext, env, message_id, toAddress);
}
export { email }

View File

@@ -6,7 +6,7 @@ import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
import i18n from '../i18n';
import { CONSTANTS } from '../constants'
import { getJsonSetting, getDomains, getBooleanValue, getJsonObjectValue } from '../utils';
import { getJsonSetting, getDomains, getBooleanValue, getJsonObjectValue, getDomainMapValue, getMailDomain, includesDomain } from '../utils';
import { GeoData } from '../models'
import { handleListQuery, isSendMailBindingEnabled, updateAddressUpdatedAt } from '../common'
import { getSendBalanceState, requestSendMailAccess } from './send_balance';
@@ -81,7 +81,7 @@ const sendMailByResend = async (
subject: string, content: string, is_html: boolean
}
): Promise<void> => {
const mailDomain = address.split("@")[1];
const mailDomain = getMailDomain(address);
const token = c.env[
`RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}`
] || c.env.RESEND_TOKEN;
@@ -143,9 +143,9 @@ export const sendMail = async (
throw new Error(msgs.AddressNotFoundMsg)
}
// check domain
const mailDomain = address.split("@")[1];
const mailDomain = getMailDomain(address);
const domains = getDomains(c);
if (!domains.includes(mailDomain)) {
if (!includesDomain(domains, mailDomain)) {
throw new Error(msgs.InvalidDomainMsg)
}
const sendBalanceState = await getSendBalanceState(c, address, {
@@ -182,7 +182,7 @@ export const sendMail = async (
];
// send by smtp
const smtpConfigMap = getJsonObjectValue<Record<string, WorkerMailerOptions>>(c.env.SMTP_CONFIG);
const smtpConfig = smtpConfigMap ? smtpConfigMap[mailDomain] : null;
const smtpConfig = getDomainMapValue(smtpConfigMap, mailDomain);
// send by verified address list
let sendByVerifiedAddressList = false;
if (c.env.SEND_MAIL) {

View File

@@ -4,7 +4,7 @@ import { Telegraf, Context as TgContext, Markup } from "telegraf";
import { callbackQuery } from "telegraf/filters";
import { CONSTANTS } from "../constants";
import { getBooleanValue, getDomains, getJsonObjectValue, getStringValue } from '../utils';
import { getBooleanValue, getDomains, getJsonObjectValue, trimLower } from '../utils';
import { TelegramSettings } from "./settings";
import { sendTelegramAttachments } from "./tg_file_upload";
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
@@ -117,7 +117,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
bot.command("start", async (ctx: TgContext) => {
const msgs = await getTgMessages(c, ctx);
const prefix = getStringValue(c.env.PREFIX)
const prefix = trimLower(c.env.PREFIX)
const domains = getDomains(c);
const commands = getTelegramCommands(c);
return await ctx.reply(

View File

@@ -2,7 +2,7 @@ import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import i18n from '../i18n';
import { getJsonSetting, getStringValue, getUserRoles } from '../utils';
import { getJsonSetting, getMailDomain, getStringValue, getUserRoles, includesDomain } from '../utils';
import { UserOauth2Settings } from '../models';
import { CONSTANTS } from '../constants';
@@ -107,8 +107,8 @@ export default {
return c.text(msgs.Oauth2FailedGetUserEmailMsg, 400);
}
// check email in mail allow list
const mailDomain = email.split("@")[1];
if (setting.enableMailAllowList && !setting.mailAllowList?.includes(mailDomain)) {
const mailDomain = getMailDomain(email);
if (setting.enableMailAllowList && !includesDomain(setting.mailAllowList, mailDomain)) {
return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(setting.mailAllowList, null, 2)}`, 400)
}
// insert or update user

View File

@@ -2,7 +2,7 @@ import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import i18n from '../i18n';
import utils, { checkCfTurnstile, getJsonSetting, checkUserPassword, getUserRoles, getStringValue } from "../utils"
import utils, { checkCfTurnstile, getJsonSetting, checkUserPassword, getUserRoles, getStringValue, getMailDomain, includesDomain } from "../utils"
import { CONSTANTS } from "../constants";
import { GeoData, UserInfo, UserSettings } from "../models";
import { sendMail } from "../mails_api/send_mail_api";
@@ -20,10 +20,10 @@ export default {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value)
// check mail domain allow list
const mailDomain = email.split("@")[1];
const mailDomain = getMailDomain(email);
if (settings.enableMailAllowList
&& settings.mailAllowList
&& !settings.mailAllowList.includes(mailDomain)
&& !includesDomain(settings.mailAllowList, mailDomain)
) {
return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
}
@@ -95,10 +95,10 @@ export default {
return c.text(msgs.InvalidVerifyCodeMsg, 400)
}
// check mail domain allow list
const mailDomain = email.split("@")[1];
const mailDomain = getMailDomain(email);
if (settings.enableMailAllowList
&& settings.mailAllowList
&& !settings.mailAllowList.includes(mailDomain)
&& !includesDomain(settings.mailAllowList, mailDomain)
) {
return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
}

View File

@@ -137,12 +137,97 @@ export const getStringArray = (
return value;
}
export const trimLower = (
value: string | undefined | null
): string => {
return getStringValue(value).trim().toLowerCase();
}
export const normalizeDomain = (
value: string | undefined | null
): string => {
return trimLower(value);
}
export const normalizeDomains = (domains: string[]): string[] => {
return domains
.map((domain) => normalizeDomain(domain))
.filter((domain) => domain.length > 0);
}
export const getMailDomain = (
value: string | undefined | null
): string => {
const address = getStringValue(value).trim();
const atIndex = address.lastIndexOf("@");
if (atIndex < 0) {
return "";
}
return normalizeDomain(address.slice(atIndex + 1));
}
export const normalizeAddressDomain = (
value: string | undefined | null
): string => {
const address = getStringValue(value).trim();
const atIndex = address.lastIndexOf("@");
if (atIndex < 0) {
return address;
}
const localPart = address.slice(0, atIndex).trim();
const domain = normalizeDomain(address.slice(atIndex + 1));
if (!localPart || !domain) {
return address;
}
return `${localPart}@${domain}`;
}
export const includesDomain = (
domains: string[] | undefined | null,
domain: string | undefined | null
): boolean => {
const normalizedDomain = normalizeDomain(domain);
if (!normalizedDomain || !domains || domains.length === 0) {
return false;
}
return normalizeDomains(domains).includes(normalizedDomain);
}
export const isDomainOrSubdomain = (
domain: string | undefined | null,
allowDomain: string | undefined | null
): boolean => {
const normalizedDomain = normalizeDomain(domain);
const normalizedAllowDomain = normalizeDomain(allowDomain);
if (!normalizedDomain || !normalizedAllowDomain) {
return false;
}
return normalizedDomain === normalizedAllowDomain
|| normalizedDomain.endsWith(`.${normalizedAllowDomain}`);
}
export const getDomainMapValue = <T>(
valueMap: Record<string, T> | undefined | null,
domain: string | undefined | null
): T | null => {
const normalizedDomain = normalizeDomain(domain);
if (!normalizedDomain || !valueMap) {
return null;
}
for (const [key, value] of Object.entries(valueMap)) {
if (normalizeDomain(key) === normalizedDomain) {
return value;
}
}
return null;
}
export const getDefaultDomains = (c: Context<HonoCustomType>): string[] => {
if (c.env.DEFAULT_DOMAINS == undefined || c.env.DEFAULT_DOMAINS == null) {
return getDomains(c);
}
const domains = getStringArray(c.env.DEFAULT_DOMAINS);
return domains || getDomains(c);
const domains = normalizeDomains(getStringArray(c.env.DEFAULT_DOMAINS));
return domains.length > 0 ? domains : getDomains(c);
}
export const getDomains = (c: Context<HonoCustomType>): string[] => {
@@ -152,36 +237,46 @@ export const getDomains = (c: Context<HonoCustomType>): string[] => {
// check if DOMAINS is an array, if not use json.parse
if (!Array.isArray(c.env.DOMAINS)) {
try {
return JSON.parse(c.env.DOMAINS);
return normalizeDomains(JSON.parse(c.env.DOMAINS));
} catch (e) {
console.error("Failed to parse DOMAINS", e);
return [];
}
}
return c.env.DOMAINS;
return normalizeDomains(c.env.DOMAINS);
}
export const getRandomSubdomainDomains = (c: Context<HonoCustomType>): string[] => {
if (!c.env.RANDOM_SUBDOMAIN_DOMAINS) {
return [];
}
return getStringArray(c.env.RANDOM_SUBDOMAIN_DOMAINS);
return normalizeDomains(getStringArray(c.env.RANDOM_SUBDOMAIN_DOMAINS));
}
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
if (!c.env.USER_ROLES) {
return [];
}
const normalizeRoles = (roles: UserRole[]): UserRole[] => {
return roles.map((role) => ({
...role,
domains: Array.isArray(role.domains)
? normalizeDomains(role.domains)
: typeof role.domains === "string"
? normalizeDomains([role.domains])
: role.domains,
}));
};
// check if USER_ROLES is an array, if not use json.parse
if (!Array.isArray(c.env.USER_ROLES)) {
try {
return JSON.parse(c.env.USER_ROLES);
return normalizeRoles(JSON.parse(c.env.USER_ROLES));
} catch (e) {
console.error("Failed to parse USER_ROLES", e);
return [];
}
}
return c.env.USER_ROLES;
return normalizeRoles(c.env.USER_ROLES);
}
export const getAnotherWorkerList = (c: Context<HonoCustomType>): AnotherWorker[] => {
@@ -414,6 +509,13 @@ export default {
getBooleanValue,
getIntValue,
getStringArray,
trimLower,
normalizeDomain,
normalizeDomains,
getMailDomain,
normalizeAddressDomain,
includesDomain,
getDomainMapValue,
getDefaultDomains,
getDomains,
getRandomSubdomainDomains,

View File

@@ -14,7 +14,7 @@ import { api as telegramApi } from './telegram_api'
import i18n from './i18n';
import { email } from './email';
import { scheduled } from './scheduled';
import { getPasswords, getBooleanValue, getStringArray, checkIsAdmin } from './utils';
import { getPasswords, getBooleanValue, getDomains, checkIsAdmin } from './utils';
import { checkAccessControl } from './ip_blacklist';
const API_PATHS = [
@@ -270,7 +270,7 @@ const health_check = async (c: Context<HonoCustomType>) => {
if (!c.env.JWT_SECRET) {
return c.text(msgs.JWTSecretNotSetMsg, 400);
}
if (getStringArray(c.env.DOMAINS).length === 0) {
if (getDomains(c).length === 0) {
return c.text(msgs.DomainsNotSetMsg, 400);
}
return c.text("OK");