import * as fs from "fs"; import * as path from "path"; // Load .env variables const envPath = path.join(__dirname, "../.env"); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, "utf8"); for (const line of envContent.split("\n")) { const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/); if (match) { const key = match[1]; let value = match[2].trim(); if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1); else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1); process.env[key] = value; } } } const N8N_HOST = process.env.N8N_HOST || "https://n8n.gaboggamer.online"; const N8N_API_KEY = process.env.N8N_API_KEY; const EMAIL_USER = process.env.EMAIL_USER; const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD; const EMAIL_HOST = process.env.EMAIL_HOST; const EMAIL_PORT = parseInt(process.env.EMAIL_PORT || "465", 10); const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; const SUPABASE_SECRET_KEY = process.env.SUPABASE_SECRET_KEY; if (!N8N_API_KEY) { console.error("Error: N8N_API_KEY is not defined in .env"); process.exit(1); } async function n8nRequest(endpoint: string, method: string = "GET", body?: any) { const response = await fetch(`${N8N_HOST}${endpoint}`, { method, headers: { "X-N8N-API-KEY": N8N_API_KEY!, "Content-Type": "application/json", }, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const text = await response.text(); throw new Error(`n8n API request failed: ${response.status} ${response.statusText} - ${text}`); } return response.json(); } async function getOrCreateCredential(name: string, type: string, data: any) { try { const credsList = await n8nRequest("/api/v1/credentials"); const existingCred = credsList.data.find((c: any) => c.name === name && c.type === type); if (existingCred) { console.log(`Reusing existing credential: ${name} (ID: ${existingCred.id})...`); return existingCred.id; } const newCred = await n8nRequest("/api/v1/credentials", "POST", { name, type, data, }); console.log(`Created new credential: ${name} (ID: ${newCred.id})`); return newCred.id; } catch (err: any) { console.error(`Error setting up credential ${name}:`, err.message); process.exit(1); } } async function main() { console.log("\n=============================================="); console.log("DEPLOYING EMAIL AUTOMATION FOR N8N"); console.log("=============================================="); // 1. Setup SMTP Credentials const smtpCredId = await getOrCreateCredential("Semillero2_SMTP", "smtp", { host: EMAIL_HOST, port: EMAIL_PORT, user: EMAIL_USER, password: EMAIL_PASSWORD, secure: true, }); // 2. Setup IMAP Credentials const imapCredId = await getOrCreateCredential("Semillero2_IMAP_V2", "imap", { host: EMAIL_HOST, port: 993, user: EMAIL_USER, password: EMAIL_PASSWORD, secure: true, }); // 3. Setup Supabase Credentials const supabaseCredId = await getOrCreateCredential("Semillero2_Supabase", "supabaseApi", { host: SUPABASE_URL, serviceRole: SUPABASE_SECRET_KEY, allowedHttpRequestDomains: "none", }); // 4. Fetch Candidate Evaluation Workflow ID const workflowsList = await n8nRequest("/api/v1/workflows"); const evalWf = workflowsList.data.find( (w: any) => w.name === "Semillero2: End-to-End Candidate Evaluation" ); if (!evalWf) { console.error("Error: Could not find candidate evaluation workflow ID."); process.exit(1); } const evalWfId = evalWf.id; console.log(`Candidate Evaluation Workflow ID: ${evalWfId}`); // 5. Deploy / Update IMAP Ingestion Workflow const imapWf = workflowsList.data.find( (w: any) => w.name === "Semillero2: Email Ingestion Listener" ); const imapWfDefinition = { name: "Semillero2: Email Ingestion Listener", settings: {}, nodes: [ { parameters: { postProcessAction: "markAsRead", format: "resolved", downloadAttachments: true, options: { customEmailConfig: '["UNSEEN"]', }, }, id: "w2-imap-id", name: "Email Trigger (IMAP)", type: "n8n-nodes-base.emailReadImap", typeVersion: 2, position: [100, 300], credentials: { imap: { id: imapCredId, name: "Semillero2_IMAP_V2", }, }, }, { parameters: { operation: "pdf", binaryPropertyName: "attachment_0", }, id: "w2-extract-id", name: "Extract From PDF", type: "n8n-nodes-base.extractFromFile", typeVersion: 1, position: [320, 300], }, { parameters: { jsCode: `const item = $input.first().json; const text = item.text || ""; const emailBody = $('Email Trigger (IMAP)').item.json.textHtml || $('Email Trigger (IMAP)').item.json.text || ""; const fromEmail = $('Email Trigger (IMAP)').item.json.from.value[0].address; const fromName = $('Email Trigger (IMAP)').item.json.from.value[0].name || fromEmail.split('@')[0]; return [{ json: { name: fromName, email: fromEmail, text: \`Email Body:\\n\${emailBody}\\n\\nResume Text:\\n\${text}\` } }];`, }, id: "w2-format-id", name: "Format Email Data", type: "n8n-nodes-base.code", typeVersion: 2, position: [540, 300], }, { parameters: { workflowId: evalWfId, options: {}, }, id: "w2-exec-id", name: "Execute Workflow 3", type: "n8n-nodes-base.executeWorkflow", typeVersion: 1, position: [760, 300], }, ], connections: { "Email Trigger (IMAP)": { main: [ [ { node: "Extract From PDF", type: "main", index: 0, }, ], ], }, "Extract From PDF": { main: [ [ { node: "Format Email Data", type: "main", index: 0, }, ], ], }, "Format Email Data": { main: [ [ { node: "Execute Workflow 3", type: "main", index: 0, }, ], ], }, }, }; let imapDeployResult; if (imapWf) { if (imapWf.isArchived) { console.log(`Deleting archived IMAP workflow (ID: ${imapWf.id})...`); await n8nRequest(`/api/v1/workflows/${imapWf.id}`, "DELETE"); console.log("Creating new IMAP workflow..."); imapDeployResult = await n8nRequest("/api/v1/workflows", "POST", imapWfDefinition); } else { console.log(`Updating existing IMAP workflow (ID: ${imapWf.id})...`); imapDeployResult = await n8nRequest(`/api/v1/workflows/${imapWf.id}`, "PUT", imapWfDefinition); } } else { console.log("Creating new IMAP workflow..."); imapDeployResult = await n8nRequest("/api/v1/workflows", "POST", imapWfDefinition); } // Activate IMAP Ingestion Workflow console.log(`Activating IMAP Ingestion workflow (ID: ${imapDeployResult.id})...`); await n8nRequest(`/api/v1/workflows/${imapDeployResult.id}/activate`, "POST"); // 6. Deploy / Update Stage Change Email Notifier const notifierWf = workflowsList.data.find( (w: any) => w.name === "Semillero2: Stage Change Email Notifier" ); const notifierWfDefinition = { name: "Semillero2: Stage Change Email Notifier", settings: {}, nodes: [ { parameters: { httpMethod: "POST", path: "stage-changed", responseMode: "responseNode", options: {}, }, id: "notifier-webhook-id", name: "Stage Changed Webhook", type: "n8n-nodes-base.webhook", typeVersion: 1.1, position: [100, 300], }, { parameters: { resource: "row", operation: "get", tableId: "candidates", rowId: "={{ $('Stage Changed Webhook').item.json.body.new_record.candidate_id }}", }, id: "notifier-get-cand-id", name: "Get Candidate", type: "n8n-nodes-base.supabase", typeVersion: 1, position: [320, 300], credentials: { supabaseApi: { id: supabaseCredId, name: "Semillero2_Supabase", }, }, }, { parameters: { resource: "row", operation: "get", tableId: "jobs", rowId: "={{ $('Stage Changed Webhook').item.json.body.new_record.job_id }}", }, id: "notifier-get-job-id", name: "Get Job", type: "n8n-nodes-base.supabase", typeVersion: 1, position: [540, 300], credentials: { supabaseApi: { id: supabaseCredId, name: "Semillero2_Supabase", }, }, }, { parameters: { jsCode: `const candidate = $('Get Candidate').first().json; const candidateName = candidate.name; const contactInfo = candidate.contact_info || {}; const candidateEmail = contactInfo.email || "unknown@example.com"; const jobTitle = $('Get Job').first().json.title; const oldRecord = $('Stage Changed Webhook').first().json.body.old_record; const oldStage = oldRecord ? oldRecord.stage : 'None'; const newStage = $('Stage Changed Webhook').first().json.body.new_record.stage; let subject = ''; let html = ''; if (newStage === 'Screening') { subject = \`Application Received: \${jobTitle}\`; html = \`

Hello \${candidateName},

Thank you for applying to the \${jobTitle} position. We have received your CV and are currently reviewing it.

Best regards,
The Recruitment Team

\`; } else if (newStage === 'Technical') { subject = \`Next Steps - Technical Interview: \${jobTitle}\`; html = \`

Hello \${candidateName},

Great news! Your application for the \${jobTitle} position has progressed to the Technical Interview stage.

Our team will contact you shortly to schedule the interview. Please prepare by reviewing core concepts for the role.

Best regards,
The Recruitment Team

\`; } else if (newStage === 'Cultural') { subject = \`Next Steps - Cultural Interview: \${jobTitle}\`; html = \`

Hello \${candidateName},

We are pleased to invite you to the Cultural Fit Interview for the \${jobTitle} role.

This conversation will be a casual discussion to get to know you better and answer any questions you have about our culture and team.

Best regards,
The Recruitment Team

\`; } else if (newStage === 'Offer') { subject = \`Job Offer - \${jobTitle}!\`; html = \`

Hello \${candidateName},

Congratulations! We are thrilled to extend a Job Offer for the \${jobTitle} position.

We will send the official offer letter and details via email shortly. We look forward to having you join us!

Best regards,
The Hiring Team

\`; } else if (newStage === 'Hired') { subject = \`Welcome to the Team!\`; html = \`

Hello \${candidateName},

Welcome aboard! You have been officially Hired for the \${jobTitle} position.

Our onboarding team will reach out with the next steps for your first day.

Best regards,
The Team

\`; } else if (newStage === 'Rejected') { subject = \`Application Update: \${jobTitle}\`; html = \`

Hello \${candidateName},

Thank you for your interest in the \${jobTitle} position and for taking the time to interview with us.

Unfortunately, we have decided to proceed with other candidates whose profiles more closely align with our current needs.

We will keep your profile in our database for future opportunities.

Best regards,
The Recruitment Team

\`; } else { subject = \`Application Status Update: \${jobTitle}\`; html = \`

Hello \${candidateName},

The status of your application for the \${jobTitle} position has been updated to: \${newStage}.

Best regards,
The Recruitment Team

\`; } return [{ json: { toEmail: candidateEmail, subject, html } }];`, }, id: "notifier-code-id", name: "Format Notification Email", type: "n8n-nodes-base.code", typeVersion: 2, position: [760, 300], }, { parameters: { fromEmail: '"Recruitment Team" ', toEmail: "={{ $json.toEmail }}", subject: "={{ $json.subject }}", html: "={{ $json.html }}", options: {}, }, id: "notifier-send-id", name: "Send Notification Email", type: "n8n-nodes-base.emailSend", typeVersion: 2.1, position: [980, 300], credentials: { smtp: { id: smtpCredId, name: "Semillero2_SMTP", }, }, }, { parameters: { options: {}, }, id: "notifier-respond-id", name: "Respond to Webhook", type: "n8n-nodes-base.respondToWebhook", typeVersion: 1.1, position: [1200, 300], }, ], connections: { "Stage Changed Webhook": { main: [ [ { node: "Get Candidate", type: "main", index: 0, }, ], ], }, "Get Candidate": { main: [ [ { node: "Get Job", type: "main", index: 0, }, ], ], }, "Get Job": { main: [ [ { node: "Format Notification Email", type: "main", index: 0, }, ], ], }, "Format Notification Email": { main: [ [ { node: "Send Notification Email", type: "main", index: 0, }, ], ], }, "Send Notification Email": { main: [ [ { node: "Respond to Webhook", type: "main", index: 0, }, ], ], }, }, }; let notifierDeployResult; if (notifierWf) { if (notifierWf.isArchived) { console.log(`Deleting archived Notifier workflow (ID: ${notifierWf.id})...`); await n8nRequest(`/api/v1/workflows/${notifierWf.id}`, "DELETE"); console.log("Creating new Notifier workflow..."); notifierDeployResult = await n8nRequest("/api/v1/workflows", "POST", notifierWfDefinition); } else { console.log(`Updating existing Notifier workflow (ID: ${notifierWf.id})...`); notifierDeployResult = await n8nRequest(`/api/v1/workflows/${notifierWf.id}`, "PUT", notifierWfDefinition); } } else { console.log("Creating new Notifier workflow..."); notifierDeployResult = await n8nRequest("/api/v1/workflows", "POST", notifierWfDefinition); } // Activate Notifier Workflow console.log(`Activating Notifier workflow (ID: ${notifierDeployResult.id})...`); await n8nRequest(`/api/v1/workflows/${notifierDeployResult.id}/activate`, "POST"); const notifierWebhookUrl = `${N8N_HOST}/webhook/stage-changed`; console.log("\n=============================================="); console.log("EMAIL AUTOMATION SETUP COMPLETE"); console.log("=============================================="); console.log(`IMAP Workflow ID: ${imapDeployResult.id}`); console.log(`Notifier Workflow ID: ${notifierDeployResult.id}`); console.log(`Notifier Webhook URL: ${notifierWebhookUrl}`); console.log("=============================================="); } main().catch((err) => { console.error("Email automation setup failed:", err); process.exit(1); });