mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-28 19:48:01 +08:00
fix: limit SEND_MAIL domain checks to binding paths (#987)
* fix: scope SEND_MAIL domain gating to binding * test: cover SEND_MAIL domain gating in e2e
This commit is contained in:
@@ -72,6 +72,24 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
|
worker-send-mail-domain:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: e2e/Dockerfile.worker
|
||||||
|
args:
|
||||||
|
WRANGLER_TOML: e2e/fixtures/wrangler.toml.e2e.send-mail-domain
|
||||||
|
ports:
|
||||||
|
- "8791:8791"
|
||||||
|
command: ["pnpm", "exec", "wrangler", "dev", "--port", "8791", "--ip", "0.0.0.0"]
|
||||||
|
depends_on:
|
||||||
|
- mailpit
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-sf", "http://localhost:8791/health_check"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
@@ -128,6 +146,7 @@ services:
|
|||||||
WORKER_URL_SUBDOMAIN: http://worker-subdomain:8789
|
WORKER_URL_SUBDOMAIN: http://worker-subdomain:8789
|
||||||
WORKER_URL_ENV_OFF: http://worker-env-off:8790
|
WORKER_URL_ENV_OFF: http://worker-env-off:8790
|
||||||
WORKER_GZIP_URL: http://worker-gzip:8788
|
WORKER_GZIP_URL: http://worker-gzip:8788
|
||||||
|
WORKER_URL_SEND_MAIL_DOMAIN: http://worker-send-mail-domain:8791
|
||||||
FRONTEND_URL: https://frontend:5173
|
FRONTEND_URL: https://frontend:5173
|
||||||
MAILPIT_API: http://mailpit:8025/api
|
MAILPIT_API: http://mailpit:8025/api
|
||||||
SMTP_PROXY_HOST: smtp-proxy
|
SMTP_PROXY_HOST: smtp-proxy
|
||||||
@@ -146,6 +165,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
worker-gzip:
|
worker-gzip:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
worker-send-mail-domain:
|
||||||
|
condition: service_healthy
|
||||||
frontend:
|
frontend:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
smtp-proxy:
|
smtp-proxy:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const WORKER_URL = process.env.WORKER_URL!;
|
|||||||
export const WORKER_URL_SUBDOMAIN = process.env.WORKER_URL_SUBDOMAIN || '';
|
export const WORKER_URL_SUBDOMAIN = process.env.WORKER_URL_SUBDOMAIN || '';
|
||||||
export const WORKER_URL_ENV_OFF = process.env.WORKER_URL_ENV_OFF || '';
|
export const WORKER_URL_ENV_OFF = process.env.WORKER_URL_ENV_OFF || '';
|
||||||
export const WORKER_GZIP_URL = process.env.WORKER_GZIP_URL || '';
|
export const WORKER_GZIP_URL = process.env.WORKER_GZIP_URL || '';
|
||||||
|
export const WORKER_URL_SEND_MAIL_DOMAIN = process.env.WORKER_URL_SEND_MAIL_DOMAIN || '';
|
||||||
export const FRONTEND_URL = process.env.FRONTEND_URL!;
|
export const FRONTEND_URL = process.env.FRONTEND_URL!;
|
||||||
export const MAILPIT_API = process.env.MAILPIT_API!;
|
export const MAILPIT_API = process.env.MAILPIT_API!;
|
||||||
export const TEST_DOMAIN = 'test.example.com';
|
export const TEST_DOMAIN = 'test.example.com';
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name = "cloudflare_temp_email"
|
||||||
|
main = "src/worker.ts"
|
||||||
|
compatibility_date = "2025-04-01"
|
||||||
|
compatibility_flags = [ "nodejs_compat" ]
|
||||||
|
keep_vars = true
|
||||||
|
|
||||||
|
send_email = [
|
||||||
|
{ name = "SEND_MAIL" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
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
|
||||||
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
|
ENABLE_AUTO_REPLY = true
|
||||||
|
DEFAULT_SEND_BALANCE = 10
|
||||||
|
ENABLE_ADDRESS_PASSWORD = true
|
||||||
|
DISABLE_ADMIN_PASSWORD_CHECK = true
|
||||||
|
ADMIN_PASSWORDS = '["e2e-admin-pass"]'
|
||||||
|
ENABLE_WEBHOOK = true
|
||||||
|
E2E_TEST_MODE = true
|
||||||
|
SMTP_CONFIG = """
|
||||||
|
{"test.example.com":{"host":"mailpit","port":1025,"secure":false}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[kv_namespaces]]
|
||||||
|
binding = "KV"
|
||||||
|
id = "e2e-test-kv-00000000-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "e2e-temp-email"
|
||||||
|
database_id = "e2e-test-db-00000000-0000-0000-0000-000000000000"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { test, expect, APIRequestContext } from '@playwright/test';
|
import { test, expect, APIRequestContext } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
WORKER_URL,
|
WORKER_URL,
|
||||||
|
WORKER_URL_SEND_MAIL_DOMAIN,
|
||||||
createTestAddress,
|
createTestAddress,
|
||||||
deleteAddress,
|
deleteAddress,
|
||||||
deleteAllMailpitMessages,
|
deleteAllMailpitMessages,
|
||||||
@@ -424,6 +425,34 @@ test.describe('Send Mail Limit', () => {
|
|||||||
expect(res.status()).toBe(400);
|
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 }) => {
|
test('daily and monthly counters both increment on successful send', async ({ request }) => {
|
||||||
const { jwt } = await createTestAddress(request, 'limit-both-inc');
|
const { jwt } = await createTestAddress(request, 'limit-both-inc');
|
||||||
await requestSendAccess(request, jwt);
|
await requestSendAccess(request, jwt);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Context } from "hono";
|
import { Context } from "hono";
|
||||||
|
import { isSendMailBindingEnabled } from "../common";
|
||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
import { sendMail } from "../mails_api/send_mail_api";
|
import { sendMail } from "../mails_api/send_mail_api";
|
||||||
import { ensureSendMailLimit, increaseSendMailLimitCount } from "../mails_api/send_mail_limit_utils";
|
import { ensureSendMailLimit, increaseSendMailLimitCount } from "../mails_api/send_mail_limit_utils";
|
||||||
@@ -66,6 +67,16 @@ export const sendMailByBindingAdmin = async (c: Context<HonoCustomType>) => {
|
|||||||
if (!from || !to || !subject || (!html && !text)) {
|
if (!from || !to || !subject || (!html && !text)) {
|
||||||
return c.text(msgs.InvalidInputMsg, 400)
|
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;
|
||||||
|
if (!mailDomain) {
|
||||||
|
return c.text(msgs.InvalidInputMsg, 400)
|
||||||
|
}
|
||||||
|
if (!isSendMailBindingEnabled(c, mailDomain)) {
|
||||||
|
return c.text(msgs.EnableSendMailForDomainMsg, 400)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await ensureSendMailLimit(c);
|
await ensureSendMailLimit(c);
|
||||||
await c.env.SEND_MAIL.send({
|
await c.env.SEND_MAIL.send({
|
||||||
|
|||||||
+17
-2
@@ -2,7 +2,7 @@ import { Context } from 'hono';
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
import { WorkerMailerOptions } from 'worker-mailer';
|
import { WorkerMailerOptions } from 'worker-mailer';
|
||||||
|
|
||||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains } from './utils';
|
import { getBooleanValue, getDomains, getStringArray, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword, getJsonObjectValue, getRandomSubdomainDomains } from './utils';
|
||||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||||
import { CONSTANTS } from './constants';
|
import { CONSTANTS } from './constants';
|
||||||
import { AddressCreationSettings, AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
import { AddressCreationSettings, AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||||
@@ -44,11 +44,26 @@ export const isSendMailEnabled = (
|
|||||||
if (smtpConfigMap && smtpConfigMap[mailDomain]) return true;
|
if (smtpConfigMap && smtpConfigMap[mailDomain]) return true;
|
||||||
|
|
||||||
// Check SEND_MAIL binding
|
// Check SEND_MAIL binding
|
||||||
if (c.env.SEND_MAIL) return true;
|
if (isSendMailBindingEnabled(c, mailDomain)) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isSendMailBindingEnabled = (
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
mailDomain: string
|
||||||
|
): boolean => {
|
||||||
|
if (!c.env.SEND_MAIL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const sendMailDomains = getStringArray(c.env.SEND_MAIL_DOMAINS)
|
||||||
|
.map((domain) => normalizeDomainValue(domain));
|
||||||
|
if (sendMailDomains.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return sendMailDomains.includes(normalizeDomainValue(mailDomain));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if send mail is enabled for any configured domain
|
* Check if send mail is enabled for any configured domain
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const messages: LocaleMessages = {
|
|||||||
InvalidAddressIdMsg: "Invalid address_id",
|
InvalidAddressIdMsg: "Invalid address_id",
|
||||||
EnableKVMsg: "Please enable KV first",
|
EnableKVMsg: "Please enable KV first",
|
||||||
EnableSendMailMsg: "Please enable SEND_MAIL first",
|
EnableSendMailMsg: "Please enable SEND_MAIL 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",
|
||||||
EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify",
|
EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify",
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export type LocaleMessages = {
|
|||||||
InvalidAddressIdMsg: string
|
InvalidAddressIdMsg: string
|
||||||
EnableKVMsg: string
|
EnableKVMsg: string
|
||||||
EnableSendMailMsg: string
|
EnableSendMailMsg: string
|
||||||
|
EnableSendMailForDomainMsg: string
|
||||||
InvalidCleanupConfigMsg: string
|
InvalidCleanupConfigMsg: string
|
||||||
InvalidCleanTypeMsg: string
|
InvalidCleanTypeMsg: string
|
||||||
EnableKVForMailVerifyMsg: string
|
EnableKVForMailVerifyMsg: string
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const messages: LocaleMessages = {
|
|||||||
InvalidAddressIdMsg: "无效的 address_id",
|
InvalidAddressIdMsg: "无效的 address_id",
|
||||||
EnableKVMsg: "请先启用 KV",
|
EnableKVMsg: "请先启用 KV",
|
||||||
EnableSendMailMsg: "请先启用 SEND_MAIL",
|
EnableSendMailMsg: "请先启用 SEND_MAIL",
|
||||||
|
EnableSendMailForDomainMsg: "请先为此域名启用 SEND_MAIL",
|
||||||
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
|
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
|
||||||
InvalidCleanTypeMsg: "无效的 cleanType",
|
InvalidCleanTypeMsg: "无效的 cleanType",
|
||||||
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
|
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
getJsonSetting, getDomains, getIntValue, getBooleanValue, getJsonObjectValue, getSplitStringListValue
|
getJsonSetting, getDomains, getIntValue, getBooleanValue, getJsonObjectValue, getSplitStringListValue
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { GeoData } from '../models'
|
import { GeoData } from '../models'
|
||||||
import { handleListQuery, updateAddressUpdatedAt } from '../common'
|
import { handleListQuery, isSendMailBindingEnabled, updateAddressUpdatedAt } from '../common'
|
||||||
import { ensureSendMailLimit, increaseSendMailLimitCount } from './send_mail_limit_utils';
|
import { ensureSendMailLimit, increaseSendMailLimitCount } from './send_mail_limit_utils';
|
||||||
|
|
||||||
|
|
||||||
@@ -213,6 +213,7 @@ export const sendMail = async (
|
|||||||
sendByVerifiedAddressList = true;
|
sendByVerifiedAddressList = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const sendMailBindingEnabled = isSendMailBindingEnabled(c, mailDomain);
|
||||||
|
|
||||||
// send mail workflow
|
// send mail workflow
|
||||||
if (sendByVerifiedAddressList) {
|
if (sendByVerifiedAddressList) {
|
||||||
@@ -225,7 +226,7 @@ export const sendMail = async (
|
|||||||
else if (smtpConfig) {
|
else if (smtpConfig) {
|
||||||
await sendMailBySmtp(c, address, reqJson, smtpConfig);
|
await sendMailBySmtp(c, address, reqJson, smtpConfig);
|
||||||
}
|
}
|
||||||
else if (c.env.SEND_MAIL) {
|
else if (sendMailBindingEnabled) {
|
||||||
await sendMailByBinding(c, address, reqJson);
|
await sendMailByBinding(c, address, reqJson);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
Vendored
+1
@@ -83,6 +83,7 @@ type Bindings = {
|
|||||||
|
|
||||||
// SMTP config
|
// SMTP config
|
||||||
SMTP_CONFIG: string | object | undefined
|
SMTP_CONFIG: string | object | undefined
|
||||||
|
SEND_MAIL_DOMAINS: string | string[] | undefined
|
||||||
|
|
||||||
// telegram config
|
// telegram config
|
||||||
TELEGRAM_BOT_TOKEN: string
|
TELEGRAM_BOT_TOKEN: string
|
||||||
|
|||||||
Reference in New Issue
Block a user