n8n-nodes-instantly
Version:
n8n community node for Instantly API v2
536 lines • 28.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CampaignOperations = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const generic_functions_1 = require("../../generic.functions");
const paginationHelpers_1 = require("../functions/paginationHelpers");
const resourceLocatorHelpers_1 = require("../functions/resourceLocatorHelpers");
const dateHelpers_1 = require("../functions/dateHelpers");
/**
* Helper function to resolve email addresses based on selection mode
*/
async function resolveEmailAddresses(context, itemIndex, mainParams, additionalFields = {}) {
// Email account fields are now in main parameters for create operation
const emailAccountsMode = mainParams.emailAccountsMode || additionalFields.emailAccountsMode || 'selectFromList';
const useAllEmailAccounts = mainParams.useAllEmailAccounts || additionalFields.useAllEmailAccounts || false;
const emailAccountsList = mainParams.emailAccountsList || additionalFields.emailAccountsList || [];
const inboxConfigJson = mainParams.inboxConfigJson || additionalFields.inboxConfigJson;
console.log(`DEBUG: resolveEmailAddresses called with mode: ${emailAccountsMode}, useAll: ${useAllEmailAccounts}`, {
mainParams: { emailAccountsMode: mainParams.emailAccountsMode, useAllEmailAccounts: mainParams.useAllEmailAccounts },
additionalFields: { emailAccountsMode: additionalFields.emailAccountsMode, useAllEmailAccounts: additionalFields.useAllEmailAccounts }
});
// If "Use All Available Email Accounts" toggle is enabled, fetch all accounts using pagination
if (useAllEmailAccounts) {
try {
console.log('DEBUG: Fetching ALL email accounts using pagination...');
const allAccounts = await (0, paginationHelpers_1.paginateInstantlyApi)(context, '/api/v2/accounts', 'email accounts');
const emailList = allAccounts.map((account) => account.email).filter(Boolean);
console.log(`DEBUG: Successfully loaded ${emailList.length} email accounts (useAll mode with pagination):`, emailList);
return emailList;
}
catch (error) {
console.error('Error fetching all email accounts with pagination:', error);
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Failed to fetch all email accounts: ${error.message}`, { itemIndex });
}
}
// Handle specific selection modes
switch (emailAccountsMode) {
case 'selectFromList':
// Return the selected email accounts
return emailAccountsList || [];
case 'jsonInput':
// Handle JSON/Expression input with validation
try {
if (!inboxConfigJson) {
return [];
}
let inboxConfig;
if (typeof inboxConfigJson === 'string') {
// Parse JSON string
try {
inboxConfig = JSON.parse(inboxConfigJson);
}
catch (parseError) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid JSON format in inbox configuration: ${parseError.message}`, { itemIndex });
}
}
else {
// Already parsed (from n8n expression)
inboxConfig = inboxConfigJson;
}
// Validate and extract email addresses
if (!Array.isArray(inboxConfig)) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Inbox configuration must be an array of objects', { itemIndex });
}
const emailList = [];
inboxConfig.forEach((item, index) => {
if (typeof item !== 'object' || !item.email) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid inbox configuration at index ${index}: each item must be an object with an "email" field`, { itemIndex });
}
if (typeof item.email !== 'string' || !item.email.includes('@')) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid email address at index ${index}: "${item.email}"`, { itemIndex });
}
emailList.push(item.email);
});
console.log(`DEBUG: Processed JSON input for ${emailList.length} email accounts:`, emailList);
return emailList;
}
catch (error) {
if (error instanceof n8n_workflow_1.NodeOperationError) {
throw error;
}
console.error('Error processing JSON input:', error);
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Failed to process JSON inbox configuration: ${error.message}`, { itemIndex });
}
default:
// Default to selectFromList mode
return emailAccountsList || [];
}
}
/**
* Campaign operations handler
*/
class CampaignOperations {
/**
* Create a new campaign
*/
static async create(context, itemIndex) {
// HTML formatter for final API payload (after n8n expression processing)
const convertToHtmlFormat = (content) => {
if (!content)
return '';
// Split content by line breaks and wrap each line in div tags
// This ensures proper line spacing and formatting in Instantly emails
const lines = content.split('\n');
const htmlContent = lines.map(line => {
// Handle empty lines with a div containing a break
if (line.trim() === '') {
return '<div><br /></div>';
}
// Wrap non-empty lines in div tags
return `<div>${line}</div>`;
}).join('');
return htmlContent;
};
// HTML tag stripper for subject lines (subjects should always be plain text)
const stripHtmlTags = (content) => {
if (!content)
return '';
// Remove all HTML tags and decode common HTML entities
return content
.replace(/<[^>]*>/g, '') // Remove all HTML tags
.replace(/ /g, ' ') // Replace non-breaking spaces
.replace(/&/g, '&') // Replace encoded ampersands
.replace(/</g, '<') // Replace encoded less-than
.replace(/>/g, '>') // Replace encoded greater-than
.replace(/"/g, '"') // Replace encoded quotes
.trim(); // Remove leading/trailing whitespace
};
// Required fields
const name = context.getNodeParameter('name', itemIndex);
const scheduleName = context.getNodeParameter('scheduleName', itemIndex);
const scheduleStartTime = context.getNodeParameter('scheduleStartTime', itemIndex);
const scheduleEndTime = context.getNodeParameter('scheduleEndTime', itemIndex);
const scheduleDays = context.getNodeParameter('scheduleDays', itemIndex);
const scheduleTimezone = context.getNodeParameter('scheduleTimezone', itemIndex);
// Additional fields
const additionalFields = context.getNodeParameter('additionalFields', itemIndex, {});
// Email account parameters (now in main configuration)
const emailAccountsMode = context.getNodeParameter('emailAccountsMode', itemIndex, 'selectFromList');
const useAllEmailAccounts = context.getNodeParameter('useAllEmailAccounts', itemIndex, false);
const emailAccountsList = context.getNodeParameter('emailAccountsList', itemIndex, []);
const inboxConfigJson = context.getNodeParameter('inboxConfigJson', itemIndex, '[]');
// Build the days object from the selected days array
const days = {};
scheduleDays.forEach(day => {
const dayKey = day;
days[dayKey] = true;
});
// Build the schedule object
const scheduleItem = {
name: scheduleName,
timing: {
from: scheduleStartTime,
to: scheduleEndTime,
},
days,
timezone: scheduleTimezone,
};
const campaignSchedule = {
schedules: [scheduleItem],
};
// Add optional start/end dates if provided, formatted for API
if (additionalFields.startDate) {
const formattedStartDate = (0, dateHelpers_1.formatDateTimeForApi)(additionalFields.startDate);
if (formattedStartDate) {
campaignSchedule.start_date = formattedStartDate;
}
}
if (additionalFields.endDate) {
const formattedEndDate = (0, dateHelpers_1.formatDateTimeForApi)(additionalFields.endDate);
if (formattedEndDate) {
campaignSchedule.end_date = formattedEndDate;
}
}
// Build the campaign creation payload
const campaignData = {
name,
campaign_schedule: campaignSchedule,
};
// Add optional fields if provided
if (additionalFields.plValue !== undefined) {
campaignData.pl_value = additionalFields.plValue;
}
if (additionalFields.isEvergreen !== undefined) {
campaignData.is_evergreen = additionalFields.isEvergreen;
}
if (additionalFields.emailGap !== undefined) {
campaignData.email_gap = additionalFields.emailGap;
}
if (additionalFields.randomWaitMax !== undefined) {
campaignData.random_wait_max = additionalFields.randomWaitMax;
}
if (additionalFields.textOnly !== undefined) {
campaignData.text_only = additionalFields.textOnly;
}
if (additionalFields.dailyLimit !== undefined) {
campaignData.daily_limit = additionalFields.dailyLimit;
}
if (additionalFields.dailyMaxLeads !== undefined) {
campaignData.daily_max_leads = additionalFields.dailyMaxLeads;
}
if (additionalFields.linkTracking !== undefined) {
campaignData.link_tracking = additionalFields.linkTracking;
}
if (additionalFields.openTracking !== undefined) {
campaignData.open_tracking = additionalFields.openTracking;
}
if (additionalFields.stopOnReply !== undefined) {
campaignData.stop_on_reply = additionalFields.stopOnReply;
}
if (additionalFields.stopOnAutoReply !== undefined) {
campaignData.stop_on_auto_reply = additionalFields.stopOnAutoReply;
}
if (additionalFields.stopForCompany !== undefined) {
campaignData.stop_for_company = additionalFields.stopForCompany;
}
if (additionalFields.prioritizeNewLeads !== undefined) {
campaignData.prioritize_new_leads = additionalFields.prioritizeNewLeads;
}
if (additionalFields.matchLeadEsp !== undefined) {
campaignData.match_lead_esp = additionalFields.matchLeadEsp;
}
if (additionalFields.insertUnsubscribeHeader !== undefined) {
campaignData.insert_unsubscribe_header = additionalFields.insertUnsubscribeHeader;
}
if (additionalFields.allowRiskyContacts !== undefined) {
campaignData.allow_risky_contacts = additionalFields.allowRiskyContacts;
}
if (additionalFields.disableBounceProtect !== undefined) {
campaignData.disable_bounce_protect = additionalFields.disableBounceProtect;
}
// Handle email address selection
const mainEmailParams = { emailAccountsMode, useAllEmailAccounts, emailAccountsList, inboxConfigJson };
const emailAddresses = await resolveEmailAddresses(context, itemIndex, mainEmailParams, additionalFields);
if (emailAddresses && emailAddresses.length > 0) {
campaignData.email_list = emailAddresses;
}
if (additionalFields.emailTagList && additionalFields.emailTagList.length > 0) {
campaignData.email_tag_list = additionalFields.emailTagList;
}
if (additionalFields.ccList && additionalFields.ccList.length > 0) {
campaignData.cc_list = additionalFields.ccList;
}
if (additionalFields.bccList && additionalFields.bccList.length > 0) {
campaignData.bcc_list = additionalFields.bccList;
}
// Handle auto variant select
if (additionalFields.autoVariantSelectTrigger) {
campaignData.auto_variant_select = {
trigger: additionalFields.autoVariantSelectTrigger,
};
}
// Handle email sequence steps
if (additionalFields.sequenceSteps && additionalFields.sequenceSteps.steps && additionalFields.sequenceSteps.steps.step) {
const sequenceSteps = Array.isArray(additionalFields.sequenceSteps.steps.step)
? additionalFields.sequenceSteps.steps.step
: [additionalFields.sequenceSteps.steps.step];
// Validate and build steps array for the sequence
const steps = [];
sequenceSteps.forEach((step, index) => {
const stepNumber = index + 1;
// Validate required fields - only body is required, subject is optional
// Empty subjects allow Instantly to auto-apply "Re:" threading behavior
if (!step.body) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Email step ${stepNumber} is missing required body content`, { itemIndex });
}
// Smart content processor for hybrid variable detection (HTML conversion moved to post-processing)
const smartProcessContent = (content) => {
if (!content)
return '';
// Step 1: Extract and protect Instantly variables (simple word patterns)
const instantlyVars = {};
let varIndex = 0;
// Protect Instantly variables like {{firstName}}, {{lastName}}, {{companyName}}
// Pattern matches: {{word}} where word contains only letters, numbers, and underscores
const protectedContent = content.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match, varName) => {
const placeholder = `__INSTANTLY_VAR_${varIndex++}__`;
instantlyVars[placeholder] = match;
return placeholder;
});
// Step 2: n8n expressions (like {{ $json.fieldName }}) are processed normally by n8n
// since we allow expressions on these fields for automation purposes
// Step 3: Restore Instantly variables after n8n processing
const finalContent = protectedContent.replace(/__INSTANTLY_VAR_\d+__/g, (placeholder) => {
return instantlyVars[placeholder] || placeholder;
});
// Step 4: Keep line breaks as \n for now - HTML conversion happens after n8n expression processing
return finalContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
};
// Build sequence step object with variants array
// The Instantly API expects each step to have a 'variants' array for A/B testing
// Note: HTML conversion happens later, after n8n expression processing
const sequenceStep = {
type: 'email', // Required by Instantly API for email steps
variants: [
{
subject: step.subject ? smartProcessContent(step.subject) : '', // Empty subject allows Instantly auto-threading
body: smartProcessContent(step.body),
}
]
};
// Add delay - ALL steps require a delay property
if (stepNumber === 1) {
// First step has delay of 0 (immediate)
sequenceStep.delay = 0;
}
else {
// Subsequent steps require a delay of at least 1 day
if (!step.delay || step.delay < 1) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Email step ${stepNumber} requires a delay of at least 1 day`, { itemIndex });
}
sequenceStep.delay = step.delay;
}
steps.push(sequenceStep);
});
// Add sequences to campaign data if any were provided
// The API expects an array with a single sequence object containing steps
if (steps.length > 0) {
campaignData.sequences = [{
steps: steps
}];
}
}
// Apply formatting to email content right before API call
// This ensures it happens after all n8n expression processing is complete
console.log('DEBUG: About to apply content formatting...');
if (campaignData.sequences && campaignData.sequences[0] && campaignData.sequences[0].steps) {
console.log('DEBUG: Found sequences to process, steps count:', campaignData.sequences[0].steps.length);
campaignData.sequences[0].steps.forEach((step, stepIndex) => {
if (step.variants && step.variants.length > 0) {
step.variants.forEach((variant, variantIndex) => {
if (variant.subject) {
console.log(`DEBUG: Stripping HTML from subject for step ${stepIndex}, variant ${variantIndex}:`, variant.subject);
variant.subject = stripHtmlTags(variant.subject);
console.log(`DEBUG: Subject after HTML stripping:`, variant.subject);
}
if (variant.body) {
console.log(`DEBUG: Converting body to HTML for step ${stepIndex}, variant ${variantIndex}:`, variant.body);
variant.body = convertToHtmlFormat(variant.body);
console.log(`DEBUG: Body after HTML conversion:`, variant.body);
}
});
}
});
}
else {
console.log('DEBUG: No sequences found to process');
}
console.log('DEBUG: Final campaignData before API call:', JSON.stringify(campaignData, null, 2));
return await generic_functions_1.instantlyApiRequest.call(context, 'POST', '/api/v2/campaigns', campaignData);
}
/**
* Get a single campaign by ID
*/
static async get(context, itemIndex) {
const campaignId = (0, resourceLocatorHelpers_1.getCampaignId)(context, itemIndex);
return await generic_functions_1.instantlyApiRequest.call(context, 'GET', `/api/v2/campaigns/${campaignId}`);
}
/**
* Get many campaigns with pagination support
*/
static async getMany(context, itemIndex) {
const returnAll = context.getNodeParameter('returnAll', itemIndex, false);
const limit = context.getNodeParameter('limit', itemIndex, 50);
// Validate limit doesn't exceed 100
if (limit > 100) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Limit cannot exceed 100. Instantly API has a maximum limit of 100.', { itemIndex });
}
if (returnAll) {
// Get all campaigns with pagination
const allCampaigns = await (0, paginationHelpers_1.paginateInstantlyApi)(context, '/api/v2/campaigns', 'campaigns');
return { items: allCampaigns };
}
else {
// Get single page with specified limit
const queryParams = { limit };
return await generic_functions_1.instantlyApiRequest.call(context, 'GET', '/api/v2/campaigns', {}, queryParams);
}
}
/**
* Update a campaign
*/
static async update(context, itemIndex) {
const campaignId = (0, resourceLocatorHelpers_1.getCampaignId)(context, itemIndex);
const name = context.getNodeParameter('name', itemIndex, '');
const updateAdditionalFields = context.getNodeParameter('updateAdditionalFields', itemIndex, {});
const campaignSchedule = context.getNodeParameter('campaignSchedule', itemIndex, {});
// Build the request body
const body = {};
// Only add name if it's provided
if (name && name.trim() !== '') {
body.name = name.trim();
}
// Add additional fields if provided
if (updateAdditionalFields.plValue !== undefined) {
body.pl_value = updateAdditionalFields.plValue;
}
if (updateAdditionalFields.isEvergreen !== undefined) {
body.is_evergreen = updateAdditionalFields.isEvergreen;
}
if (updateAdditionalFields.emailGap !== undefined) {
body.email_gap = updateAdditionalFields.emailGap;
}
if (updateAdditionalFields.randomWaitMax !== undefined) {
body.random_wait_max = updateAdditionalFields.randomWaitMax;
}
if (updateAdditionalFields.textOnly !== undefined) {
body.text_only = updateAdditionalFields.textOnly;
}
if (updateAdditionalFields.dailyLimit !== undefined) {
body.daily_limit = updateAdditionalFields.dailyLimit;
}
if (updateAdditionalFields.dailyMaxLeads !== undefined) {
body.daily_max_leads = updateAdditionalFields.dailyMaxLeads;
}
// Handle email address selection for updates (still in additional fields for update operation)
const emailAddresses = await resolveEmailAddresses(context, itemIndex, {}, updateAdditionalFields);
if (emailAddresses && emailAddresses.length > 0) {
body.email_list = emailAddresses;
}
if (updateAdditionalFields.emailTagList !== undefined && updateAdditionalFields.emailTagList.trim() !== '') {
body.email_tag_list = updateAdditionalFields.emailTagList.split(',').map((tag) => tag.trim());
}
if (updateAdditionalFields.ccList !== undefined && updateAdditionalFields.ccList.trim() !== '') {
body.cc_list = updateAdditionalFields.ccList.split(',').map((email) => email.trim());
}
if (updateAdditionalFields.bccList !== undefined && updateAdditionalFields.bccList.trim() !== '') {
body.bcc_list = updateAdditionalFields.bccList.split(',').map((email) => email.trim());
}
if (updateAdditionalFields.linkTracking !== undefined) {
body.link_tracking = updateAdditionalFields.linkTracking;
}
if (updateAdditionalFields.openTracking !== undefined) {
body.open_tracking = updateAdditionalFields.openTracking;
}
if (updateAdditionalFields.stopOnReply !== undefined) {
body.stop_on_reply = updateAdditionalFields.stopOnReply;
}
if (updateAdditionalFields.stopOnAutoReply !== undefined) {
body.stop_on_auto_reply = updateAdditionalFields.stopOnAutoReply;
}
if (updateAdditionalFields.stopForCompany !== undefined) {
body.stop_for_company = updateAdditionalFields.stopForCompany;
}
if (updateAdditionalFields.prioritizeNewLeads !== undefined) {
body.prioritize_new_leads = updateAdditionalFields.prioritizeNewLeads;
}
if (updateAdditionalFields.matchLeadEsp !== undefined) {
body.match_lead_esp = updateAdditionalFields.matchLeadEsp;
}
if (updateAdditionalFields.insertUnsubscribeHeader !== undefined) {
body.insert_unsubscribe_header = updateAdditionalFields.insertUnsubscribeHeader;
}
if (updateAdditionalFields.allowRiskyContacts !== undefined) {
body.allow_risky_contacts = updateAdditionalFields.allowRiskyContacts;
}
if (updateAdditionalFields.disableBounceProtect !== undefined) {
body.disable_bounce_protect = updateAdditionalFields.disableBounceProtect;
}
if (updateAdditionalFields.autoVariantSelectTrigger !== undefined && updateAdditionalFields.autoVariantSelectTrigger !== '') {
body.auto_variant_select = {
trigger: updateAdditionalFields.autoVariantSelectTrigger,
};
}
if (updateAdditionalFields.sequences !== undefined && updateAdditionalFields.sequences.trim() !== '') {
try {
body.sequences = JSON.parse(updateAdditionalFields.sequences);
}
catch (error) {
throw new Error('Invalid JSON format for sequences field');
}
}
// Handle campaign schedule
if (campaignSchedule && Object.keys(campaignSchedule).length > 0) {
const schedule = {};
if (campaignSchedule.startDate) {
const formattedStartDate = (0, dateHelpers_1.formatDateTimeForApi)(campaignSchedule.startDate);
if (formattedStartDate) {
schedule.start_date = formattedStartDate;
}
}
if (campaignSchedule.endDate) {
const formattedEndDate = (0, dateHelpers_1.formatDateTimeForApi)(campaignSchedule.endDate);
if (formattedEndDate) {
schedule.end_date = formattedEndDate;
}
}
if (campaignSchedule.schedules && campaignSchedule.schedules.schedule && campaignSchedule.schedules.schedule.length > 0) {
schedule.schedules = campaignSchedule.schedules.schedule.map((sched) => {
const scheduleItem = {
name: sched.name || 'Default Schedule',
timing: {
from: sched.from || '09:00',
to: sched.to || '17:00',
},
timezone: sched.timezone || 'America/New_York',
};
// Convert days array to object format expected by API
if (sched.days && Array.isArray(sched.days)) {
const daysObj = {};
for (let i = 0; i <= 6; i++) {
daysObj[i.toString()] = sched.days.includes(i.toString());
}
scheduleItem.days = daysObj;
}
else {
// Default to weekdays
scheduleItem.days = {
'0': false, // Sunday
'1': true, // Monday
'2': true, // Tuesday
'3': true, // Wednesday
'4': true, // Thursday
'5': true, // Friday
'6': false, // Saturday
};
}
return scheduleItem;
});
}
if (Object.keys(schedule).length > 0) {
body.campaign_schedule = schedule;
}
}
return await generic_functions_1.instantlyApiRequest.call(context, 'PATCH', `/api/v2/campaigns/${campaignId}`, body);
}
/**
* Delete a campaign
*/
static async delete(context, itemIndex) {
const campaignId = (0, resourceLocatorHelpers_1.getCampaignId)(context, itemIndex);
return await generic_functions_1.instantlyApiRequest.call(context, 'DELETE', `/api/v2/campaigns/${campaignId}`);
}
}
exports.CampaignOperations = CampaignOperations;
//# sourceMappingURL=CampaignOperations.js.map