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:
45
src/lib/email.ts
Normal file
45
src/lib/email.ts
Normal 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 } : {}),
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user