feat(SendInBlue Node): Add SendInBlue Regular + Trigger Node (#3746)

* add sendinblue svg icon

* Add code and required files for new sendinblue node

* Add node to package.json

* Update credentials to display API Key instead of Access Token

* Use new svg found in brandfetch

*  Improvements

* ♻️ Moved descriptions for email to it's own file

*  Added support for contact get

*  moved email descriptions to it's own file

*  Add logic to conditionally remove/format sms,email

*  Improvements

*  Refactor Sender descriptions to it's own file

*  Fix urls

*  Improvements attempt

*  Refactor remove inline descriptions

*  Minor improvement

* 🎨 Learn a nice way to send options as key-value

*  Improvements

* ♻️ Fix Create Operation structure

* ♻️ Refactor create functionality for attribute

♻️ Introduce override for createAttribute selectedCategory

♻️ Add delete functionality

* 🔥 Remove preSend from delete

*  Implement override for body types

*  Cleanup node file

*  Update response for contact update
 Update request url for contact delete

*  Add presend check for optional properties that are empty
 Add Model file and TransactionalEmail interface

*  formatting

* ♻️ Remove requestOperations from Node Description level

* ♻️ Cleanup routing for Get All
♻️ Make Identifier required

*  Formatting

* ♻️ Add Options Collection

* ♻️ Add Filters area

* ♻️ Formatting

* ♻️ Handle empty return

* ♻️ Remove unused code

* ♻️ Fix pagination
♻️ Fix empty return for delete

*  Add pagination

*  Fix Modified Since

* ♻️ Reorder send operation ui

*  Remove no longer needed presend
 Add send html template operation

* ♻️ Make Contact Attribute name and type required

* ♻️ Rename Attribute to Contact Attribute

* ♻️ Rename Identifier to Contact Identifier

* ♻️ Remove SMS from root level because it can exist in Contact Attributes

* ♻️ Fix Array type using 'Array<T>'
♻️ Fix double quotes should be single quotes

* 👕 Lint Fix

*  Add email attachment functionality
 Add attachment data validation

*  Add dynamic loading of Email Template IDs

* ♻️ Cleanup validation method

*  Introduce workaround and use binary data for attachments

* feat: Migrated to npm release of riot-tmpl fork.

* 👕 Lint fix rules

* 👕 Lint fix rules

* fix: Updated imports to use @n8n_io/riot-tmpl

* fix: Fixed Logger.ts types.

*  Fix mixmatch of filename and package.json credentials list

*  fix mixmatch in nodes list

* feat(core): Give access to getBinaryDataBuffer in preSend method

*  clean up mixmatches in node naming

* ♻️ Refactor code to use newly exposed getBinaryDataBuffer method

*  Improvements

* 🔥 Remove unnecessary lines

* 👕 Fix linting issues

*  Fix issues with up to date APIs and improve readability

*  update naming of files

* ♻️ Move sendHtml boolean above subject
♻️ Update naming from Parameters to Fields

* ♻️ Move sendHtml boolean above subject
♻️ Update naming from Parameters to Fields

* ♻️ Add attribute name url encoding
♻️ Change limit's default to 50

*  Fix default for templateId

*  Fix display name for attribute list

* ♻️ Add clarity to attribute value display name

* ♻️ Add tags and attachments for emails

* ♻️ Add use of item's binary data fileName

* 👕 Fix action lint rule

* 👕 Remove deprecated lint rule

* ⬆️ Update eslint-plugin-n8n-nodes-base

* 👕 Fix lint rule for file name

*  Fix update attribute

* ♻️ Add upsert capabilites

* 🔥 Remove create or update operation

* ♻️ Add sendInBlueWebhookApi namespace

* ♻️ Add Webhook API functionality

*  Add SendInBlue Trigger

*  Return correct webhookId data

*  Add placeholder for receiving data

* 👕 Fixing existing linting issues

* 🚨 Enable namespacing in tslint file

* 👕 Fix linting issues

*  Rename exported WebhookApi

* 🔥 Remove unused Model.ts file

* ♻️ Update node to use SendInBlue namespace

*  Revert back to allowing upsert functionality

* ♻️ Fix options to better describe events

* Remove update flag for create operation

* ♻️ Fix discrepancies for contact resource

* remove no-namespace lint rule

* 👕 Fix linting issues

* ♻️ Add sendInBlueWebhookApi namespace

* ♻️ Add Webhook API functionality

*  Add SendInBlue Trigger

*  Return correct webhookId data

*  Add placeholder for receiving data

* 👕 Fix linting issues

*  Rename exported WebhookApi

* ♻️ Fix options to better describe events

* Add optionswithuri import that was lost

*  Fix details from janober's review

*  Fix order of displayName and name properties

*  Fix default value and improve loadOptions

*  Introduce support for comma separated attribute values

*  Introduce support for comma separated attribute values

* 👕 Fix linting issues

* Update defaults and required props

*  Fix copy paste issue Upsert was not using correct endpoint

*  Fix upsert email field display name

*  Last update, upsert email description

*  Add PostReceived type limit

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
brianinoa 2022-08-03 18:08:51 +02:00 committed by GitHub
parent b22ff1f5c1
commit 74cedd94a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 2691 additions and 13 deletions

View file

@ -0,0 +1,34 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class SendInBlueApi implements ICredentialType {
name = 'sendInBlueApi';
displayName = 'SendInBlue';
documentationUrl = 'sendInBlueApi';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
'api-key': '={{$credentials.apiKey}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.sendinblue.com/v3',
url: '/account',
},
};
}

View file

@ -0,0 +1,532 @@
import {
IExecuteSingleFunctions,
IHttpRequestOptions,
INodeExecutionData,
INodeProperties,
JsonObject,
} from 'n8n-workflow';
import { SendInBlueNode } from './GenericFunctions';
export const attributeOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['attribute'],
},
},
options: [
{
name: 'Create',
value: 'create',
routing: {
request: {
method: 'POST',
url: '=/v3/contacts/attributes/{{$parameter.attributeCategory}}/{{encodeURI($parameter.attributeName)}}',
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}', // Also possible to use the original response data
},
},
],
},
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const selectedCategory = this.getNodeParameter('attributeCategory') as string;
const override = SendInBlueNode.INTERCEPTORS.get(selectedCategory);
if (override) {
override.call(this, requestOptions.body! as JsonObject);
}
return requestOptions;
},
],
},
},
action: 'Create an attribute',
},
{
name: 'Update',
value: 'update',
routing: {
request: {
method: 'PUT',
url: '=/v3/contacts/attributes/{{$parameter.updateAttributeCategory}}/{{encodeURI($parameter.updateAttributeName)}}',
},
},
action: 'Update an attribute',
},
{
name: 'Delete',
value: 'delete',
routing: {
request: {
method: 'DELETE',
url: '=/v3/contacts/attributes/{{$parameter.deleteAttributeCategory}}/{{encodeURI($parameter.deleteAttributeName)}}',
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}', // Also possible to use the original response data
},
},
],
},
},
action: 'Delete an attribute',
},
{
name: 'Get All',
value: 'getAll',
routing: {
request: {
method: 'GET',
url: 'v3/contacts/attributes',
},
send: {
paginate: false,
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'attributes',
},
},
],
},
},
action: 'Get all attributes',
},
],
default: 'create',
},
];
const createAttributeOperations: INodeProperties[] = [
{
displayName: 'Category',
name: 'attributeCategory',
default: 'normal',
description: 'Category of the attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['create'],
},
},
options: [
{
name: 'Calculated',
value: 'calculated',
},
{
name: 'Category',
value: 'category',
},
{
name: 'Global',
value: 'global',
},
{
name: 'Normal',
value: 'normal',
},
{
name: 'Transactional',
value: 'transactional',
},
],
type: 'options',
required: true,
},
{
displayName: 'Name',
name: 'attributeName',
default: '',
description: 'Name of the attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['create'],
},
},
required: true,
type: 'string',
},
{
displayName: 'Type',
name: 'attributeType',
default: '',
description: 'Attribute Type',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['create'],
attributeCategory: ['normal'],
},
},
options: [
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Date',
value: 'date',
},
{
name: 'Float',
value: 'float',
},
{
name: 'Text',
value: 'text',
},
],
required: true,
type: 'options',
routing: {
send: {
type: 'body',
property: 'type',
value: '={{$value}}',
},
},
},
{
displayName: 'Value',
name: 'attributeValue',
default: '',
description: 'Value of the attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['create'],
attributeCategory: ['global', 'calculated'],
},
},
type: 'string',
placeholder: '',
required: true,
routing: {
send: {
type: 'body',
property: 'value',
value: '={{$value}}',
},
},
},
{
displayName: 'Contact Attribute List',
name: 'attributeCategoryList',
type: 'collection',
placeholder: 'Add Attributes',
default: {},
displayOptions: {
show: {
resource: ['attribute'],
operation: ['create'],
attributeCategory: ['category'],
},
},
options: [
{
displayName: 'Contact Attributes',
name: 'categoryEnumeration',
placeholder: 'Add Attribute',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attributesValues',
displayName: 'Attribute',
values: [
{
displayName: 'Value ID',
name: 'attributeCategoryValue',
type: 'number',
default: 1,
description: 'ID of the value, must be numeric',
routing: {
send: {
value: '={{$value}}',
property: '=enumeration[{{$index}}].value',
type: 'body',
},
},
},
{
displayName: 'Label',
name: 'attributeCategoryLabel',
type: 'string',
default: '',
routing: {
send: {
value: '={{$value}}',
property: '=enumeration[{{$index}}].label',
type: 'body',
},
},
description: 'Label of the value',
},
],
},
],
default: {},
description: 'List of values and labels that the attribute can take',
},
],
},
];
const updateAttributeOperations: INodeProperties[] = [
{
displayName: 'Category',
name: 'updateAttributeCategory',
default: 'calculated',
description: 'Category of the attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['update'],
},
},
options: [
{
name: 'Calculated',
value: 'calculated',
},
{
name: 'Category',
value: 'category',
},
{
name: 'Global',
value: 'global',
},
],
type: 'options',
},
{
displayName: 'Name',
name: 'updateAttributeName',
default: '',
description: 'Name of the existing attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['update'],
},
},
type: 'string',
},
{
displayName: 'Value',
name: 'updateAttributeValue',
default: '',
description: 'Value of the attribute to update',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['update'],
},
hide: {
updateAttributeCategory: ['category'],
},
},
type: 'string',
placeholder: '',
routing: {
send: {
type: 'body',
property: 'value',
value: '={{$value}}',
},
},
},
{
displayName: 'Update Fields',
name: 'updateAttributeCategoryList',
default: {},
description: 'List of the values and labels that the attribute can take',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['update'],
updateAttributeCategory: ['category'],
},
},
options: [
{
displayName: 'Contact Attributes',
name: 'updateCategoryEnumeration',
placeholder: 'Add Attribute',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'updateAttributesValues',
displayName: 'Attribute',
values: [
{
displayName: 'Value',
name: 'attributeCategoryValue',
type: 'number',
default: 1,
description: 'ID of the value, must be numeric',
routing: {
send: {
value: '={{$value}}',
property: '=enumeration[{{$index}}].value',
type: 'body',
},
},
},
{
displayName: 'Label',
name: 'attributeCategoryLabel',
type: 'string',
default: '',
routing: {
send: {
value: '={{$value}}',
property: '=enumeration[{{$index}}].label',
type: 'body',
},
},
description: 'Label of the value',
},
],
},
],
default: {},
description: 'List of values and labels that the attribute can take',
},
],
},
];
const deleteAttribueOperations: INodeProperties[] = [
{
displayName: 'Category',
name: 'deleteAttributeCategory',
default: 'normal',
description: 'Category of the attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['delete'],
},
},
options: [
{
name: 'Calculated',
value: 'calculated',
},
{
name: 'Category',
value: 'category',
},
{
name: 'Global',
value: 'global',
},
{
name: 'Normal',
value: 'normal',
},
{
name: 'Transactional',
value: 'transactional',
},
],
type: 'options',
},
{
displayName: 'Name',
name: 'deleteAttributeName',
default: '',
description: 'Name of the attribute',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['delete'],
},
},
type: 'string',
},
];
const getAllAttributeOperations: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['attribute'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
routing: {
output: {
postReceive: [
{
type: 'limit',
properties: {
maxResults: '={{$value}}',
},
},
],
},
},
default: 50,
description: 'Max number of results to return',
},
];
export const attributeFields: INodeProperties[] = [
...createAttributeOperations,
...updateAttributeOperations,
...deleteAttribueOperations,
...getAllAttributeOperations,
];

View file

@ -0,0 +1,624 @@
import {
GenericValue,
IExecuteSingleFunctions,
IHttpRequestOptions,
INodeProperties,
JsonObject,
} from 'n8n-workflow';
export const contactOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['contact'],
},
},
options: [
{
name: 'Create',
value: 'create',
action: 'Create a contact',
routing: {
request: {
method: 'POST',
url: '/v3/contacts',
},
},
},
{
name: 'Create or Update',
value: 'upsert',
action: 'Upsert a contact',
routing: {
request: {
method: 'POST',
url: '=/v3/contacts',
},
},
},
{
name: 'Delete',
value: 'delete',
action: 'Delete a contact',
},
{
name: 'Get',
value: 'get',
action: 'Get a contact',
},
{
name: 'Get All',
value: 'getAll',
routing: {
request: {
method: 'GET',
url: '/v3/contacts',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'contacts',
},
},
],
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit',
offsetParameter: 'offset',
pageSize: 1000,
type: 'query',
},
},
},
},
action: 'Get all contacts',
},
{
name: 'Update',
value: 'update',
routing: {
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}', // Also possible to use the original response data
},
},
],
},
},
action: 'Update a contact',
},
],
default: 'create',
},
];
const createOperations: INodeProperties[] = [
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
displayOptions: {
show: {
resource: ['contact'],
operation: ['create'],
},
},
default: '',
routing: {
send: {
type: 'body',
property: 'email',
},
},
},
{
displayName: 'Contact Attributes',
name: 'createContactAttributes',
default: {},
description: 'Array of attributes to be added',
displayOptions: {
show: {
resource: ['contact'],
operation: ['create'],
},
},
options: [
{
name: 'attributesValues',
displayName: 'Attribute',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
type: 'options',
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '/v3/contacts/attributes',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'attributes',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.name}} - ({{$responseItem.category}})',
value: '={{$responseItem.name}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
default: '',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
default: '',
routing: {
send: {
value: '={{$value}}',
property: '=attributes.{{$parent.fieldName}}',
type: 'body',
},
},
},
],
},
],
placeholder: 'Add Attribute',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
];
const getAllOperations: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
routing: {
send: {
paginate: '={{$value}}',
},
},
displayOptions: {
show: {
resource: ['contact'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['contact'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
send: {
type: 'query',
property: 'limit',
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
displayOptions: {
show: {
resource: ['contact'],
operation: ['getAll'],
},
},
default: {},
options: [
{
displayName: 'Sort',
name: 'sort',
type: 'options',
options: [
{ name: 'DESC', value: 'desc' },
{ name: 'ASC', value: 'asc' },
],
routing: {
send: {
type: 'query',
property: 'sort',
value: '={{$value}}',
},
},
default: 'desc',
description: 'Sort the results in the ascending/descending order of record creation',
},
],
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
displayOptions: {
show: {
resource: ['contact'],
operation: ['getAll'],
},
},
default: {},
options: [
{
displayName: 'Modified Since',
name: 'modifiedSince',
type: 'dateTime',
routing: {
send: {
type: 'query',
property: 'modifiedSince',
},
},
default: '',
description:
'Filter (urlencoded) the contacts modified after a given UTC date-time (YYYY-MM-DDTHH:mm:ss.SSSZ)',
},
],
},
];
const getOperations: INodeProperties[] = [
{
displayName: 'Contact Identifier',
name: 'identifier',
type: 'string',
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
},
},
routing: {
request: {
method: 'GET',
url: '=/v3/contacts/{{encodeURIComponent($value)}}',
},
},
required: true,
default: '',
description: 'Email (urlencoded) OR ID of the contact OR its SMS attribute value',
},
];
const deleteOperations: INodeProperties[] = [
{
displayName: 'Contact Identifier',
name: 'identifier',
type: 'string',
displayOptions: {
show: {
resource: ['contact'],
operation: ['delete'],
},
},
routing: {
request: {
method: 'DELETE',
url: '=/v3/contacts/{{encodeURIComponent($parameter.identifier)}}',
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}', // Also possible to use the original response data
},
},
],
},
},
default: '',
description: 'Email (urlencoded) OR ID of the contact OR its SMS attribute value',
},
];
const updateOperations: INodeProperties[] = [
{
displayName: 'Contact Identifier',
name: 'identifier',
default: '',
description: 'Email (urlencoded) OR ID of the contact OR its SMS attribute value',
displayOptions: {
show: {
resource: ['contact'],
operation: ['update'],
},
},
type: 'string',
required: true,
},
{
displayName: 'Attributes',
name: 'updateAttributes',
default: {},
description: 'Array of attributes to be updated',
displayOptions: {
show: {
resource: ['contact'],
operation: ['update'],
},
},
options: [
{
displayName: 'Attribute',
name: 'updateAttributesValues',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
type: 'options',
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '/v3/contacts/attributes',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'attributes',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.name}} - ({{$responseItem.category}})',
value: '={{$responseItem.name}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
default: '',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
default: '',
routing: {
send: {
value: '={{$value}}',
property: '=attributes.{{$parent.fieldName}}',
type: 'body',
},
},
},
],
},
],
placeholder: 'Add Attribute',
routing: {
request: {
method: 'PUT',
url: '=/v3/contacts/{{encodeURIComponent($parameter.identifier)}}',
},
},
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
];
const upsertOperations: INodeProperties[] = [
{
displayName: 'Email',
name: 'email',
default: '',
description: 'Email of the contact',
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
type: 'string',
placeholder: 'name@email.com',
required: true,
routing: {
send: {
value: '={{$value}}',
property: 'email',
type: 'body',
},
},
},
{
displayName: 'Contact Attributes',
name: 'upsertAttributes',
default: {},
description: 'Array of attributes to be updated',
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
options: [
{
name: 'upsertAttributesValues',
displayName: 'Attribute',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
type: 'options',
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '/v3/contacts/attributes',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'attributes',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.name}} - ({{$responseItem.category}})',
value: '={{$responseItem.name}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
default: '',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
default: '',
routing: {
send: {
value: '={{$value}}',
property: '=attributes.{{$parent.fieldName}}',
type: 'body',
},
},
},
],
},
],
placeholder: 'Add Attribute',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const { body } = requestOptions as GenericValue as JsonObject;
Object.assign(body!, { updateEnabled: true });
return requestOptions;
},
],
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}', // Also possible to use the original response data
},
},
],
},
},
},
];
export const contactFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* contact:create */
/* -------------------------------------------------------------------------- */
...createOperations,
/* -------------------------------------------------------------------------- */
/* contact:getAll */
/* -------------------------------------------------------------------------- */
...getAllOperations,
/* -------------------------------------------------------------------------- */
/* contact:get */
/* -------------------------------------------------------------------------- */
...getOperations,
/* -------------------------------------------------------------------------- */
/* contact:delete */
/* -------------------------------------------------------------------------- */
...deleteOperations,
/* -------------------------------------------------------------------------- */
/* contact:update */
/* -------------------------------------------------------------------------- */
...updateOperations,
/* -------------------------------------------------------------------------- */
/* contact:update */
/* -------------------------------------------------------------------------- */
...upsertOperations,
];

View file

@ -0,0 +1,451 @@
import { IExecuteSingleFunctions, IHttpRequestOptions, INodeProperties } from 'n8n-workflow';
import { SendInBlueNode } from './GenericFunctions';
export const emailOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['email'],
},
},
options: [
{
name: 'Send',
value: 'send',
action: 'Send a transactional email',
},
{
name: 'Send Template',
value: 'sendTemplate',
action: 'Send an email with an existing Template',
},
],
routing: {
request: {
method: 'POST',
url: '/v3/smtp/email',
},
},
default: 'send',
},
];
const sendHtmlEmailFields: INodeProperties[] = [
{
displayName: 'Send HTML',
name: 'sendHTML',
type: 'boolean',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
default: false,
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
routing: {
send: {
property: 'subject',
type: 'body',
},
},
default: '',
description: 'Subject of the email',
},
{
displayName: 'Text Content',
name: 'textContent',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
sendHTML: [false],
},
},
routing: {
send: {
property: 'textContent',
type: 'body',
},
},
default: '',
description: 'Text content of the message',
},
{
displayName: 'HTML Content',
name: 'htmlContent',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
sendHTML: [true],
},
},
routing: {
send: {
property: 'htmlContent',
type: 'body',
},
},
default: '',
description: 'HTML content of the message',
},
{
displayName: 'Sender',
name: 'sender',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
default: '',
required: true,
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileSenderEmail],
},
},
},
{
displayName: 'Receipients',
name: 'receipients',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
default: '',
required: true,
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileReceipientEmails],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
placeholder: 'Add Field',
description: 'Additional fields to add',
type: 'collection',
default: {},
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
options: [
{
displayName: 'Attachments',
name: 'emailAttachments',
placeholder: 'Add Attachment',
type: 'fixedCollection',
default: {},
options: [
{
name: 'attachment',
displayName: 'Attachment Data',
values: [
{
default: '',
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
description:
'The name of the incoming field containing the binary file data to be processed',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileAttachmentsData],
},
},
},
{
displayName: 'Receipients BCC',
name: 'receipientsBCC',
placeholder: 'Add BCC',
type: 'fixedCollection',
default: {},
options: [
{
name: 'receipientBcc',
displayName: 'Receipient',
values: [
{
name: 'bcc',
displayName: 'Receipient',
type: 'string',
default: '',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileBCCEmails],
},
},
},
{
displayName: 'Receipients CC',
name: 'receipientsCC',
placeholder: 'Add CC',
type: 'fixedCollection',
default: {},
options: [
{
name: 'receipientCc',
displayName: 'Receipient',
values: [
{
name: 'cc',
displayName: 'Receipient',
type: 'string',
default: '',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileCCEmails],
},
},
},
{
displayName: 'Email Tags',
name: 'emailTags',
default: {},
description: 'Add tags to your emails to find them more easily',
placeholder: 'Add Email Tags',
type: 'fixedCollection',
options: [
{
displayName: 'Tags',
name: 'tags',
values: [
{
default: '',
displayName: 'Tag',
name: 'tag',
type: 'string',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileTags],
},
},
},
],
},
];
const sendHtmlTemplateEmailFields: INodeProperties[] = [
{
displayName: 'Template ID',
name: 'templateId',
type: 'options',
default: '',
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '/v3/smtp/templates',
qs: {
templateStatus: true,
limit: 1000,
offset: 0,
sort: 'desc',
},
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'templates',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.name}}',
value: '={{$responseItem.id}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
routing: {
send: {
type: 'body',
property: 'templateId',
},
},
},
{
displayName: 'Receipients',
name: 'receipients',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
default: '',
required: true,
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileReceipientEmails],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
description: 'Additional fields to add',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
options: [
{
displayName: 'Attachments',
name: 'emailAttachments',
placeholder: 'Add Attachment',
type: 'fixedCollection',
default: {},
options: [
{
displayName: 'Attachment Data',
name: 'attachment',
values: [
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
default: '',
type: 'string',
description:
'The name of the incoming field containing the binary file data to be processed',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileAttachmentsData],
},
},
},
{
displayName: 'Email Tags',
name: 'emailTags',
default: {},
description: 'Add tags to your emails to find them more easily',
placeholder: 'Add Email Tags',
type: 'fixedCollection',
options: [
{
displayName: 'Tags',
name: 'tags',
values: [
{
default: '',
displayName: 'Tag',
name: 'tag',
type: 'string',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileTags],
},
},
},
{
displayName: 'Template Parameters',
name: 'templateParameters',
default: {},
description: 'Pass a set of attributes to customize the template',
placeholder: 'Add Parameter',
type: 'fixedCollection',
options: [
{
name: 'parameterValues',
displayName: 'Parameters',
values: [
{
displayName: 'Parameter',
name: 'parmeters',
type: 'string',
default: '',
placeholder: 'key=value',
description: 'Comma-separated key=value pairs',
},
],
},
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileTemplateParameters],
},
},
},
],
},
];
export const emailFields: INodeProperties[] = [
...sendHtmlEmailFields,
...sendHtmlTemplateEmailFields,
];

View file

@ -0,0 +1,388 @@
import {
IExecuteSingleFunctions,
IHookFunctions,
IHttpRequestOptions,
IWebhookFunctions,
JsonObject,
NodeOperationError,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import MailComposer from 'nodemailer/lib/mail-composer';
export namespace SendInBlueNode {
type ValidEmailFields = { to: string } | { sender: string } | { cc: string } | { bcc: string };
type Address = { address: string; name?: string };
type Email = { email: string; name?: string };
type ToEmail = { to: Email[] };
type SenderEmail = { sender: Email };
type CCEmail = { cc: Email[] };
type BBCEmail = { bbc: Email[] };
type ValidatedEmail = ToEmail | SenderEmail | CCEmail | BBCEmail;
enum OVERRIDE_MAP_VALUES {
'CATEGORY' = 'category',
'NORMAL' = 'boolean',
'TRANSACTIONAL' = 'id',
}
enum OVERRIDE_MAP_TYPE {
'CATEGORY' = 'category',
'NORMAL' = 'normal',
'TRANSACTIONAL' = 'transactional',
}
export const INTERCEPTORS = new Map<string, (body: JsonObject) => void>([
[
OVERRIDE_MAP_TYPE.CATEGORY,
(body: JsonObject) => {
body!.type = OVERRIDE_MAP_VALUES.CATEGORY;
},
],
[
OVERRIDE_MAP_TYPE.NORMAL,
(body: JsonObject) => {
body!.type = OVERRIDE_MAP_VALUES.NORMAL;
},
],
[
OVERRIDE_MAP_TYPE.TRANSACTIONAL,
(body: JsonObject) => {
body!.type = OVERRIDE_MAP_VALUES.TRANSACTIONAL;
},
],
]);
export namespace Validators {
export async function validateAndCompileAttachmentsData(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const dataPropertyList = this.getNodeParameter(
'additionalFields.emailAttachments.attachment',
) as JsonObject;
const { body } = requestOptions;
const { attachment = [] } = body as { attachment: Array<{ content: string; name: string }> };
try {
const { binaryPropertyName } = dataPropertyList;
const dataMappingList = (binaryPropertyName as string).split(',');
for (const attachmentDataName of dataMappingList) {
const binaryPropertyName = attachmentDataName;
const item = this.getInputData();
if (item.binary![binaryPropertyName as string] === undefined) {
throw new NodeOperationError(
this.getNode(),
`No binary data property “${binaryPropertyName}” exists on item!`,
);
}
const bufferFromIncomingData = (await this.helpers.getBinaryDataBuffer(
binaryPropertyName,
)) as Buffer;
const {
data: content,
mimeType,
fileName,
fileExtension,
} = await this.helpers.prepareBinaryData(bufferFromIncomingData);
const itemIndex = this.getItemIndex();
const name = getFileName(
itemIndex,
mimeType,
fileExtension,
fileName || item.binary!.data.fileName,
);
attachment.push({ content, name });
}
Object.assign(body!, { attachment });
return requestOptions;
} catch (err) {
throw new NodeOperationError(this.getNode(), `${err}`);
}
}
export async function validateAndCompileTags(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const { tag } = this.getNodeParameter('additionalFields.emailTags.tags') as JsonObject;
const tags = (tag as string)
.split(',')
.map((tag) => tag.trim())
.filter((tag) => {
return tag !== '';
});
const { body } = requestOptions;
Object.assign(body!, { tags });
return requestOptions;
}
export async function validateAndCompileCCEmails(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const ccData = this.getNodeParameter(
'additionalFields.receipientsCC.receipientCc',
) as JsonObject;
const { cc } = ccData;
const { body } = requestOptions;
const data = validateEmailStrings({ cc: cc as string });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileBCCEmails(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const bccData = this.getNodeParameter(
'additionalFields.receipientsBCC.receipientBcc',
) as JsonObject;
const { bcc } = bccData;
const { body } = requestOptions;
const data = validateEmailStrings({ bcc: bcc as string });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileReceipientEmails(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const to = this.getNodeParameter('receipients') as string;
const { body } = requestOptions;
const data = validateEmailStrings({ to });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileSenderEmail(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const sender = this.getNodeParameter('sender') as string;
const { body } = requestOptions;
const data = validateEmailStrings({ sender });
Object.assign(body!, data);
return requestOptions;
}
export async function validateAndCompileTemplateParameters(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const parameterData = this.getNodeParameter(
'additionalFields.templateParameters.parameterValues',
);
const { body } = requestOptions;
const { parmeters } = parameterData as JsonObject;
const params = (parmeters as string)
.split(',')
.filter((parameter) => {
return parameter.split('=').length === 2;
})
.map((parameter) => {
const [key, value] = parameter.split('=');
return {
[key]: value,
};
})
.reduce((obj, cObj) => {
Object.assign(obj, cObj);
return obj;
}, {});
Object.assign(body!, { params });
return requestOptions;
}
function validateEmailStrings(input: ValidEmailFields): ValidatedEmail {
const composer = new MailComposer({ ...input });
const addressFields = composer.compile().getAddresses();
const fieldFetcher = new Map<string, () => Email[] | Email>([
[
'bcc',
() => {
return (addressFields.bcc as unknown as Address[])?.map(formatToEmailName);
},
],
[
'cc',
() => {
return (addressFields.cc as unknown as Address[])?.map(formatToEmailName);
},
],
[
'from',
() => {
return (addressFields.from as unknown as Address[])?.map(formatToEmailName);
},
],
[
'reply-to',
() => {
return (addressFields['reply-to'] as unknown as Address[])?.map(formatToEmailName);
},
],
[
'sender',
() => {
return (addressFields.sender as unknown as Address[])?.map(formatToEmailName)[0];
},
],
[
'to',
() => {
return (addressFields.to as unknown as Address[])?.map(formatToEmailName);
},
],
]);
const result: { [key in keyof ValidatedEmail]: Email[] | Email } = {} as ValidatedEmail;
Object.keys(input).reduce((obj: { [key: string]: Email[] | Email }, key: string) => {
const getter = fieldFetcher.get(key);
const value = getter!();
obj[key] = value;
return obj;
}, result);
return result as ValidatedEmail;
}
}
function getFileName(
itemIndex: number,
mimeType: string,
fileExt: string,
fileName: string,
): string {
let ext = fileExt;
if (fileExt === undefined) {
ext = mimeType.split('/')[1];
}
let name = `${fileName}.${ext}`;
if (fileName === undefined) {
name = `file-${itemIndex}.${ext}`;
}
return name;
}
function formatToEmailName(data: Address): Email {
const { address: email, name } = data;
const result = { email };
if (name !== undefined && name !== '') {
Object.assign(result, { name });
}
return { ...result };
}
}
export namespace SendInBlueWebhookApi {
interface WebhookDetails {
url: string;
id: number;
description: string;
events: string[];
type: string;
createdAt: string;
modifiedAt: string;
}
interface WebhookId {
id: string;
}
interface Webhooks {
webhooks: WebhookDetails[];
}
const credentialsName = 'sendinblueApi';
const baseURL = 'https://api.sendinblue.com/v3';
export const supportedAuthMap = new Map<string, (ref: IWebhookFunctions) => Promise<string>>([
[
'apiKey',
async (ref: IWebhookFunctions): Promise<string> => {
const credentials = await ref.getCredentials(credentialsName);
return credentials.sharedSecret as string;
},
],
]);
export const fetchWebhooks = async (ref: IHookFunctions, type: string): Promise<Webhooks> => {
const endpoint = `${baseURL}/webhooks?type=${type}`;
const options: OptionsWithUri = {
method: 'GET',
headers: {
Accept: 'application/json',
},
uri: endpoint,
};
const webhooks = (await ref.helpers.requestWithAuthentication.call(
ref,
credentialsName,
options,
)) as string;
return JSON.parse(webhooks) as Webhooks;
};
export const createWebHook = async (
ref: IHookFunctions,
type: string,
events: string[],
url: string,
): Promise<WebhookId> => {
const endpoint = `${baseURL}/webhooks`;
const options: OptionsWithUri = {
method: 'POST',
headers: {
Accept: 'application/json',
},
uri: endpoint,
body: {
events,
type,
url,
},
};
const webhookId = await ref.helpers.requestWithAuthentication.call(
ref,
credentialsName,
options,
);
return JSON.parse(webhookId) as WebhookId;
};
export const deleteWebhook = async (ref: IHookFunctions, webhookId: string) => {
const endpoint = `${baseURL}/webhooks/${webhookId}`;
const body = {};
const options: OptionsWithUri = {
method: 'DELETE',
headers: {
Accept: 'application/json',
},
uri: endpoint,
body,
};
return await ref.helpers.requestWithAuthentication.call(ref, credentialsName, options);
};
}

View file

@ -0,0 +1,21 @@
{
"node": "n8n-nodes-base.sendInBlue",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Marketing & Content",
"Communication"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/sendInBlue"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.sendInBlue/"
}
]
}
}

View file

@ -0,0 +1,69 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { attributeFields, attributeOperations } from './AttributeDescription';
import { contactFields, contactOperations } from './ContactDescription';
import { emailFields, emailOperations } from './EmailDescription';
import { senderFields, senderOperations } from './SenderDescrition';
export class SendInBlue implements INodeType {
description: INodeTypeDescription = {
displayName: 'SendInBlue',
name: 'sendInBlue',
icon: 'file:sendinblue.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Sendinblue API',
defaults: {
name: 'SendInBlue',
color: '#044a75',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'sendInBlueApi',
required: true,
},
],
requestDefaults: {
baseURL: 'https://api.sendinblue.com',
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Contact',
value: 'contact',
},
{
name: 'Contact Attribute',
value: 'attribute',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Sender',
value: 'sender',
},
],
default: 'email',
},
...attributeOperations,
...attributeFields,
...senderOperations,
...senderFields,
...contactOperations,
...contactFields,
...emailOperations,
...emailFields,
],
};
}

View file

@ -0,0 +1,22 @@
{
"node": "n8n-nodes-base.sendInBlueTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Marketing & Content",
"Communication"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/sendInBlue"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.sendInBlueTrigger/"
}
],
"generic": []
}
}

View file

@ -0,0 +1,293 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import {
IDataObject,
IHookFunctions,
INodeType,
INodeTypeDescription,
IWebhookFunctions,
IWebhookResponseData,
NodeOperationError,
} from 'n8n-workflow';
import { SendInBlueWebhookApi } from './GenericFunctions';
export class SendInBlueTrigger implements INodeType {
description: INodeTypeDescription = {
credentials: [
{
name: 'sendInBlueApi',
required: true,
},
],
displayName: 'SendInBlue Trigger',
defaults: {
name: 'SendInBlue-Trigger',
color: '#044a75',
},
description: 'Starts the workflow when SendInBlue events occur',
group: ['trigger'],
icon: 'file:sendinblue.svg',
inputs: [],
name: 'sendInBlueTrigger',
outputs: ['main'],
version: 1,
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhooks',
},
],
properties: [
{
default: 'transactional',
displayName: 'Resource',
name: 'type',
options: [
{ name: 'Inbound', value: 'inbound' },
{ name: 'Marketing', value: 'marketing' },
{ name: 'Transactional', value: 'transactional' },
],
required: true,
type: 'options',
},
{
displayName: 'Trigger On',
displayOptions: {
show: {
type: ['transactional'],
},
},
name: 'events',
placeholder: 'Add Event',
options: [
{
name: 'Email Blocked',
value: 'blocked',
description: 'Triggers when transactional email is blocked',
},
{
name: 'Email Clicked',
value: 'click',
description: 'Triggers when transactional email is clicked',
},
{
name: 'Email Deferred',
value: 'deferred',
description: 'Triggers when transactional email is deferred',
},
{
name: 'Email Delivered',
value: 'delivered',
description: 'Triggers when transactional email is delivered',
},
{
name: 'Email Hard Bounce',
value: 'hardBounce',
description: 'Triggers when transactional email is hard bounced',
},
{
name: 'Email Invalid',
value: 'invalid',
description: 'Triggers when transactional email is invalid',
},
{
name: 'Email Marked Spam',
value: 'spam',
description: 'Triggers when transactional email is set to spam',
},
{
name: 'Email Opened',
value: 'opened',
description: 'Triggers when transactional email is opened',
},
{
name: 'Email Sent',
value: 'request',
description: 'Triggers when transactional email is sent',
},
{
name: 'Email Soft-Bounce',
value: 'softBounce',
description: 'Triggers when transactional email is soft bounced',
},
{
name: 'Email Unique Open',
value: 'uniqueOpened',
description: 'Triggers when transactional email is unique opened',
},
{
name: 'Email Unsubscribed',
value: 'unsubscribed',
description: 'Triggers when transactional email is unsubscribed',
},
],
default: [],
required: true,
type: 'multiOptions',
},
{
displayName: 'Trigger On',
displayOptions: {
show: {
type: ['marketing'],
},
},
name: 'events',
placeholder: 'Add Event',
options: [
{
name: 'Marketing Email Clicked',
value: 'click',
description: 'Triggers when marketing email is clicked',
},
{
name: 'Marketing Email Delivered',
value: 'delivered',
description: 'Triggers when marketing email is delivered',
},
{
name: 'Marketing Email Hard Bounce',
value: 'hardBounce',
description: 'Triggers when marketing email is hard bounced',
},
{
name: 'Marketing Email List Addition',
value: 'listAddition',
description: 'Triggers when marketing email is clicked',
},
{
name: 'Marketing Email Opened',
value: 'opened',
description: 'Triggers when marketing email is opened',
},
{
name: 'Marketing Email Soft Bounce',
value: 'softBounce',
description: 'Triggers when marketing email is soft bounced',
},
{
name: 'Marketing Email Spam',
value: 'spam',
description: 'Triggers when marketing email is spam',
},
{
name: 'Marketing Email Unsubscribed',
value: 'unsubscribed',
description: 'Triggers when marketing email is unsubscribed',
},
],
default: [],
required: true,
type: 'multiOptions',
},
{
displayName: 'Trigger On',
displayOptions: {
show: {
type: ['inbound'],
},
},
name: 'events',
placeholder: 'Add Event',
options: [
{
name: 'Inbound Email Processed',
value: 'inboundEmailProcessed',
description: 'Triggers when inbound email is processed',
},
],
default: [],
required: true,
type: 'multiOptions',
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const type = this.getNodeParameter('type') as string;
const events = this.getNodeParameter('events') as string[];
try {
const { webhooks } = await SendInBlueWebhookApi.fetchWebhooks(this, type);
for (const webhook of webhooks) {
if (
webhook.type === type &&
webhook.events.every((event) => events.includes(event)) &&
webhookUrl === webhook.url
) {
webhookData.webhookId = webhook.id;
return true;
}
}
// If it did not error then the webhook exists
return false;
} catch (err) {
return false;
}
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const type = this.getNodeParameter('type') as string;
const events = this.getNodeParameter('events') as string[];
const responseData = await SendInBlueWebhookApi.createWebHook(
this,
type,
events,
webhookUrl,
);
if (responseData === undefined || responseData.id === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.id;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
try {
await SendInBlueWebhookApi.deleteWebhook(this, webhookData.webhookId as string);
} catch (error) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registred anymore
delete webhookData.webhookId;
delete webhookData.webhookEvents;
delete webhookData.hookSecret;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// The data to return and so start the workflow with
const bodyData = this.getBodyData() as IDataObject;
return {
workflowData: [this.helpers.returnJsonArray(bodyData)],
};
}
}

View file

@ -0,0 +1,195 @@
import { IExecuteSingleFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
export const senderOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['sender'],
},
},
options: [
{
name: 'Create',
value: 'create',
action: 'Create a sender',
},
{
name: 'Delete',
value: 'delete',
routing: {
request: {
method: 'DELETE',
url: '=/v3/senders/{{$parameter.id}}',
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}',
},
},
],
},
},
action: 'Delete a sender',
},
{
name: 'Get All',
value: 'getAll',
routing: {
request: {
method: 'GET',
url: '/v3/senders',
},
send: {
paginate: false,
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'senders',
},
},
],
},
},
action: 'Get all senders',
},
],
default: 'create',
},
];
const senderCreateOperation: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['sender'],
operation: ['create'],
},
},
routing: {
request: {
method: 'POST',
url: '/v3/senders',
},
send: {
property: 'name',
type: 'body',
},
},
required: true,
description: 'Name of the sender',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
displayOptions: {
show: {
resource: ['sender'],
operation: ['create'],
},
},
routing: {
send: {
property: 'email',
type: 'body',
},
},
required: true,
description: 'Email of the sender',
},
];
const senderDeleteOperation: INodeProperties[] = [
{
displayName: 'Sender ID',
name: 'id',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['sender'],
operation: ['delete'],
},
},
description: 'ID of the sender to delete',
},
];
const senderGetAllOperation: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['sender'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['sender'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
routing: {
output: {
postReceive: [
{
type: 'limit',
properties: {
maxResults: '={{$value}}',
},
},
],
},
},
default: 10,
description: 'Max number of results to return',
},
];
export const senderFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* sender:create */
/* -------------------------------------------------------------------------- */
...senderCreateOperation,
/* -------------------------------------------------------------------------- */
/* sender:delete */
/* -------------------------------------------------------------------------- */
...senderDeleteOperation,
/* -------------------------------------------------------------------------- */
/* sender:getAll */
/* -------------------------------------------------------------------------- */
...senderGetAllOperation,
];

View file

@ -0,0 +1,20 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 94.8 101.7" style="enable-background:new 0 0 94.8 101.7;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#0092FF;}
</style>
<g>
<path class="st0" d="M85.2,72.8c-2.6,4.5-6.9,7.9-11.9,9.3c0.7-2.5,1.1-5,1.1-7.6c0-12.8-8.8-23.4-20.6-26.4c2.4-2.4,5.5-4.1,8.8-5
c5.1-1.4,10.6-0.7,15.2,2C87.5,50.8,90.8,63.1,85.2,72.8 M47.4,94.7c-5.2,0-10.2-2.1-13.9-5.7c2.5-0.6,4.8-1.6,7-2.8
c11-6.4,15.8-19.4,12.4-31.1c8.4,2.4,14.6,10.2,14.6,19.4C67.5,85.7,58.5,94.7,47.4,94.7 M9.6,72.8c-2.6-4.6-3.3-10-2.1-15
c1.8,1.8,3.8,3.4,6,4.7c4.1,2.4,8.7,3.6,13.5,3.6c7.4,0,14.4-3,19.5-8.4c2.1,8.5-1.5,17.8-9.4,22.4c-3,1.8-6.5,2.7-10,2.7
C19.9,82.9,13.2,79,9.6,72.8 M9.6,28.9c2.6-4.5,6.9-7.9,11.9-9.3c-0.7,2.5-1.1,5-1.1,7.6c0,12.7,8.8,23.4,20.6,26.4
c-6.3,6.1-16.1,7.6-24,3c-4.6-2.7-8-7.1-9.4-12.3C6.2,39.1,6.9,33.5,9.6,28.9 M47.4,7c5.2,0,10.2,2.1,13.9,5.7
c-2.5,0.6-4.8,1.6-7,2.8C48.1,19.1,43.6,25,41.7,32c-1.3,4.8-1.2,9.8,0.2,14.6c-8.4-2.4-14.6-10.2-14.6-19.4C27.3,16,36.3,7,47.4,7
M85.2,28.9c2.6,4.6,3.3,10,2.1,15c-1.8-1.8-3.8-3.4-6-4.7c-6.2-3.6-13.5-4.6-20.5-2.7c-4.8,1.3-9.1,3.9-12.5,7.5
c-0.8-3.3-0.8-6.8,0.1-10.1c1.4-5.2,4.7-9.6,9.4-12.3c4.6-2.7,10.1-3.4,15.2-2C78.2,20.9,82.5,24.2,85.2,28.9 M91.2,25.4
c-3.6-6.3-9.4-10.8-16.4-12.7c-1.6-0.4-3.3-0.7-5-0.9C64.8,4.5,56.4,0,47.4,0c-9.3,0-17.5,4.7-22.3,11.9c-8.9,0.7-17,5.7-21.5,13.5
C0,31.7-0.9,39,0.9,46c0.4,1.7,1,3.3,1.8,4.8c-3.9,8.1-3.6,17.6,0.9,25.4c4.6,8.1,12.8,12.8,21.4,13.5c5,7.4,13.4,11.9,22.4,11.9
c9.3,0,17.5-4.7,22.3-11.9c8.9-0.7,17-5.7,21.5-13.5c4.6-8.1,4.7-17.6,0.9-25.4C96,42.8,95.7,33.2,91.2,25.4">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -255,6 +255,7 @@
"dist/credentials/SecurityScorecardApi.credentials.js",
"dist/credentials/SegmentApi.credentials.js",
"dist/credentials/SendGridApi.credentials.js",
"dist/credentials/SendInBlueApi.credentials.js",
"dist/credentials/SendyApi.credentials.js",
"dist/credentials/SentryIoApi.credentials.js",
"dist/credentials/SentryIoOAuth2Api.credentials.js",
@ -556,6 +557,8 @@
"dist/nodes/Netlify/NetlifyTrigger.node.js",
"dist/nodes/NextCloud/NextCloud.node.js",
"dist/nodes/NocoDB/NocoDB.node.js",
"dist/nodes/SendInBlue/SendInBlue.node.js",
"dist/nodes/SendInBlue/SendInBlueTrigger.node.js",
"dist/nodes/StickyNote/StickyNote.node.js",
"dist/nodes/NoOp/NoOp.node.js",
"dist/nodes/Onfleet/Onfleet.node.js",

View file

@ -46,7 +46,11 @@
"forin": true,
"jsdoc-format": true,
"label-position": true,
"indent": [true, "tabs", 2],
"indent": [
true,
"tabs",
2
],
"member-access": [
true,
"no-public"
@ -61,12 +65,15 @@
"no-default-export": true,
"no-duplicate-variable": true,
"no-inferrable-types": true,
"ordered-imports": [true, {
"ordered-imports": [
true,
{
"import-sources-order": "any",
"named-imports-order": "case-insensitive"
}],
}
],
"no-namespace": [
true,
false,
"allow-declarations"
],
"no-reference": true,

View file

@ -1111,6 +1111,7 @@ export type PostReceiveAction =
response: IN8nHttpFullResponse,
) => Promise<INodeExecutionData[]>)
| IPostReceiveBinaryData
| IPostReceiveLimit
| IPostReceiveRootProperty
| IPostReceiveSet
| IPostReceiveSetKeyValue
@ -1151,6 +1152,13 @@ export interface IPostReceiveBinaryData extends IPostReceiveBase {
};
}
export interface IPostReceiveLimit extends IPostReceiveBase {
type: 'limit';
properties: {
maxResults: number | string;
};
}
export interface IPostReceiveRootProperty extends IPostReceiveBase {
type: 'rootProperty';
properties: {

View file

@ -283,6 +283,17 @@ export class RoutingNode {
);
}
}
if (action.type === 'limit') {
const maxResults = this.getParameterValue(
action.properties.maxResults,
itemIndex,
runIndex,
executeSingleFunctions.getExecuteData(),
{ $response: responseData, $value: parameterValue },
false,
) as string;
return inputData.slice(0, parseInt(maxResults, 10));
}
if (action.type === 'set') {
const { value } = action.properties;
// If the value is an expression resolve it