Sostituisce Resend con Cloudflare Email Service.

Usa il binding EMAIL per contatti e newsletter, eliminando la dipendenza da Resend e dalle relative API key.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Javaxman
2026-07-29 09:54:12 +02:00
parent 2b470c778a
commit 0743521d48
9 changed files with 97 additions and 93 deletions

45
src/lib/email.ts Normal file
View File

@ -0,0 +1,45 @@
/**
* Outbound email via Cloudflare Email Service (`send_email` binding).
*/
export type RuntimeEnv = Record<string, unknown>;
type EmailBinding = {
send: (message: {
to: string;
from: string;
subject: string;
text?: string;
html?: string;
replyTo?: string;
}) => Promise<{ messageId: string }>;
};
export function getEmailBinding(runtimeEnv: RuntimeEnv): EmailBinding | null {
const binding = runtimeEnv.EMAIL;
if (binding && typeof binding === 'object' && 'send' in binding) {
return binding as EmailBinding;
}
return null;
}
export async function sendCloudflareEmail(
email: EmailBinding,
options: {
fromEmail: string;
toEmail: string;
subject: string;
text: string;
html?: string;
replyTo?: string;
},
): Promise<void> {
await email.send({
from: options.fromEmail,
to: options.toEmail,
subject: options.subject,
text: options.text,
...(options.html ? { html: options.html } : {}),
...(options.replyTo ? { replyTo: options.replyTo } : {}),
});
}

View File

@ -100,34 +100,7 @@ export async function verifyTurnstile(
return payload.success === true;
}
export async function sendWithResend(options: {
apiKey: string;
fromEmail: string;
toEmail: string;
subject: string;
text: string;
html?: string;
}): Promise<void> {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${options.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: options.fromEmail,
to: [options.toEmail],
subject: options.subject,
text: options.text,
...(options.html ? { html: options.html } : {}),
}),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Resend error ${response.status}: ${detail}`);
}
}
export { getEmailBinding, sendCloudflareEmail } from './email';
export function siteOrigin(requestUrl: URL, runtimeEnv: RuntimeEnv): string {
const configured = readEnv('PUBLIC_SITE_URL', runtimeEnv) || readEnv('SITE', runtimeEnv);

View File

@ -1,4 +1,5 @@
import type { APIRoute } from 'astro';
import { getEmailBinding, sendCloudflareEmail } from '../../lib/email';
export const prerender = false;
@ -58,27 +59,6 @@ async function verifyTurnstile(token: string, secret: string, ip?: string | null
return payload.success === true;
}
async function sendWithResend(apiKey: string, fromEmail: string, toEmail: string, subject: string, text: string) {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: fromEmail,
to: [toEmail],
subject,
text,
}),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Resend error ${response.status}: ${detail}`);
}
}
function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string {
const normalized = department.trim().toLowerCase();
const map: Record<string, string> = {
@ -92,7 +72,6 @@ function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: st
export const POST: APIRoute = async ({ request, locals, url }) => {
const runtimeEnv = getRuntimeEnv(locals);
const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev';
const resendApiKey = readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
const turnstileSecret = readEnv('CONTACT_TURNSTILE_SECRET_KEY', runtimeEnv);
@ -141,13 +120,23 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
.join('\n');
if (mode === 'live') {
if (!resendApiKey || !fromEmail || !toEmail) {
return new Response('Live mode requires CONTACT_FORM_RESEND_API_KEY, CONTACT_FORM_FROM_EMAIL and CONTACT_FORM_TO_EMAIL.', {
status: 500,
});
const emailBinding = getEmailBinding(runtimeEnv);
if (!emailBinding || !fromEmail || !toEmail) {
return new Response(
'Live mode requires EMAIL binding (Cloudflare Email Service), CONTACT_FORM_FROM_EMAIL and CONTACT_FORM_TO_EMAIL.',
{
status: 500,
},
);
}
const destination = resolveDepartmentRecipient(runtimeEnv, toEmail, department);
await sendWithResend(resendApiKey, fromEmail, destination, subject, text);
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: destination,
subject,
text,
replyTo: email,
});
} else {
console.info('[contact:dev] Contact payload received', {
name,

View File

@ -1,10 +1,11 @@
import type { APIRoute } from 'astro';
import {
absoluteUrl,
getEmailBinding,
getNewsletterDb,
getRuntimeEnv,
readEnv,
sendWithResend,
sendCloudflareEmail,
siteOrigin,
type NewsletterSubscriber,
} from '../../../../lib/newsletter';
@ -73,15 +74,13 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
});
}
const resendApiKey =
readEnv('NEWSLETTER_RESEND_API_KEY', runtimeEnv) ||
readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
const emailBinding = getEmailBinding(runtimeEnv);
const fromEmail =
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
if (!resendApiKey || !fromEmail) {
return new Response('Missing Resend API key or from email for newsletter send.', {
if (!emailBinding || !fromEmail) {
return new Response('Missing EMAIL binding or from email for newsletter send.', {
status: 500,
});
}
@ -129,8 +128,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
: undefined;
try {
await sendWithResend({
apiKey: resendApiKey,
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: subscriber.email,
subject,

View File

@ -1,10 +1,11 @@
import type { APIRoute } from 'astro';
import {
getEmailBinding,
getNewsletterDb,
getRuntimeEnv,
readEnv,
required,
sendWithResend,
sendCloudflareEmail,
type NewsletterSubscriber,
} from '../../../lib/newsletter';
@ -57,9 +58,7 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
.bind(row.id)
.run();
const resendApiKey =
readEnv('NEWSLETTER_RESEND_API_KEY', runtimeEnv) ||
readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
const emailBinding = getEmailBinding(runtimeEnv);
const fromEmail =
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
@ -67,10 +66,9 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
if (resendApiKey && fromEmail && notifyTo) {
if (emailBinding && fromEmail && notifyTo) {
try {
await sendWithResend({
apiKey: resendApiKey,
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: notifyTo,
subject: 'Iscrizione newsletter confermata — NexStudio',

View File

@ -2,6 +2,7 @@ import type { APIRoute } from 'astro';
import {
absoluteUrl,
createToken,
getEmailBinding,
getNewsletterDb,
getRuntimeEnv,
isValidEmail,
@ -9,7 +10,7 @@ import {
readEnv,
required,
sanitizeRedirect,
sendWithResend,
sendCloudflareEmail,
siteOrigin,
verifyTurnstile,
type NewsletterSubscriber,
@ -118,9 +119,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
});
}
const resendApiKey =
readEnv('NEWSLETTER_RESEND_API_KEY', runtimeEnv) ||
readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
const emailBinding = getEmailBinding(runtimeEnv);
const fromEmail =
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
@ -128,9 +127,9 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
if (!resendApiKey || !fromEmail) {
if (!emailBinding || !fromEmail) {
return new Response(
'Live mode requires NEWSLETTER_RESEND_API_KEY (or CONTACT_FORM_RESEND_API_KEY) and NEWSLETTER_FROM_EMAIL (or CONTACT_FORM_FROM_EMAIL).',
'Live mode requires EMAIL binding (Cloudflare Email Service) and NEWSLETTER_FROM_EMAIL (or CONTACT_FORM_FROM_EMAIL).',
{ status: 500 },
);
}
@ -173,8 +172,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
const copy = confirmEmailCopy(locale, confirmUrl);
try {
await sendWithResend({
apiKey: resendApiKey,
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: email,
subject: copy.subject,
@ -187,8 +185,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
if (notifyTo) {
try {
await sendWithResend({
apiKey: resendApiKey,
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: notifyTo,
subject: required(form.get('_subject')) || 'Nuova richiesta newsletter — NexStudio',