import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { EmailTemplateRenderer } from "../shared/email-template-renderer.ts"; import QRCode from 'https://esm.sh/qrcode@1.5.3'; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; interface NotificationRequest { template_key: string; recipient_email: string; recipient_name?: string; variables?: Record; } interface SMTPConfig { host: string; port: number; username: string; password: string; from_name: string; from_email: string; use_tls: boolean; } interface EmailPayload { to: string; subject: string; html: string; from_name: string; from_email: string; } // Send via SMTP async function sendViaSMTP(payload: EmailPayload, config: SMTPConfig): Promise { const boundary = "----=_Part_" + Math.random().toString(36).substr(2, 9); const emailContent = [ `From: "${payload.from_name}" <${payload.from_email}>`, `To: ${payload.to}`, `Subject: =?UTF-8?B?${btoa(payload.subject)}?=`, `MIME-Version: 1.0`, `Content-Type: multipart/alternative; boundary="${boundary}"`, ``, `--${boundary}`, `Content-Type: text/html; charset=UTF-8`, ``, payload.html, `--${boundary}--`, ].join("\r\n"); const conn = config.use_tls ? await Deno.connectTls({ hostname: config.host, port: config.port }) : await Deno.connect({ hostname: config.host, port: config.port }); const encoder = new TextEncoder(); const decoder = new TextDecoder(); async function readResponse(): Promise { const buffer = new Uint8Array(1024); const n = await conn.read(buffer); if (n === null) return ""; return decoder.decode(buffer.subarray(0, n)); } async function sendCommand(cmd: string): Promise { await conn.write(encoder.encode(cmd + "\r\n")); return await readResponse(); } try { await readResponse(); await sendCommand(`EHLO localhost`); await sendCommand("AUTH LOGIN"); await sendCommand(btoa(config.username)); const authResponse = await sendCommand(btoa(config.password)); if (!authResponse.includes("235") && !authResponse.includes("Authentication successful")) { throw new Error("SMTP Authentication failed"); } await sendCommand(`MAIL FROM:<${payload.from_email}>`); await sendCommand(`RCPT TO:<${payload.to}>`); await sendCommand("DATA"); await conn.write(encoder.encode(emailContent + "\r\n.\r\n")); await readResponse(); await sendCommand("QUIT"); conn.close(); } catch (error) { conn.close(); throw error; } } // Send via Resend async function sendViaResend(payload: EmailPayload, apiKey: string): Promise { const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: `${payload.from_name} <${payload.from_email}>`, to: [payload.to], subject: payload.subject, html: payload.html, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Resend error: ${error}`); } } // Send via ElasticEmail async function sendViaElasticEmail(payload: EmailPayload, apiKey: string): Promise { const response = await fetch("https://api.elasticemail.com/v4/emails/transactional", { method: "POST", headers: { "X-ElasticEmail-ApiKey": apiKey, "Content-Type": "application/json", }, body: JSON.stringify({ Recipients: { To: [payload.to], }, Content: { From: `${payload.from_name} <${payload.from_email}>`, Subject: payload.subject, Body: [ { ContentType: "HTML", Content: payload.html, }, ], }, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`ElasticEmail error: ${error}`); } } // Send via SendGrid async function sendViaSendGrid(payload: EmailPayload, apiKey: string): Promise { const response = await fetch("https://api.sendgrid.com/v3/mail/send", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ personalizations: [{ to: [{ email: payload.to }] }], from: { email: payload.from_email, name: payload.from_name }, subject: payload.subject, content: [{ type: "text/html", value: payload.html }], }), }); if (!response.ok) { const error = await response.text(); throw new Error(`SendGrid error: ${error}`); } } // Send via Mailgun async function sendViaMailgun(payload: EmailPayload, apiKey: string, domain: string): Promise { const formData = new FormData(); formData.append("from", `${payload.from_name} <${payload.from_email}>`); formData.append("to", payload.to); formData.append("subject", payload.subject); formData.append("html", payload.html); const response = await fetch(`https://api.mailgun.net/v3/${domain}/messages`, { method: "POST", headers: { "Authorization": `Basic ${btoa(`api:${apiKey}`)}`, }, body: formData, }); if (!response.ok) { const error = await response.text(); throw new Error(`Mailgun error: ${error}`); } } function replaceVariables(template: string, variables: Record): string { let result = template; for (const [key, value] of Object.entries(variables)) { result = result.replace(new RegExp(`{{${key}}}`, 'g'), value); } return result; } serve(async (req: Request): Promise => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } try { const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const supabase = createClient(supabaseUrl, supabaseKey); const body: NotificationRequest = await req.json(); const { template_key, recipient_email, recipient_name, variables = {} } = body; // Get notification template const { data: template, error: templateError } = await supabase .from("notification_templates") .select("*") .eq("key", template_key) .eq("is_active", true) .single(); if (templateError || !template) { console.log(`Template not found: ${template_key}`); return new Response( JSON.stringify({ success: false, message: "Template not found or inactive" }), { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Get platform settings const { data: settings } = await supabase .from("platform_settings") .select("*") .single(); if (!settings) { return new Response( JSON.stringify({ success: false, message: "Platform settings not configured" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Build email payload const allVariables = { recipient_name: recipient_name || "Pelanggan", platform_name: settings.brand_name || "Platform", ...variables, }; // Special handling for order_created: generate QR code image if (template_key === 'order_created' && allVariables.qr_string) { console.log('[SEND-NOTIFICATION] Generating QR code for order_created email'); try { const qrDataUrl = await QRCode.toDataURL(allVariables.qr_string, { width: 300, margin: 2, color: { dark: '#000000', light: '#FFFFFF' } }); allVariables.qr_code_image = qrDataUrl; console.log('[SEND-NOTIFICATION] QR code generated successfully'); } catch (qrError) { console.error('[SEND-NOTIFICATION] Failed to generate QR code:', qrError); // Continue without QR code - don't fail the email allVariables.qr_code_image = ''; } } const subject = replaceVariables(template.subject, allVariables); const htmlContent = replaceVariables(template.body_html || template.body_text || "", allVariables); // Wrap with master template for consistent branding const htmlBody = EmailTemplateRenderer.render({ subject: subject, content: htmlContent, brandName: settings.brand_name || "ACCESS HUB", }); const emailPayload: EmailPayload = { to: recipient_email, subject, html: htmlBody, from_name: settings.brand_email_from_name || settings.brand_name || "Notifikasi", from_email: settings.smtp_from_email || "noreply@example.com", }; // Determine provider and send const provider = settings.integration_email_provider || "smtp"; console.log(`Sending email via ${provider} to ${recipient_email}`); switch (provider) { case "smtp": await sendViaSMTP(emailPayload, { host: settings.smtp_host, port: settings.smtp_port || 587, username: settings.smtp_username, password: settings.smtp_password, from_name: emailPayload.from_name, from_email: emailPayload.from_email, use_tls: settings.smtp_use_tls ?? true, }); break; case "resend": const resendKey = Deno.env.get("RESEND_API_KEY"); if (!resendKey) throw new Error("RESEND_API_KEY not configured"); await sendViaResend(emailPayload, resendKey); break; case "elasticemail": const elasticKey = Deno.env.get("ELASTICEMAIL_API_KEY"); if (!elasticKey) throw new Error("ELASTICEMAIL_API_KEY not configured"); await sendViaElasticEmail(emailPayload, elasticKey); break; case "sendgrid": const sendgridKey = Deno.env.get("SENDGRID_API_KEY"); if (!sendgridKey) throw new Error("SENDGRID_API_KEY not configured"); await sendViaSendGrid(emailPayload, sendgridKey); break; case "mailgun": const mailgunKey = Deno.env.get("MAILGUN_API_KEY"); const mailgunDomain = Deno.env.get("MAILGUN_DOMAIN"); if (!mailgunKey || !mailgunDomain) throw new Error("MAILGUN credentials not configured"); await sendViaMailgun(emailPayload, mailgunKey, mailgunDomain); break; default: throw new Error(`Unknown email provider: ${provider}`); } // Log notification await supabase.from("notification_logs").insert({ template_id: template.id, recipient_email, channel: "email", status: "sent", sent_at: new Date().toISOString(), }); console.log(`Email sent successfully to ${recipient_email}`); return new Response( JSON.stringify({ success: true, message: "Notification sent" }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (error: any) { console.error("Error sending notification:", error); return new Response( JSON.stringify({ success: false, message: error.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } });