UNPKG

bento-n8n-sdk

Version:

n8n community node for Bento integration

1,252 lines 135 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Bento = void 0; const n8n_workflow_1 = require("n8n-workflow"); const buffer_1 = require("buffer"); const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; const INPUT_LIMITS = { EMAIL: 254, NAME: 50, SUBJECT: 200, CUSTOM_FIELD_KEY: 50, CUSTOM_FIELD_VALUE: 500, EVENT_NAME: 100, EVENT_PROPERTY_KEY: 50, EVENT_PROPERTY_VALUE: 500, HTML_CONTENT: 50000, TEXT_CONTENT: 50000, USER_ID: 254, IP_ADDRESS: 45, VALIDATE_NAME: 100, USER_AGENT: 512, SEGMENT_ID: 100, }; const DANGEROUS_HTML_PATTERNS = [ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, /javascript:/gi, /on\w+\s*=/gi, /<iframe\b[^>]*>/gi, /<object\b[^>]*>/gi, /<embed\b[^>]*>/gi, /<form\b[^>]*>/gi, /<input\b[^>]*>/gi, /<meta\b[^>]*>/gi, /<link\b[^>]*>/gi, /data:(?!image\/)/gi, /<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ]; const SECURE_ERROR_MESSAGES = { AUTHENTICATION_FAILED: 'Authentication failed. Please check your Bento API credentials.', INVALID_REQUEST: 'Request validation failed. Please check your input parameters.', API_ERROR: 'Bento API request failed. Please try again or contact support.', NETWORK_ERROR: 'Network error occurred. Please check your connection and try again.', VALIDATION_ERROR: 'Input validation failed. Please check your data format.', RATE_LIMITED: 'Rate limit exceeded. Please wait before making more requests.', SERVER_ERROR: 'Server error occurred. Please try again later.', UNKNOWN_ERROR: 'An unexpected error occurred. Please contact support if the issue persists.', }; const REQUEST_LIMITS = { MAX_PAYLOAD_SIZE: 1024 * 1024, DEFAULT_TIMEOUT: 30000, MAX_RETRIES: 3, RETRY_DELAY_BASE: 1000, RATE_LIMIT_DELAY: 60000, MAX_CONCURRENT_REQUESTS: 5, }; const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504]; const RETRYABLE_ERROR_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED']; function isValidEmail(email) { if (typeof email !== 'string' || email.trim() === '') { return false; } const trimmedEmail = email.trim(); if (trimmedEmail.length > 254) { return false; } if (!EMAIL_REGEX.test(trimmedEmail)) { return false; } if (trimmedEmail.includes('..')) { return false; } if (trimmedEmail.startsWith('.') || trimmedEmail.endsWith('.')) { return false; } const [localPart, domainPart] = trimmedEmail.split('@'); if (localPart.length > 64) { return false; } if (domainPart.length > 253) { return false; } const domainParts = domainPart.split('.'); if (domainParts.length < 2) { return false; } for (const part of domainParts) { if (part.length === 0 || part.length > 63) { return false; } if (part.startsWith('-') || part.endsWith('-')) { return false; } } return true; } function sanitizeEmail(email) { if (typeof email !== 'string') { return ''; } return email.trim().toLowerCase(); } function validateHtmlContent(html) { if (typeof html !== 'string') { return false; } for (const pattern of DANGEROUS_HTML_PATTERNS) { const regex = new RegExp(pattern.source, pattern.flags); if (regex.test(html)) { return false; } } return true; } function sanitizeHtmlContent(html) { if (typeof html !== 'string') { return ''; } let sanitized = html.trim(); for (const pattern of DANGEROUS_HTML_PATTERNS) { const regex = new RegExp(pattern.source, pattern.flags); sanitized = sanitized.replace(regex, ''); } sanitized = sanitized.replace(/javascript\s*:/gi, ''); sanitized = sanitized.replace(/vbscript\s*:/gi, ''); sanitized = sanitized.replace(/data\s*:/gi, ''); return sanitized; } function validateHtmlStructure(html) { const issues = []; if (typeof html !== 'string') { issues.push('HTML content must be a string'); return { isValid: false, issues }; } if (/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi.test(html)) { issues.push('Script tags are not allowed'); } if (/javascript:/gi.test(html)) { issues.push('JavaScript URLs are not allowed'); } if (/on\w+\s*=/gi.test(html)) { issues.push('Event handlers (onclick, onload, etc.) are not allowed'); } if (/<iframe\b[^>]*>/gi.test(html)) { issues.push('Iframe tags are not allowed'); } if (/<form\b[^>]*>/gi.test(html)) { issues.push('Form tags are not allowed in email content'); } if (/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi.test(html)) { issues.push('Style tags are not recommended (use inline styles instead)'); } const openTags = html.match(/<[^\/][^>]*>/g) || []; const closeTags = html.match(/<\/[^>]*>/g) || []; if (openTags.length !== closeTags.length) { issues.push('HTML may contain unclosed tags'); } return { isValid: issues.length === 0, issues }; } function validateInputLength(input, maxLength, fieldName, itemIndex) { if (typeof input === 'string' && input.length > maxLength) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${fieldName} exceeds maximum length of ${maxLength} characters (current: ${input.length})`, { itemIndex }); } } function createSecureErrorMessage(error, operation) { if (error.statusCode) { switch (error.statusCode) { case 400: return `${SECURE_ERROR_MESSAGES.INVALID_REQUEST} (Operation: ${operation})`; case 401: return SECURE_ERROR_MESSAGES.AUTHENTICATION_FAILED; case 403: return SECURE_ERROR_MESSAGES.AUTHENTICATION_FAILED; case 404: return `${SECURE_ERROR_MESSAGES.API_ERROR} Resource not found.`; case 429: return SECURE_ERROR_MESSAGES.RATE_LIMITED; case 500: case 502: case 503: case 504: return SECURE_ERROR_MESSAGES.SERVER_ERROR; default: return SECURE_ERROR_MESSAGES.API_ERROR; } } if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { return SECURE_ERROR_MESSAGES.NETWORK_ERROR; } if (error.message && error.message.includes('validation')) { return SECURE_ERROR_MESSAGES.VALIDATION_ERROR; } return SECURE_ERROR_MESSAGES.UNKNOWN_ERROR; } function logSecureError(error, operation, context) { var _a; this.logger.error('Bento Node Error', { operation, itemIndex: context.itemIndex, endpoint: context.endpoint, statusCode: error.statusCode, errorCode: error.code, hasMessage: !!error.message, messageLength: ((_a = error.message) === null || _a === void 0 ? void 0 : _a.length) || 0, timestamp: new Date().toISOString(), }); } function validatePayloadSize(body) { if (!body) return true; const payloadSize = JSON.stringify(body).length; return payloadSize <= REQUEST_LIMITS.MAX_PAYLOAD_SIZE; } function calculateBackoffDelay(attempt, baseDelay = REQUEST_LIMITS.RETRY_DELAY_BASE) { const exponentialDelay = baseDelay * Math.pow(2, attempt); const jitter = Math.random() * 0.1 * exponentialDelay; return Math.min(exponentialDelay + jitter, 30000); } function shouldRetryRequest(error, attempt) { if (attempt >= REQUEST_LIMITS.MAX_RETRIES) { return false; } if (error.statusCode && RETRYABLE_STATUS_CODES.includes(error.statusCode)) { return true; } if (error.code && RETRYABLE_ERROR_CODES.includes(error.code)) { return true; } return false; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const activeRequests = new Map(); const requestQueue = new Map(); async function acquireRequestSlot(nodeId) { const currentRequests = activeRequests.get(nodeId) || 0; if (currentRequests >= REQUEST_LIMITS.MAX_CONCURRENT_REQUESTS) { const queue = requestQueue.get(nodeId) || []; return new Promise((resolve) => { queue.push(resolve); requestQueue.set(nodeId, queue); }); } activeRequests.set(nodeId, currentRequests + 1); } function releaseRequestSlot(nodeId) { const currentRequests = activeRequests.get(nodeId) || 0; const newCount = Math.max(0, currentRequests - 1); activeRequests.set(nodeId, newCount); const queue = requestQueue.get(nodeId) || []; if (queue.length > 0 && newCount < REQUEST_LIMITS.MAX_CONCURRENT_REQUESTS) { const nextResolve = queue.shift(); if (nextResolve) { requestQueue.set(nodeId, queue); activeRequests.set(nodeId, newCount + 1); nextResolve(); } } } class Bento { constructor() { this.ai = true; this.aiCategory = 'automation'; this.supportsStreaming = false; this.inputType = 'json'; this.outputType = 'json'; this.description = { displayName: 'Bento', name: 'bento', icon: 'file:bento.svg', group: ['communication'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Native integration for Bento API actions', defaults: { name: 'Bento', }, inputs: ["main"], outputs: ["main"], credentials: [ { name: 'bentoApi', required: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Blacklist Check', value: 'blacklistCheck', description: 'Check an email against Bento\'s blacklist service', action: 'Check email against blacklist', }, { name: 'Content Moderation', value: 'contentModeration', description: 'Send content to Bento\'s moderation service', action: 'Moderate content', }, { name: 'Create Subscriber', value: 'createSubscriber', description: 'Add a new subscriber to your Bento audience with email and profile data', action: 'Create a subscriber', }, { name: 'Gender Guess', value: 'genderGuess', description: 'Predict subscriber gender using Bento\'s experimental classifier', action: 'Guess gender', }, { name: 'Geolocation Lookup', value: 'geolocationLookup', description: 'Look up location details for an IP address', action: 'Look up geolocation', }, { name: 'Get Subscriber', value: 'getSubscriber', description: 'Retrieve detailed information about an existing subscriber by email', action: 'Get a subscriber', }, { name: 'List Broadcasts', value: 'listBroadcasts', description: 'List Bento broadcasts with optional filters', action: 'List broadcasts', }, { name: 'Report Metrics', value: 'reportStats', description: 'Pull Bento analytics reports for broadcasts, automations, or revenue', action: 'Fetch report metrics', }, { name: 'Segment Metrics', value: 'segmentStats', description: 'Fetch segment-level analytics for a selected date range', action: 'Fetch segment metrics', }, { name: 'Send Broadcast', value: 'sendBroadcast', description: 'Send a broadcast immediately or at a scheduled time', action: 'Send a broadcast', }, { name: 'Send Transactional Email', value: 'sendTransactionalEmail', description: 'Send personalized transactional emails using Bento templates', action: 'Send a transactional email', }, { name: 'Site Metrics', value: 'siteStats', description: 'Fetch site-wide analytics for a selected date range', action: 'Fetch site metrics', }, { name: 'Subscriber Command', value: 'subscriberCommand', description: 'Execute commands on subscribers (add/remove tags, fields, subscribe/unsubscribe, etc.)', action: 'Execute a subscriber command', }, { name: 'Track Event', value: 'trackEvent', description: 'Record custom events and behaviors for subscriber segmentation and automation', action: 'Track an event', }, { name: 'Update Subscriber', value: 'updateSubscriber', description: 'Modify subscriber profile information and custom attributes', action: 'Update a subscriber', }, { name: 'Validate Email', value: 'validateEmail', description: 'Validate email address for spam/throwaway detection', action: 'Validate an email address', }, ], default: 'createSubscriber', }, { displayName: 'Email', name: 'email', type: 'string', required: true, displayOptions: { show: { operation: ['createSubscriber', 'getSubscriber', 'updateSubscriber'], }, }, default: '', placeholder: 'user@example.com', description: 'The email address of the subscriber - must be a valid email format', }, { displayName: 'First Name', name: 'firstName', type: 'string', displayOptions: { show: { operation: ['createSubscriber', 'updateSubscriber'], }, }, default: '', placeholder: 'John', description: 'The first name of the subscriber for personalization', }, { displayName: 'Last Name', name: 'lastName', type: 'string', displayOptions: { show: { operation: ['createSubscriber', 'updateSubscriber'], }, }, default: '', placeholder: 'Doe', description: 'The last name of the subscriber for personalization', }, { displayName: 'Custom Fields', name: 'customFields', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: ['createSubscriber', 'updateSubscriber'], }, }, default: {}, options: [ { name: 'field', displayName: 'Field', values: [ { displayName: 'Key', name: 'key', type: 'string', default: '', placeholder: 'company', description: 'Custom field name', }, { displayName: 'Value', name: 'value', type: 'string', default: '', placeholder: 'Acme Corp', description: 'Custom field value', }, ], }, ], description: 'Additional custom fields to store with the subscriber', }, { displayName: 'User ID', name: 'userId', type: 'string', required: true, displayOptions: { show: { operation: ['trackEvent'], }, }, default: '', placeholder: 'user123@example.com', description: 'The unique identifier for the user (typically email address)', }, { displayName: 'Event Name', name: 'eventName', type: 'string', required: true, displayOptions: { show: { operation: ['trackEvent'], }, }, default: '', placeholder: 'purchase_completed', description: 'The name of the custom event to track (e.g., purchase_completed, page_viewed)', }, { displayName: 'Event Properties', name: 'eventProperties', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: ['trackEvent'], }, }, default: {}, options: [ { name: 'property', displayName: 'Property', values: [ { displayName: 'Key', name: 'key', type: 'string', default: '', description: 'Property key', }, { displayName: 'Value', name: 'value', type: 'string', default: '', description: 'Property value', }, ], }, ], description: 'Additional properties and data to attach to the event for segmentation and personalization', }, { displayName: 'Recipient Email', name: 'recipientEmail', type: 'string', required: true, displayOptions: { show: { operation: ['sendTransactionalEmail'], }, }, default: '', placeholder: 'recipient@example.com', description: 'The email address of the recipient', }, { displayName: 'From Email', name: 'fromEmail', type: 'string', required: true, displayOptions: { show: { operation: ['sendTransactionalEmail'], }, }, default: '', placeholder: 'noreply@yourcompany.com', description: 'The sender email address', }, { displayName: 'Subject', name: 'subject', type: 'string', required: true, displayOptions: { show: { operation: ['sendTransactionalEmail'], }, }, default: '', placeholder: 'Reset Password', description: 'Email subject line', }, { displayName: 'Email Type', name: 'emailType', type: 'options', displayOptions: { show: { operation: ['sendTransactionalEmail'], }, }, options: [ { name: 'HTML Body', value: 'html', description: 'Send HTML formatted email', }, { name: 'Text Body', value: 'text', description: 'Send plain text email', }, ], default: 'html', description: 'Type of email body to send', }, { displayName: 'HTML Body', name: 'htmlBody', type: 'string', typeOptions: { rows: 8, }, displayOptions: { show: { operation: ['sendTransactionalEmail'], emailType: ['html'], }, }, default: '', placeholder: '<p>Here is a link to reset your password ... {{ link }}</p>', description: 'HTML content of the email (supports template variables like {{ variable_name }})', }, { displayName: 'Text Body', name: 'textBody', type: 'string', typeOptions: { rows: 8, }, displayOptions: { show: { operation: ['sendTransactionalEmail'], emailType: ['text'], }, }, default: '', placeholder: 'Here is a link to reset your password ... {{ link }}', description: 'Plain text content of the email (supports template variables like {{ variable_name }})', }, { displayName: 'Transactional', name: 'transactional', type: 'boolean', displayOptions: { show: { operation: ['sendTransactionalEmail'], }, }, default: false, description: 'Whether this is a transactional email (affects tracking and analytics)', }, { displayName: 'Personalizations', name: 'personalizations', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: ['sendTransactionalEmail'], }, }, default: {}, options: [ { name: 'personalization', displayName: 'Personalization', values: [ { displayName: 'Key', name: 'key', type: 'string', default: '', placeholder: 'link', description: 'Template variable name (without {{ }})', }, { displayName: 'Value', name: 'value', type: 'string', default: '', placeholder: 'https://example.com/reset', description: 'Value to substitute in template', }, ], }, ], description: 'Template variables to personalize the email (e.g., link, first_name, order_total)', }, { displayName: 'Email', name: 'commandEmail', type: 'string', required: true, displayOptions: { show: { operation: ['subscriberCommand'], }, }, default: '', placeholder: 'user@example.com', description: 'The email address of the subscriber to execute the command on', }, { displayName: 'Command', name: 'command', type: 'options', required: true, displayOptions: { show: { operation: ['subscriberCommand'], }, }, options: [ { name: 'Add Field', value: 'add_field', description: 'Add a custom field to the subscriber', }, { name: 'Add Tag', value: 'add_tag', description: 'Add a tag to the subscriber', }, { name: 'Add Tag via Event', value: 'add_tag_via_event', description: 'Add a tag to the subscriber via event', }, { name: 'Change Email', value: 'change_email', description: 'Change the email address of the subscriber', }, { name: 'Remove Field', value: 'remove_field', description: 'Remove a custom field from the subscriber', }, { name: 'Remove Tag', value: 'remove_tag', description: 'Remove a tag from the subscriber', }, { name: 'Subscribe', value: 'subscribe', description: 'Subscribe the email address', }, { name: 'Unsubscribe', value: 'unsubscribe', description: 'Unsubscribe the email address', }, ], default: 'add_tag', description: 'The command to execute on the subscriber', }, { displayName: 'Tag/Field Name', name: 'query', type: 'string', displayOptions: { show: { operation: ['subscriberCommand'], command: ['add_tag', 'add_tag_via_event', 'remove_tag', 'remove_field'], }, }, default: '', placeholder: 'vip_customer', description: 'The name of the tag or field to add/remove', }, { displayName: 'Field Key', name: 'fieldKey', type: 'string', displayOptions: { show: { operation: ['subscriberCommand'], command: ['add_field'], }, }, default: '', placeholder: 'company', description: 'The key/name of the custom field', }, { displayName: 'Field Value', name: 'fieldValue', type: 'string', displayOptions: { show: { operation: ['subscriberCommand'], command: ['add_field'], }, }, default: '', placeholder: 'Acme Corp', description: 'The value of the custom field', }, { displayName: 'New Email', name: 'newEmail', type: 'string', displayOptions: { show: { operation: ['subscriberCommand'], command: ['change_email'], }, }, default: '', placeholder: 'newemail@example.com', description: 'The new email address for the subscriber', }, { displayName: 'Email', name: 'validateEmail', type: 'string', required: true, displayOptions: { show: { operation: ['validateEmail'], }, }, default: '', placeholder: 'test@example.com', description: 'The email address to validate', }, { displayName: 'Name', name: 'validateName', type: 'string', displayOptions: { show: { operation: ['validateEmail'], }, }, default: '', placeholder: 'John Doe', description: 'The name associated with the email (optional but recommended for better validation)', }, { displayName: 'IP Address', name: 'validateIp', type: 'string', displayOptions: { show: { operation: ['validateEmail'], }, }, default: '', placeholder: '1.1.1.1', description: 'The IP address associated with the email (optional but recommended for better validation)', }, { displayName: 'Domain', name: 'blacklistDomain', type: 'string', required: true, displayOptions: { show: { operation: ['blacklistCheck'], }, }, default: '', placeholder: 'test.com', description: 'Domain to evaluate against Bento\'s blacklist service', }, { displayName: 'IP Address', name: 'blacklistIp', type: 'string', displayOptions: { show: { operation: ['blacklistCheck'], }, }, default: '', placeholder: '123.45.67.89', description: 'Optional IP address to include in the blacklist evaluation', }, { displayName: 'Content', name: 'moderationContent', type: 'string', typeOptions: { rows: 6, }, required: true, displayOptions: { show: { operation: ['contentModeration'], }, }, default: '', placeholder: 'Message text to evaluate...', description: 'Content that should be evaluated by Bento\'s moderation service', }, { displayName: 'Metadata', name: 'moderationMetadata', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: ['contentModeration'], }, }, default: {}, description: 'Optional metadata to provide additional context for moderation (key/value pairs)', options: [ { name: 'metadata', displayName: 'Metadata', values: [ { displayName: 'Key', name: 'key', type: 'string', default: '', placeholder: 'source', }, { displayName: 'Value', name: 'value', type: 'string', default: '', placeholder: 'contact_form', }, ], }, ], }, { displayName: 'Email', name: 'genderGuessEmail', type: 'string', displayOptions: { show: { operation: ['genderGuess'], }, }, default: '', placeholder: 'subscriber@example.com', description: 'Email address associated with the person (optional but improves accuracy)', }, { displayName: 'First Name', name: 'genderGuessFirstName', type: 'string', displayOptions: { show: { operation: ['genderGuess'], }, }, default: '', description: 'First name to help the gender guess service', }, { displayName: 'Last Name', name: 'genderGuessLastName', type: 'string', displayOptions: { show: { operation: ['genderGuess'], }, }, default: '', description: 'Last name to help the gender guess service', }, { displayName: 'IP Address', name: 'geolocationIp', type: 'string', required: true, displayOptions: { show: { operation: ['geolocationLookup'], }, }, default: '', placeholder: '203.0.113.42', description: 'IP address to look up', }, { displayName: 'User Agent', name: 'geolocationUserAgent', type: 'string', displayOptions: { show: { operation: ['geolocationLookup'], }, }, default: '', description: 'Optional user agent string for additional context', }, { displayName: 'Segment ID', name: 'segmentStatsSegmentId', type: 'string', required: true, displayOptions: { show: { operation: ['segmentStats'], }, }, default: '', placeholder: 'seg_12345', description: 'Unique identifier of the Bento segment to analyze', }, { displayName: 'Report ID', name: 'reportStatsReportId', type: 'string', required: true, displayOptions: { show: { operation: ['reportStats'], }, }, default: '', placeholder: '456', description: 'Identifier of the Bento report (report_id) to fetch metrics for', }, { displayName: 'Status', name: 'listBroadcastsStatus', type: 'options', noDataExpression: true, displayOptions: { show: { operation: ['listBroadcasts'], }, }, options: [ { name: 'Any', value: 'any', }, { name: 'Archived', value: 'archived', }, { name: 'Draft', value: 'draft', }, { name: 'Scheduled', value: 'scheduled', }, { name: 'Sending', value: 'sending', }, { name: 'Sent', value: 'sent', }, ], default: 'any', description: 'Filter broadcasts by status', }, { displayName: 'Created After', name: 'listBroadcastsCreatedAfter', type: 'dateTime', displayOptions: { show: { operation: ['listBroadcasts'], }, }, default: '', description: 'Return broadcasts created after this date', }, { displayName: 'Tag IDs', name: 'listBroadcastsTagIds', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: ['listBroadcasts'], }, }, default: {}, description: 'Filter broadcasts linked to specific tag IDs', options: [ { name: 'tagId', displayName: 'Tag ID', values: [ { displayName: 'Tag ID', name: 'id', type: 'string', default: '', placeholder: 'tag_12345', }, ], }, ], }, { displayName: 'Campaign Name', name: 'sendBroadcastName', type: 'string', required: true, displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'Campaign #1 — Plain Text Example', description: 'Name to assign to the broadcast campaign', }, { displayName: 'Subject', name: 'sendBroadcastSubject', type: 'string', required: true, displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'Hello Plain World', description: 'Email subject line', }, { displayName: 'Content', name: 'sendBroadcastContent', type: 'string', typeOptions: { rows: 5, }, required: true, displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', description: 'Email content (plain text or HTML based on Content Type)', }, { displayName: 'Content Type', name: 'sendBroadcastType', type: 'options', noDataExpression: true, displayOptions: { show: { operation: ['sendBroadcast'], }, }, options: [ { name: 'Plain Text', value: 'plain', }, { name: 'HTML', value: 'html', }, ], default: 'plain', description: 'Render the campaign as plain text or HTML', }, { displayName: 'From Email', name: 'sendBroadcastFromEmail', type: 'string', required: true, displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'example@example.com', description: 'Email address shown to recipients', }, { displayName: 'From Name', name: 'sendBroadcastFromName', type: 'string', required: true, displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'John Doe', description: 'Display name shown to recipients', }, { displayName: 'Approved', name: 'sendBroadcastApproved', type: 'boolean', displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: false, description: 'Whether to mark the broadcast as approved for sending', }, { displayName: 'Inclusive Tags', name: 'sendBroadcastInclusiveTags', type: 'string', displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'lead,mql', description: 'Comma-separated tags to include in the audience', }, { displayName: 'Exclusive Tags', name: 'sendBroadcastExclusiveTags', type: 'string', displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'customers', description: 'Comma-separated tags to exclude from the audience', }, { displayName: 'Segment ID', name: 'sendBroadcastSegmentId', type: 'string', displayOptions: { show: { operation: ['sendBroadcast'], }, }, default: '', placeholder: 'segment_123456789', description: 'Segment ide