Replace Resend with n8n webhook relay for emails

- email.ts: POST to N8N_EMAIL_WEBHOOK_URL instead of Resend API
- env.ts: replace RESEND_API_KEY with N8N_EMAIL_WEBHOOK_URL
- .env.example: document new env var
- Add n8n workflow JSON (Webhook -> Gmail -> Respond) ready to import
This commit is contained in:
2026-04-30 12:02:04 +01:00
parent adcedb7cfd
commit 5912955b9c
4 changed files with 89 additions and 24 deletions

View File

@@ -12,9 +12,9 @@ NODE_ENV=development
SUPABASE_URL=https://tu-supabase.tudominio.com SUPABASE_URL=https://tu-supabase.tudominio.com
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Resend — servicio de email # n8n — relé de email vía webhook (n8n se encarga de enviar por Gmail/SMTP)
# Crear cuenta en https://resend.com y configurar dominio # La URL es la del webhook configurado en el workflow de n8n.
RESEND_API_KEY=re_... N8N_EMAIL_WEBHOOK_URL=https://n8n.tudominio.com/webhook/naturcalabacera-email
EMAIL_FROM=Naturcalabacera <reservas@tudominio.com> EMAIL_FROM=Naturcalabacera <reservas@tudominio.com>
# Destinatarios de notificaciones (parametrizables, sin hardcodear) # Destinatarios de notificaciones (parametrizables, sin hardcodear)

View File

@@ -22,8 +22,8 @@ export const env = {
SUPABASE_URL: required('SUPABASE_URL'), SUPABASE_URL: required('SUPABASE_URL'),
SUPABASE_SERVICE_ROLE_KEY: required('SUPABASE_SERVICE_ROLE_KEY'), SUPABASE_SERVICE_ROLE_KEY: required('SUPABASE_SERVICE_ROLE_KEY'),
// Email — Resend // Email — n8n webhook (relé a Gmail)
RESEND_API_KEY: required('RESEND_API_KEY'), N8N_EMAIL_WEBHOOK_URL: required('N8N_EMAIL_WEBHOOK_URL'),
EMAIL_FROM: optional('EMAIL_FROM', 'Naturcalabacera <reservas@naturcalabacera.com>'), EMAIL_FROM: optional('EMAIL_FROM', 'Naturcalabacera <reservas@naturcalabacera.com>'),
// Destinatarios de notificaciones (configurables, sin hardcodear) // Destinatarios de notificaciones (configurables, sin hardcodear)

View File

@@ -7,40 +7,34 @@ interface SendEmailOptions {
replyTo?: string; replyTo?: string;
} }
interface ResendResponse {
id?: string;
error?: { message: string; name: string };
}
/** /**
* Envía un email usando la API de Resend. * Envía un email a través del webhook de n8n.
* Docs: https://resend.com/docs/api-reference/emails/send-email * El workflow de n8n recibe { to, subject, html } y lo envía vía Gmail.
*/ */
export async function sendEmail(options: SendEmailOptions): Promise<{ success: boolean; id?: string; error?: string }> { export async function sendEmail(options: SendEmailOptions): Promise<{ success: boolean; id?: string; error?: string }> {
try { try {
const res = await fetch('https://api.resend.com/emails', { const to = Array.isArray(options.to) ? options.to.join(', ') : options.to;
const res = await fetch(env.N8N_EMAIL_WEBHOOK_URL, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
from: env.EMAIL_FROM, to,
to: Array.isArray(options.to) ? options.to : [options.to],
subject: options.subject, subject: options.subject,
html: options.html, html: options.html,
...(options.replyTo && { reply_to: options.replyTo }), from: env.EMAIL_FROM,
replyTo: options.replyTo,
}), }),
}); });
const data = await res.json() as ResendResponse; if (!res.ok) {
const text = await res.text().catch(() => '');
if (!res.ok || data.error) { const errorMsg = `HTTP ${res.status}${text ? `${text.slice(0, 200)}` : ''}`;
const errorMsg = data.error?.message ?? `HTTP ${res.status}`;
console.error('[email] Error al enviar:', errorMsg); console.error('[email] Error al enviar:', errorMsg);
return { success: false, error: errorMsg }; return { success: false, error: errorMsg };
} }
const data = await res.json().catch(() => ({})) as { id?: string };
return { success: true, id: data.id }; return { success: true, id: data.id };
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Error desconocido'; const errorMsg = err instanceof Error ? err.message : 'Error desconocido';

View File

@@ -0,0 +1,71 @@
{
"name": "Naturcalabacera - Email Relay",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "naturcalabacera-email",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-naturcalabacera",
"name": "Webhook (POST email)",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300]
},
{
"parameters": {
"sendTo": "={{ $json.body.to }}",
"subject": "={{ $json.body.subject }}",
"emailType": "html",
"message": "={{ $json.body.html }}",
"options": {}
},
"id": "gmail-send",
"name": "Gmail — Enviar",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [520, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { success: true, id: $json.id } }}",
"options": {}
},
"id": "respond-ok",
"name": "Responder OK",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [800, 300]
}
],
"connections": {
"Webhook (POST email)": {
"main": [
[
{
"node": "Gmail — Enviar",
"type": "main",
"index": 0
}
]
]
},
"Gmail — Enviar": {
"main": [
[
{
"node": "Responder OK",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}