Skip to content

Commit e653edf

Browse files
fix(miniflare): expose send_email in platform proxy (#13723)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 0827815 commit e653edf

6 files changed

Lines changed: 225 additions & 31 deletions

File tree

.changeset/email-platform-proxy.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Expose `send_email` bindings from `getPlatformProxy()`
6+
7+
Projects developing in Node can now access `send_email` bindings from the platform proxy. This supports the plain-object MessageBuilder API locally, so calls like `env.EMAIL.send({ from, to, subject, text })` no longer fail because the binding is missing.

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

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { mkdir } from "node:fs/promises";
2+
import path from "node:path";
13
import EMAIL_MESSAGE from "worker:email/email";
24
import SEND_EMAIL_BINDING from "worker:email/send_email";
35
import { z } from "zod";
46
import {
57
getUserBindingServiceName,
68
remoteProxyClientWorker,
7-
WORKER_BINDING_SERVICE_LOOPBACK,
9+
ProxyNodeBinding,
810
} from "../shared";
911
import type { Service, Worker_Binding } from "../../runtime";
1012
import type { Plugin, RemoteProxyConnectionString } from "../shared";
@@ -41,6 +43,8 @@ export const EmailOptionsSchema = z.object({
4143

4244
export const EMAIL_PLUGIN_NAME = "email";
4345
const SERVICE_SEND_EMAIL_WORKER_PREFIX = `SEND-EMAIL-WORKER`;
46+
const EMAIL_DISK_SERVICE_NAME = `${EMAIL_PLUGIN_NAME}:disk`;
47+
const EMAIL_DISK_BINDING_NAME = "MINIFLARE_EMAIL_DISK";
4448

4549
function buildJsonBindings(bindings: Record<string, any>): Worker_Binding[] {
4650
return Object.entries(bindings).map(([name, value]) => ({
@@ -68,11 +72,32 @@ export const EMAIL_PLUGIN: Plugin<typeof EmailOptionsSchema> = {
6872
},
6973
}));
7074
},
71-
getNodeBindings(_options) {
72-
return {};
75+
getNodeBindings(options) {
76+
if (!options.email?.send_email) {
77+
return {};
78+
}
79+
80+
return Object.fromEntries(
81+
options.email.send_email.map(({ name }) => [name, new ProxyNodeBinding()])
82+
);
7383
},
7484
async getServices(args) {
75-
const services: Service[] = [];
85+
if (!args.options.email?.send_email) {
86+
return [];
87+
}
88+
89+
const emailDirectory = path.join(args.tmpPath, EMAIL_PLUGIN_NAME);
90+
await mkdir(emailDirectory, { recursive: true });
91+
92+
const services: Service[] = [
93+
{
94+
name: EMAIL_DISK_SERVICE_NAME,
95+
disk: {
96+
path: emailDirectory,
97+
writable: true,
98+
},
99+
},
100+
];
76101

77102
for (const { name, remoteProxyConnectionString, ...config } of args.options
78103
.email?.send_email ?? []) {
@@ -90,7 +115,14 @@ export const EMAIL_PLUGIN: Plugin<typeof EmailOptionsSchema> = {
90115
],
91116
bindings: [
92117
...buildJsonBindings(config),
93-
WORKER_BINDING_SERVICE_LOOPBACK,
118+
{
119+
name: EMAIL_DISK_BINDING_NAME,
120+
service: { name: EMAIL_DISK_SERVICE_NAME },
121+
},
122+
{
123+
name: "email_directory",
124+
json: JSON.stringify(emailDirectory),
125+
},
94126
],
95127
},
96128
});

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { WorkerEntrypoint } from "cloudflare:workers";
22
import { blue } from "kleur/colors";
3-
import { LogLevel, SharedHeaders } from "miniflare:shared";
43
import PostalMime from "postal-mime";
5-
import { CoreBindings } from "../core/constants";
64
import { RAW_EMAIL } from "./constants";
75
import { type MiniflareEmailMessage as EmailMessage } from "./email.worker";
86
import type { EmailAddress, MessageBuilder } from "./types";
@@ -66,31 +64,33 @@ function formatMessageBuilder(builder: MessageBuilder): string {
6664
return lines.join("\n");
6765
}
6866

67+
/**
68+
* Appends path segments to a base path using the separator already implied by
69+
* the base path string. This trims trailing `/` and `\` from the base before
70+
* joining, but does not otherwise normalize the full path.
71+
*/
72+
function joinPath(base: string, ...segments: string[]): string {
73+
const separator = base.includes("\\") ? "\\" : "/";
74+
return [base.replace(/[\\/]+$/, ""), ...segments].join(separator);
75+
}
76+
6977
interface SendEmailEnv {
70-
[CoreBindings.SERVICE_LOOPBACK]: Fetcher;
78+
MINIFLARE_EMAIL_DISK: Fetcher;
79+
email_directory: string;
7180
destination_address: string | undefined;
7281
allowed_destination_addresses: string[] | undefined;
7382
allowed_sender_addresses: string[] | undefined;
7483
}
7584

7685
export class SendEmailBinding extends WorkerEntrypoint<SendEmailEnv> {
7786
/**
78-
* Logs a message via the loopback service
87+
* Logs a message via the runtime console.
7988
*/
80-
private log(message: string, level: LogLevel = LogLevel.INFO): void {
81-
this.ctx.waitUntil(
82-
this.env[CoreBindings.SERVICE_LOOPBACK].fetch(
83-
"http://localhost/core/log",
84-
{
85-
method: "POST",
86-
headers: { [SharedHeaders.LOG_LEVEL]: level.toString() },
87-
body: message,
88-
}
89-
)
90-
);
89+
private log(message: string): void {
90+
console.log(message);
9191
}
9292
/**
93-
* Stores content to a temporary file via the loopback service
93+
* Stores content to a temporary file via the disk service.
9494
*/
9595
private async storeTempFile(
9696
content: string | ArrayBuffer | ArrayBufferView,
@@ -111,14 +111,14 @@ export class SendEmailBinding extends WorkerEntrypoint<SendEmailEnv> {
111111
);
112112
}
113113

114-
const resp = await this.env[CoreBindings.SERVICE_LOOPBACK].fetch(
115-
`http://localhost/core/store-temp-file?extension=${extension}&prefix=${prefix}`,
116-
{
117-
method: "POST",
118-
body,
119-
}
120-
);
121-
return await resp.text();
114+
const fileName = `${crypto.randomUUID()}.${extension}`;
115+
const url = new URL(`${prefix}/${fileName}`, "http://placeholder/");
116+
await this.env.MINIFLARE_EMAIL_DISK.fetch(url, {
117+
method: "PUT",
118+
body,
119+
});
120+
121+
return joinPath(this.env.email_directory, prefix, fileName);
122122
}
123123

124124
private checkDestinationAllowed(to: string) {

0 commit comments

Comments
 (0)