Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/email-platform-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": patch
---

Expose `send_email` bindings from `getPlatformProxy()`

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.
42 changes: 37 additions & 5 deletions packages/miniflare/src/plugins/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { mkdir } from "node:fs/promises";
import path from "node:path";
import EMAIL_MESSAGE from "worker:email/email";
import SEND_EMAIL_BINDING from "worker:email/send_email";
import { z } from "zod";
import {
getUserBindingServiceName,
ProxyNodeBinding,
remoteProxyClientWorker,
WORKER_BINDING_SERVICE_LOOPBACK,
} from "../shared";
import type { Service, Worker_Binding } from "../../runtime";
import type { Plugin, RemoteProxyConnectionString } from "../shared";
Expand Down Expand Up @@ -41,6 +43,8 @@ export const EmailOptionsSchema = z.object({

export const EMAIL_PLUGIN_NAME = "email";
const SERVICE_SEND_EMAIL_WORKER_PREFIX = `SEND-EMAIL-WORKER`;
const EMAIL_DISK_SERVICE_NAME = `${EMAIL_PLUGIN_NAME}:disk`;
const EMAIL_DISK_BINDING_NAME = "MINIFLARE_EMAIL_DISK";

function buildJsonBindings(bindings: Record<string, any>): Worker_Binding[] {
return Object.entries(bindings).map(([name, value]) => ({
Expand Down Expand Up @@ -68,11 +72,32 @@ export const EMAIL_PLUGIN: Plugin<typeof EmailOptionsSchema> = {
},
}));
},
getNodeBindings(_options) {
return {};
getNodeBindings(options) {
if (!options.email?.send_email) {
return {};
}

return Object.fromEntries(
options.email.send_email.map(({ name }) => [name, new ProxyNodeBinding()])
);
},
async getServices(args) {
const services: Service[] = [];
if (!args.options.email?.send_email) {
return [];
}

const emailDirectory = path.join(args.tmpPath, EMAIL_PLUGIN_NAME);
Comment thread
edmundhung marked this conversation as resolved.
await mkdir(emailDirectory, { recursive: true });

const services: Service[] = [
{
name: EMAIL_DISK_SERVICE_NAME,
disk: {
path: emailDirectory,
writable: true,
},
},
];

for (const { name, remoteProxyConnectionString, ...config } of args.options
.email?.send_email ?? []) {
Expand All @@ -90,7 +115,14 @@ export const EMAIL_PLUGIN: Plugin<typeof EmailOptionsSchema> = {
],
bindings: [
...buildJsonBindings(config),
WORKER_BINDING_SERVICE_LOOPBACK,
{
name: EMAIL_DISK_BINDING_NAME,
service: { name: EMAIL_DISK_SERVICE_NAME },
},
{
name: "email_directory",
json: JSON.stringify(emailDirectory),
},
],
},
});
Expand Down
37 changes: 13 additions & 24 deletions packages/miniflare/src/workers/email/send_email.worker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { WorkerEntrypoint } from "cloudflare:workers";
import { blue } from "kleur/colors";
import { LogLevel, SharedHeaders } from "miniflare:shared";
import PostalMime from "postal-mime";
import { CoreBindings } from "../core/constants";
import { RAW_EMAIL } from "./constants";
import { type MiniflareEmailMessage as EmailMessage } from "./email.worker";
import type { EmailAddress, MessageBuilder } from "./types";
Expand Down Expand Up @@ -67,30 +65,22 @@ function formatMessageBuilder(builder: MessageBuilder): string {
}

interface SendEmailEnv {
[CoreBindings.SERVICE_LOOPBACK]: Fetcher;
MINIFLARE_EMAIL_DISK: Fetcher;
email_directory: string;
destination_address: string | undefined;
allowed_destination_addresses: string[] | undefined;
allowed_sender_addresses: string[] | undefined;
}

export class SendEmailBinding extends WorkerEntrypoint<SendEmailEnv> {
/**
* Logs a message via the loopback service
* Logs a message via the runtime console.
*/
private log(message: string, level: LogLevel = LogLevel.INFO): void {
this.ctx.waitUntil(
this.env[CoreBindings.SERVICE_LOOPBACK].fetch(
"http://localhost/core/log",
{
method: "POST",
headers: { [SharedHeaders.LOG_LEVEL]: level.toString() },
body: message,
}
)
);
private log(message: string): void {
console.log(message);
}
/**
* Stores content to a temporary file via the loopback service
* Stores content to a temporary file via the disk service.
*/
private async storeTempFile(
content: string | ArrayBuffer | ArrayBufferView,
Expand All @@ -111,14 +101,13 @@ export class SendEmailBinding extends WorkerEntrypoint<SendEmailEnv> {
);
}

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

private checkDestinationAllowed(to: string) {
Expand Down
Loading
Loading