Skip to content

Commit 6caf938

Browse files
authored
Add new attribute allowed_sender_addresses (#10651)
Added new attribute called "allowed_sender_addresses" to send_email binding. This allows the binding to be restricted in the possible values for the sender of emails.
1 parent a4e2439 commit 6caf938

File tree

13 files changed

+147
-9
lines changed

13 files changed

+147
-9
lines changed

.changeset/soft-flies-search.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"miniflare": minor
3+
"wrangler": minor
4+
---
5+
6+
Added new attribute "allowed_sender_addresses" to send email binding.

packages/miniflare/src/plugins/email/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const EmailBindingOptionsSchema = z
1717
remoteProxyConnectionString: z
1818
.custom<RemoteProxyConnectionString>()
1919
.optional(),
20+
allowed_sender_addresses: z.array(z.string()).optional(),
2021
})
2122
.and(
2223
z.union([

packages/miniflare/src/workers/email/send_email.worker.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface SendEmailEnv {
1010
[CoreBindings.SERVICE_LOOPBACK]: Fetcher;
1111
destination_address: string | undefined;
1212
allowed_destination_addresses: string[] | undefined;
13+
allowed_sender_addresses: string[] | undefined;
1314
}
1415

1516
export class SendEmailBinding extends WorkerEntrypoint<SendEmailEnv> {
@@ -28,8 +29,17 @@ export class SendEmailBinding extends WorkerEntrypoint<SendEmailEnv> {
2829
throw new Error(`email to ${to} not allowed`);
2930
}
3031
}
32+
private checkSenderAllowed(from: string) {
33+
if (
34+
this.env.allowed_sender_addresses !== undefined &&
35+
!this.env.allowed_sender_addresses.includes(from)
36+
) {
37+
throw new Error(`email from ${from} not allowed`);
38+
}
39+
}
3140
async send(emailMessage: EmailMessage): Promise<void> {
3241
this.checkDestinationAllowed(emailMessage.to);
42+
this.checkSenderAllowed(emailMessage.from);
3343

3444
const rawEmail: ReadableStream<Uint8Array> = emailMessage[RAW_EMAIL];
3545

packages/miniflare/test/plugins/email/index.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,96 @@ This is a random email body.
263263
t.is(res.status, 200);
264264
});
265265

266+
test("Multiple allowed senders send_email binding works", async (t) => {
267+
const mf = new Miniflare({
268+
modules: true,
269+
script: SEND_EMAIL_WORKER,
270+
email: {
271+
send_email: [
272+
{
273+
name: "SEND_EMAIL",
274+
allowed_sender_addresses: [
275+
276+
277+
],
278+
},
279+
],
280+
},
281+
compatibilityDate: "2025-03-17",
282+
});
283+
284+
t.teardown(() => mf.dispose());
285+
286+
const res = await mf.dispatchFetch(
287+
"http://localhost/?" +
288+
new URLSearchParams({
289+
290+
291+
}).toString(),
292+
{
293+
body: `To: someone <[email protected]>
294+
From: someone else <[email protected]>
295+
Message-ID: <[email protected]>
296+
MIME-Version: 1.0
297+
Content-Type: text/plain
298+
299+
This is a random email body.
300+
`,
301+
method: "POST",
302+
}
303+
);
304+
305+
t.is(await res.text(), "ok");
306+
t.is(res.status, 200);
307+
});
308+
309+
test("Sending email from a sender not in the allowed list does not work", async (t) => {
310+
const mf = new Miniflare({
311+
modules: true,
312+
script: SEND_EMAIL_WORKER,
313+
email: {
314+
send_email: [
315+
{
316+
name: "SEND_EMAIL",
317+
allowed_sender_addresses: [
318+
319+
320+
],
321+
},
322+
],
323+
},
324+
compatibilityDate: "2025-03-17",
325+
});
326+
327+
t.teardown(() => mf.dispose());
328+
329+
const res = await mf.dispatchFetch(
330+
"http://localhost/?" +
331+
new URLSearchParams({
332+
333+
334+
}).toString(),
335+
{
336+
body: `To: someone <[email protected]>
337+
From: someone else <[email protected]>
338+
Message-ID: <[email protected]>
339+
MIME-Version: 1.0
340+
Content-Type: text/plain
341+
342+
This is a random email body.
343+
`,
344+
method: "POST",
345+
}
346+
);
347+
348+
t.true(
349+
(await res.text()).startsWith(
350+
"Error: email from [email protected] not allowed"
351+
)
352+
);
353+
t.is(res.status, 500);
354+
});
355+
266356
test("Multiple allowed send_email binding throws if destination is not equal", async (t) => {
267357
const mf = new Miniflare({
268358
modules: true,

packages/wrangler/src/__tests__/init.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ describe("init", () => {
300300
name: "EMAIL_BINDING",
301301
destination_address: "[email protected]",
302302
allowed_destination_addresses: ["[email protected]"],
303+
allowed_sender_addresses: ["[email protected]"],
303304
},
304305
{
305306
type: "version_metadata",
@@ -474,6 +475,7 @@ describe("init", () => {
474475
],
475476
send_email: [
476477
{
478+
allowed_sender_addresses: ["[email protected]"],
477479
allowed_destination_addresses: ["[email protected]"],
478480
destination_address: "[email protected]",
479481
name: "EMAIL_BINDING",
@@ -1004,6 +1006,7 @@ describe("init", () => {
10041006
name = \\"EMAIL_BINDING\\"
10051007
destination_address = \\"[email protected]\\"
10061008
allowed_destination_addresses = [ \\"[email protected]\\" ]
1009+
allowed_sender_addresses = [ \\"[email protected]\\" ]
10071010
10081011
[version_metadata]
10091012
binding = \\"Version_BINDING\\"

packages/wrangler/src/__tests__/versions/versions.view.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ describe("versions view", () => {
618618
name: "MAIL_3",
619619
destination_address: "[email protected]",
620620
allowed_destination_addresses: ["[email protected]", "[email protected]"],
621+
allowed_sender_addresses: ["[email protected]", "[email protected]"],
621622
},
622623
{ type: "service", name: "SERVICE", service: "worker" },
623624
{
@@ -740,6 +741,7 @@ describe("versions view", () => {
740741
name = \\"MAIL_3\\"
741742
destination_address = \\"[email protected]\\"
742743
allowed_destination_addresses = [\\"[email protected]\\", \\"[email protected]\\"]
744+
allowed_sender_addresses = [\\"[email protected]\\", \\"[email protected]\\"]
743745
744746
[[services]]
745747
binding = \\"SERVICE\\"

packages/wrangler/src/config/environment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,8 @@ export interface EnvironmentNonInheritable {
723723
destination_address?: string;
724724
/** If this binding should be restricted to a set of verified addresses */
725725
allowed_destination_addresses?: string[];
726+
/** If this binding should be restricted to a set of sender addresses */
727+
allowed_sender_addresses?: string[];
726728
/** Whether the binding should be remote or not */
727729
remote?: boolean;
728730
}[];

packages/wrangler/src/config/validation.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,6 +2956,14 @@ const validateSendEmailBinding: ValidatorFn = (diagnostics, field, value) => {
29562956
);
29572957
isValid = false;
29582958
}
2959+
if (!isOptionalProperty(value, "allowed_sender_addresses", "object")) {
2960+
diagnostics.errors.push(
2961+
`"${field}" bindings should, optionally, have a []string "allowed_sender_addresses" field but got ${JSON.stringify(
2962+
value
2963+
)}.`
2964+
);
2965+
isValid = false;
2966+
}
29592967
if (
29602968
"destination_address" in value &&
29612969
"allowed_destination_addresses" in value
@@ -2971,6 +2979,7 @@ const validateSendEmailBinding: ValidatorFn = (diagnostics, field, value) => {
29712979
}
29722980

29732981
validateAdditionalProperties(diagnostics, field, Object.keys(value), [
2982+
"allowed_sender_addresses",
29742983
"allowed_destination_addresses",
29752984
"destination_address",
29762985
"name",

packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type WorkerMetadataBinding =
7272
name: string;
7373
destination_address?: string;
7474
allowed_destination_addresses?: string[];
75+
allowed_sender_addresses?: string[];
7576
}
7677
| {
7778
type: "durable_object_namespace";
@@ -301,11 +302,16 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
301302
"allowed_destination_addresses" in emailBinding
302303
? emailBinding.allowed_destination_addresses
303304
: undefined;
305+
const allowed_sender_addresses =
306+
"allowed_sender_addresses" in emailBinding
307+
? emailBinding.allowed_sender_addresses
308+
: undefined;
304309
metadataBindings.push({
305310
name: emailBinding.name,
306311
type: "send_email",
307312
destination_address,
308313
allowed_destination_addresses,
314+
allowed_sender_addresses,
309315
});
310316
});
311317

packages/wrangler/src/deployment-bundle/worker.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,8 @@ export type CfSendEmailBindings = {
9595
} & (
9696
| { destination_address?: string }
9797
| { allowed_destination_addresses?: string[] }
98+
| { allowed_sender_addresses?: string[] }
9899
);
99-
// export interface CfSendEmailBindings {
100-
// name: string;
101-
// destination_address?: string | undefined;
102-
// allowed_destination_addresses?: string[] | undefined;
103-
// }
104100

105101
/**
106102
* A binding to a wasm module (in service-worker format)

0 commit comments

Comments
 (0)