Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 9 additions & 8 deletions src/services/extensions/v2/email/mxroute.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {
EmailIdentity,
EmailMessage,
EmailSender,
EmailSendResult,
EnvReader,
FetchFn
import {
defaultFetch,
type EmailIdentity,
type EmailMessage,
type EmailSender,
type EmailSendResult,
type EnvReader,
type FetchFn
} from "./types";

const SMTP_API_URL = "https://smtpapi.mxroute.com/";
Expand Down Expand Up @@ -33,7 +34,7 @@ export function loadMxrouteConfig(
export class MxrouteSender implements EmailSender {
constructor(
private config: MxrouteConfig,
private fetchFn: FetchFn = globalThis.fetch
private fetchFn: FetchFn = defaultFetch
) {}

async send(message: EmailMessage): Promise<EmailSendResult> {
Expand Down
17 changes: 9 additions & 8 deletions src/services/extensions/v2/email/resend.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {
EmailIdentity,
EmailMessage,
EmailSender,
EmailSendResult,
EnvReader,
FetchFn
import {
defaultFetch,
type EmailIdentity,
type EmailMessage,
type EmailSender,
type EmailSendResult,
type EnvReader,
type FetchFn
} from "./types";

const RESEND_API_URL = "https://api.resend.com/emails";
Expand All @@ -29,7 +30,7 @@ export function loadResendConfig(
export class ResendSender implements EmailSender {
constructor(
private config: ResendConfig,
private fetchFn: FetchFn = globalThis.fetch
private fetchFn: FetchFn = defaultFetch
) {}

async send(message: EmailMessage): Promise<EmailSendResult> {
Expand Down
6 changes: 6 additions & 0 deletions src/services/extensions/v2/email/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export interface EmailSender {

export type FetchFn = typeof globalThis.fetch;

// Workers' fetch requires its `this` receiver: storing `globalThis.fetch`
// as a bare reference and calling it later throws "Illegal invocation".
// Keep the lookup behind an arrow so the default sender always calls it
// bound.
export const defaultFetch: FetchFn = (...args) => globalThis.fetch(...args);

// Minimal env surface the email subsystem needs. Shaped to match
// PlatformContext, so routes pass getPlatform(c) directly — the same way
// GITHUB_TOKEN and the assertion secrets are read everywhere else.
Expand Down
56 changes: 56 additions & 0 deletions test/services/extensions/v2/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,62 @@ describe("factory", () => {
});
});

describe("default fetch binding", () => {
it("sends through globalThis.fetch without an Illegal invocation error", async () => {
// Workers throws "Illegal invocation" for a detached fetch while Node
// tolerates it: enforce the Workers behaviour so the default stays bound.
const originalFetch = globalThis.fetch;
const urls: string[] = [];
function workersLikeFetch(
this: unknown,
input: string | URL | Request,
_init?: RequestInit
): Promise<Response> {
if (this !== globalThis) {
throw new TypeError(
"Illegal invocation: function called with incorrect `this` reference."
);
}
urls.push(String(input));
return Promise.resolve(jsonResponse({ success: true, message: "sent" }));
}

globalThis.fetch = workersLikeFetch as typeof fetch;
try {
const message = {
to: "author@example.com",
subject: "s",
html: "<p>hi</p>",
text: "hi"
};
const mxrouteConfig = loadMxrouteConfig(reader(MXROUTE_VARS), IDENTITY)!;
await expect(
new MxrouteSender(mxrouteConfig).send(message)
).resolves.toEqual({ ok: true });

const resendConfig = loadResendConfig(
reader({ EXTENSIONS_V2_RESEND_API_KEY: "re_key" }),
IDENTITY
)!;
await expect(
new ResendSender(resendConfig).send(message)
).resolves.toEqual({ ok: true });

// notify.ts calls createEmailSender(env) with no injected fetch.
const viaFactory = createEmailSender(reader(MXROUTE_VARS));
await expect(viaFactory.send(message)).resolves.toEqual({ ok: true });

expect(urls).toEqual([
"https://smtpapi.mxroute.com/",
"https://api.resend.com/emails",
"https://smtpapi.mxroute.com/"
]);
} finally {
globalThis.fetch = originalFetch;
}
});
});

describe("moderation templates", () => {
it("includes the reason and escapes markup", () => {
const message = buildModerationEmail({
Expand Down
Loading