type SendEmailParams = { to: string | string[]; subject: string; text?: string; html?: string; from?: string; cc?: string | string[]; bcc?: string | string[]; replyTo?: string | string[]; attachments?: Array<{ filename: string; content: string; // base64 encoded type?: string; }>; }; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const RESEND_API_KEY = process.env.RESEND_API_KEY; const DEFAULT_FROM = process.env.EMAIL_FROM || "Stargirlnails "; // Helper function to format dates for ICS files (YYYYMMDDTHHMMSS) function formatDateForICS(date: string, time: string): string { // date is in YYYY-MM-DD format, time is in HH:MM format const [year, month, day] = date.split('-'); const [hours, minutes] = time.split(':'); return `${year}${month}${day}T${hours}${minutes}00`; } // Helper function to create ICS (iCalendar) file content function createICSFile(params: { date: string; // YYYY-MM-DD time: string; // HH:MM durationMinutes: number; customerName: string; treatmentName: string; }): string { const { date, time, durationMinutes, customerName, treatmentName } = params; // Calculate start and end times in Europe/Berlin timezone const dtStart = formatDateForICS(date, time); // Calculate end time const [hours, minutes] = time.split(':').map(Number); const startDate = new Date(`${date}T${time}:00`); const endDate = new Date(startDate.getTime() + durationMinutes * 60000); const endHours = String(endDate.getHours()).padStart(2, '0'); const endMinutes = String(endDate.getMinutes()).padStart(2, '0'); const dtEnd = formatDateForICS(date, `${endHours}:${endMinutes}`); // Create unique ID for this event const uid = `booking-${Date.now()}-${Math.random().toString(36).substr(2, 9)}@stargirlnails.de`; // Current timestamp for DTSTAMP const now = new Date(); const dtstamp = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; // ICS content const icsContent = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Stargirlnails Kiel//Booking System//DE', 'CALSCALE:GREGORIAN', 'METHOD:REQUEST', 'BEGIN:VEVENT', `UID:${uid}`, `DTSTAMP:${dtstamp}`, `DTSTART;TZID=Europe/Berlin:${dtStart}`, `DTEND;TZID=Europe/Berlin:${dtEnd}`, `SUMMARY:${treatmentName} - Stargirlnails Kiel`, `DESCRIPTION:Termin für ${treatmentName} bei Stargirlnails Kiel`, 'LOCATION:Stargirlnails Kiel', `ORGANIZER;CN=Stargirlnails Kiel:mailto:${process.env.EMAIL_FROM?.match(/<(.+)>/)?.[1] || 'no-reply@stargirlnails.de'}`, `ATTENDEE;CN=${customerName};RSVP=TRUE:mailto:${customerName}`, 'STATUS:CONFIRMED', 'SEQUENCE:0', 'BEGIN:VALARM', 'TRIGGER:-PT24H', 'ACTION:DISPLAY', 'DESCRIPTION:Erinnerung: Termin morgen bei Stargirlnails Kiel', 'END:VALARM', 'END:VEVENT', 'BEGIN:VTIMEZONE', 'TZID:Europe/Berlin', 'BEGIN:DAYLIGHT', 'TZOFFSETFROM:+0100', 'TZOFFSETTO:+0200', 'TZNAME:CEST', 'DTSTART:19700329T020000', 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU', 'END:DAYLIGHT', 'BEGIN:STANDARD', 'TZOFFSETFROM:+0200', 'TZOFFSETTO:+0100', 'TZNAME:CET', 'DTSTART:19701025T030000', 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU', 'END:STANDARD', 'END:VTIMEZONE', 'END:VCALENDAR' ].join('\r\n'); return icsContent; } // Cache for AGB PDF to avoid reading it multiple times let cachedAGBPDF: string | null = null; async function getAGBPDFBase64(): Promise { if (cachedAGBPDF) return cachedAGBPDF; try { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const agbPath = resolve(__dirname, "../../../AGB.pdf"); const buf = await readFile(agbPath); cachedAGBPDF = buf.toString('base64'); return cachedAGBPDF; } catch (error) { console.warn("Could not read AGB.pdf:", error); return null; } } export async function sendEmail(params: SendEmailParams): Promise<{ success: boolean }> { if (!RESEND_API_KEY) { // In development or if not configured, skip sending but don't fail the flow console.warn("Resend API key not configured. Skipping email send."); return { success: false }; } const payload = { from: params.from || DEFAULT_FROM, to: Array.isArray(params.to) ? params.to : [params.to], subject: params.subject, text: params.text, html: params.html, cc: params.cc ? (Array.isArray(params.cc) ? params.cc : [params.cc]) : undefined, bcc: params.bcc ? (Array.isArray(params.bcc) ? params.bcc : [params.bcc]) : undefined, reply_to: params.replyTo ? (Array.isArray(params.replyTo) ? params.replyTo : [params.replyTo]) : undefined, attachments: params.attachments, }; console.log(`Sending email via Resend: to=${JSON.stringify(payload.to)}, subject="${params.subject}"`); const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { "Authorization": `Bearer ${RESEND_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { const body = await response.text().catch(() => ""); console.error("Resend send error:", response.status, body); return { success: false }; } const responseData = await response.json().catch(() => ({})); console.log("Resend email sent successfully:", responseData); return { success: true }; } export async function sendEmailWithAGB(params: SendEmailParams): Promise<{ success: boolean }> { const agbBase64 = await getAGBPDFBase64(); if (agbBase64) { params.attachments = [ ...(params.attachments || []), { filename: "AGB_Stargirlnails_Kiel.pdf", content: agbBase64, type: "application/pdf" } ]; } return sendEmail(params); } export async function sendEmailWithAGBAndCalendar( params: SendEmailParams, calendarParams: { date: string; time: string; durationMinutes: number; customerName: string; treatmentName: string; } ): Promise<{ success: boolean }> { const agbBase64 = await getAGBPDFBase64(); // Create ICS file content const icsContent = createICSFile(calendarParams); const icsBase64 = Buffer.from(icsContent, 'utf-8').toString('base64'); // Attach both AGB and ICS file params.attachments = [...(params.attachments || [])]; if (agbBase64) { params.attachments.push({ filename: "AGB_Stargirlnails_Kiel.pdf", content: agbBase64, type: "application/pdf" }); } params.attachments.push({ filename: "Termin_Stargirlnails.ics", content: icsBase64, type: "text/calendar" }); return sendEmail(params); } export async function sendEmailWithInspirationPhoto( params: SendEmailParams, photoData: string, customerName: string ): Promise<{ success: boolean }> { if (!photoData) { return sendEmail(params); } // Extract file extension from base64 data URL const match = photoData.match(/data:image\/([^;]+);base64,(.+)/); if (!match) { console.warn("Invalid photo data format"); return sendEmail(params); } const [, extension, base64Content] = match; const filename = `inspiration_${customerName.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}.${extension}`; // Check if attachment is too large (max 1MB base64 content) if (base64Content.length > 1024 * 1024) { console.warn(`Photo attachment too large: ${base64Content.length} chars, skipping attachment`); return sendEmail(params); } // console.log(`Sending email with photo attachment: ${filename}, size: ${base64Content.length} chars`); params.attachments = [ ...(params.attachments || []), { filename, content: base64Content, type: `image/${extension}` } ]; return sendEmail(params); }