first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const contactOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create or Update',
|
||||
value: 'upsert',
|
||||
description:
|
||||
'Create a new contact, or update the current one if it already exists (upsert)',
|
||||
action: 'Create or update a contact',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a contact',
|
||||
action: 'Delete a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a contact by ID',
|
||||
action: 'Get a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many contacts',
|
||||
action: 'Get many contacts',
|
||||
},
|
||||
],
|
||||
default: 'upsert',
|
||||
},
|
||||
];
|
||||
|
||||
export const contactFields: INodeProperties[] = [
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
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],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
default: 100,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The query field accepts valid <a href="https://sendgrid.com/docs/for-developers/sending-email/segmentation-query-language/">SGQL</a> for searching for a contact',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['upsert'],
|
||||
resource: ['contact'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Primary email for the contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['upsert'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'addressUi',
|
||||
placeholder: 'Address',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'addressValues',
|
||||
displayName: 'Address',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Address Line 1',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Address Line 2',
|
||||
name: 'address2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Alternate Emails',
|
||||
name: 'alternateEmails',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postalCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State/Province/Region',
|
||||
name: 'stateProvinceRegion',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'List IDs',
|
||||
name: 'listIdsUi',
|
||||
placeholder: 'List IDs',
|
||||
description: 'Adds a custom field to set also values which have not been predefined',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'listIdValues',
|
||||
displayName: 'List IDs',
|
||||
values: [
|
||||
{
|
||||
displayName: 'List Names or IDs',
|
||||
name: 'listIds',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getListIds',
|
||||
},
|
||||
default: [],
|
||||
description:
|
||||
'ID of the field to set. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Fields',
|
||||
name: 'customFieldsUi',
|
||||
placeholder: 'Add Custom Fields',
|
||||
description: 'Adds custom fields',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'customFieldValues',
|
||||
displayName: 'Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Name or ID',
|
||||
name: 'fieldId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCustomFields',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the field. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'fieldValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value for the field',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Contact IDs',
|
||||
name: 'ids',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['delete'],
|
||||
deleteAll: [false],
|
||||
},
|
||||
},
|
||||
description: 'ID of the contact. Multiple can be added separated by comma.',
|
||||
},
|
||||
{
|
||||
displayName: 'Delete All',
|
||||
name: 'deleteAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether all contacts will be deleted',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'By',
|
||||
name: 'by',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'ID',
|
||||
value: 'id',
|
||||
},
|
||||
{
|
||||
name: 'Email',
|
||||
value: 'email',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['contact'],
|
||||
},
|
||||
},
|
||||
default: 'id',
|
||||
description: 'Search the user by ID or email',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['contact'],
|
||||
by: ['id'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of the contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['contact'],
|
||||
by: ['email'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Email of the contact',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function sendGridApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
endpoint: string,
|
||||
method: IHttpRequestMethods,
|
||||
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
option: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const host = 'api.sendgrid.com/v3';
|
||||
|
||||
const options: IRequestOptions = {
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
uri: `https://${host}${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(body as IDataObject).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
if (Object.keys(option).length !== 0) {
|
||||
Object.assign(options, option);
|
||||
}
|
||||
|
||||
return await this.helpers.requestWithAuthentication.call(this, 'sendGridApi', options);
|
||||
}
|
||||
|
||||
export async function sendGridApiRequestAllItems(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
endpoint: string,
|
||||
method: IHttpRequestMethods,
|
||||
propertyName: string,
|
||||
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
|
||||
let uri;
|
||||
|
||||
do {
|
||||
responseData = await sendGridApiRequest.call(this, endpoint, method, body, query, uri); // possible bug, as function does not have uri parameter
|
||||
uri = responseData._metadata.next;
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
const limit = query.limit as number | undefined;
|
||||
if (limit && returnData.length >= limit) {
|
||||
return returnData;
|
||||
}
|
||||
} while (responseData._metadata.next !== undefined);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const listOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a list',
|
||||
action: 'Create a list',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a list',
|
||||
action: 'Delete a list',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a list',
|
||||
action: 'Get a list',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many lists',
|
||||
action: 'Get many lists',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a list',
|
||||
action: 'Update a list',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
export const listFields: INodeProperties[] = [
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['list'],
|
||||
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: ['list'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
default: 100,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['create'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Name of the list',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'List ID',
|
||||
name: 'listId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['delete'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of the list',
|
||||
},
|
||||
{
|
||||
displayName: 'Delete Contacts',
|
||||
name: 'deleteContacts',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['delete'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
description: 'Whether to delete all contacts on the list',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'List ID',
|
||||
name: 'listId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of the list',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Sample',
|
||||
name: 'contactSample',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
description: 'Whether to return the contact sample',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* list:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'List ID',
|
||||
name: 'listId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of the list',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
resource: ['list'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Name of the list',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,341 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const mailOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Send',
|
||||
value: 'send',
|
||||
description: 'Send an email',
|
||||
action: 'Send an email',
|
||||
},
|
||||
],
|
||||
default: 'send',
|
||||
},
|
||||
];
|
||||
|
||||
export const mailFields: INodeProperties[] = [
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* mail:send */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Sender Email',
|
||||
name: 'fromEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'sender@domain.com',
|
||||
description: 'Email address of the sender of the email',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sender Name',
|
||||
name: 'fromName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'John Smith',
|
||||
description: 'Name of the sender of the email',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Recipient Email',
|
||||
name: 'toEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'recipient@domain.com',
|
||||
description: 'Comma-separated list of recipient email addresses',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subject',
|
||||
name: 'subject',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Subject of the email to send',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
dynamicTemplate: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Dynamic Template',
|
||||
name: 'dynamicTemplate',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
default: false,
|
||||
description: 'Whether this email will contain a dynamic template',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'MIME Type',
|
||||
name: 'contentType',
|
||||
type: 'options',
|
||||
default: 'text/plain',
|
||||
description: 'MIME type of the email to send',
|
||||
options: [
|
||||
{
|
||||
name: 'Plain Text',
|
||||
value: 'text/plain',
|
||||
},
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'text/html',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
dynamicTemplate: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Message Body',
|
||||
name: 'contentValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Message body of the email to send',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
dynamicTemplate: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Template Name or ID',
|
||||
name: 'templateId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
default: [],
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTemplateIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
dynamicTemplate: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Dynamic Template Fields',
|
||||
name: 'dynamicTemplateFields',
|
||||
placeholder: 'Add Dynamic Template Fields',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
dynamicTemplate: [true],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'fields',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Key of the dynamic template field',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value for the field',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['mail'],
|
||||
operation: ['send'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachments',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Comma-separated list of binary properties',
|
||||
},
|
||||
{
|
||||
displayName: 'BCC Email',
|
||||
name: 'bccEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of emails of the recipients of a blind carbon copy of the email',
|
||||
},
|
||||
{
|
||||
displayName: 'Categories',
|
||||
name: 'categories',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of categories. Each category name may not exceed 255 characters.',
|
||||
},
|
||||
{
|
||||
displayName: 'CC Email',
|
||||
name: 'ccEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of emails of the recipients of a carbon copy of the email',
|
||||
},
|
||||
{
|
||||
displayName: 'Enable Sandbox',
|
||||
name: 'enableSandbox',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to use to the sandbox for testing out email-sending functionality',
|
||||
},
|
||||
{
|
||||
displayName: 'IP Pool Name',
|
||||
name: 'ipPoolName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The IP Pool that you would like to send this email from',
|
||||
},
|
||||
{
|
||||
displayName: 'Reply-To Email',
|
||||
name: 'replyToEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'reply@domain.com',
|
||||
description:
|
||||
'Comma-separated list of email addresses that will appear in the reply-to field of the email',
|
||||
},
|
||||
{
|
||||
displayName: 'Headers',
|
||||
name: 'headers',
|
||||
placeholder: 'Add Header',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Key to set in the header object',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value to set in the header object',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Send At',
|
||||
name: 'sendAt',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description:
|
||||
'When to deliver the email. Scheduling more than 72 hours in advance is forbidden.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export type SendMailBody = {
|
||||
personalizations: Array<{
|
||||
to: EmailName[];
|
||||
subject?: string;
|
||||
cc?: EmailName[];
|
||||
bcc?: EmailName[];
|
||||
dynamic_template_data?: { [key: string]: string };
|
||||
send_at?: number;
|
||||
}>;
|
||||
ip_pool_name?: string;
|
||||
from: EmailName;
|
||||
reply_to_list?: EmailName[];
|
||||
template_id?: string;
|
||||
content?: Array<{
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
categories?: string[];
|
||||
headers?: { [key: string]: string };
|
||||
attachments?: Array<{
|
||||
content: string;
|
||||
filename: string;
|
||||
type: string;
|
||||
}>;
|
||||
mail_settings: {
|
||||
sandbox_mode: {
|
||||
enable: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type EmailName = {
|
||||
email: string;
|
||||
name?: string;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.sendGrid",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Marketing", "Communication"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/sendgrid/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.sendgrid/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,666 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type IHttpRequestMethods,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodePropertyOptions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { contactFields, contactOperations } from './ContactDescription';
|
||||
import { sendGridApiRequest, sendGridApiRequestAllItems } from './GenericFunctions';
|
||||
import { listFields, listOperations } from './ListDescription';
|
||||
import type { SendMailBody } from './MailDescription';
|
||||
import { mailFields, mailOperations } from './MailDescription';
|
||||
|
||||
export class SendGrid implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'SendGrid',
|
||||
name: 'sendGrid',
|
||||
icon: 'file:sendGrid.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
|
||||
description: 'Consume SendGrid API',
|
||||
defaults: {
|
||||
name: 'SendGrid',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'sendGridApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
// Node properties which the user gets displayed and
|
||||
// can change on the node.
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Contact',
|
||||
value: 'contact',
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
value: 'list',
|
||||
},
|
||||
{
|
||||
name: 'Mail',
|
||||
value: 'mail',
|
||||
},
|
||||
],
|
||||
default: 'list',
|
||||
required: true,
|
||||
},
|
||||
...listOperations,
|
||||
...listFields,
|
||||
...contactOperations,
|
||||
...contactFields,
|
||||
...mailOperations,
|
||||
...mailFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get custom fields to display to user so that they can select them easily
|
||||
async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { custom_fields } = await sendGridApiRequest.call(
|
||||
this,
|
||||
'/marketing/field_definitions',
|
||||
'GET',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
if (custom_fields !== undefined) {
|
||||
for (const customField of custom_fields) {
|
||||
returnData.push({
|
||||
name: customField.name,
|
||||
value: customField.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
// Get lists to display to user so that they can select them easily
|
||||
async getListIds(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const lists = await sendGridApiRequestAllItems.call(
|
||||
this,
|
||||
'/marketing/lists',
|
||||
'GET',
|
||||
'result',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
for (const list of lists) {
|
||||
returnData.push({
|
||||
name: list.name,
|
||||
value: list.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getTemplateIds(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
'/templates',
|
||||
'GET',
|
||||
{},
|
||||
{ generations: 'dynamic' },
|
||||
);
|
||||
return responseData.templates.map(({ id, name }: { id: string; name: string }) => ({
|
||||
name,
|
||||
value: id,
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const length = items.length;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const timezone = this.getTimezone();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
// https://sendgrid.com/docs/api-reference/
|
||||
if (resource === 'contact') {
|
||||
if (operation === 'getAll') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
let endpoint = '/marketing/contacts';
|
||||
let method: IHttpRequestMethods = 'GET';
|
||||
const body: IDataObject = {};
|
||||
if (filters.query && filters.query !== '') {
|
||||
endpoint = '/marketing/contacts/search';
|
||||
method = 'POST';
|
||||
Object.assign(body, { query: filters.query });
|
||||
}
|
||||
responseData = await sendGridApiRequestAllItems.call(
|
||||
this,
|
||||
endpoint,
|
||||
method,
|
||||
'result',
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
if (!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i);
|
||||
responseData = responseData.splice(0, limit);
|
||||
}
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'get') {
|
||||
const by = this.getNodeParameter('by', 0) as string;
|
||||
let endpoint;
|
||||
let method: IHttpRequestMethods;
|
||||
const body: IDataObject = {};
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (by === 'id') {
|
||||
method = 'GET';
|
||||
const contactId = this.getNodeParameter('contactId', i) as string;
|
||||
endpoint = `/marketing/contacts/${contactId}`;
|
||||
} else {
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
endpoint = '/marketing/contacts/search';
|
||||
method = 'POST';
|
||||
Object.assign(body, { query: `email LIKE '${email}' ` });
|
||||
}
|
||||
responseData = await sendGridApiRequest.call(this, endpoint, method, body, qs);
|
||||
responseData = responseData.result || responseData;
|
||||
if (Array.isArray(responseData)) {
|
||||
responseData = responseData[0];
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'upsert') {
|
||||
try {
|
||||
const contacts = [];
|
||||
let lists;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const contact: IDataObject = {
|
||||
email,
|
||||
};
|
||||
if (additionalFields.addressUi) {
|
||||
const addressValues = (additionalFields.addressUi as IDataObject)
|
||||
.addressValues as IDataObject;
|
||||
const addressLine1 = addressValues.address1 as string;
|
||||
const addressLine2 = addressValues.address2 as string;
|
||||
if (addressLine2) {
|
||||
Object.assign(contact, { address_line_2: addressLine2 });
|
||||
}
|
||||
Object.assign(contact, { address_line_1: addressLine1 });
|
||||
}
|
||||
if (additionalFields.city) {
|
||||
const city = additionalFields.city as string;
|
||||
Object.assign(contact, { city });
|
||||
}
|
||||
if (additionalFields.country) {
|
||||
const country = additionalFields.country as string;
|
||||
Object.assign(contact, { country });
|
||||
}
|
||||
if (additionalFields.firstName) {
|
||||
const firstName = additionalFields.firstName as string;
|
||||
Object.assign(contact, { first_name: firstName });
|
||||
}
|
||||
if (additionalFields.lastName) {
|
||||
const lastName = additionalFields.lastName as string;
|
||||
Object.assign(contact, { last_name: lastName });
|
||||
}
|
||||
if (additionalFields.postalCode) {
|
||||
const postalCode = additionalFields.postalCode as string;
|
||||
Object.assign(contact, { postal_code: postalCode });
|
||||
}
|
||||
if (additionalFields.stateProvinceRegion) {
|
||||
const stateProvinceRegion = additionalFields.stateProvinceRegion as string;
|
||||
Object.assign(contact, { state_province_region: stateProvinceRegion });
|
||||
}
|
||||
if (additionalFields.alternateEmails) {
|
||||
const alternateEmails = (additionalFields.alternateEmails as string)
|
||||
.split(',')
|
||||
.filter((mail) => !!mail);
|
||||
if (alternateEmails.length !== 0) {
|
||||
Object.assign(contact, { alternate_emails: alternateEmails });
|
||||
}
|
||||
}
|
||||
if (additionalFields.listIdsUi) {
|
||||
const listIdValues = (additionalFields.listIdsUi as IDataObject)
|
||||
.listIdValues as IDataObject;
|
||||
const listIds = listIdValues.listIds as IDataObject[];
|
||||
lists = listIds;
|
||||
}
|
||||
if (additionalFields.customFieldsUi) {
|
||||
const customFields = (additionalFields.customFieldsUi as IDataObject)
|
||||
.customFieldValues as IDataObject[];
|
||||
if (customFields) {
|
||||
const data = customFields.reduce(
|
||||
(obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }),
|
||||
{},
|
||||
);
|
||||
Object.assign(contact, { custom_fields: data });
|
||||
}
|
||||
}
|
||||
contacts.push(contact);
|
||||
}
|
||||
responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
'/marketing/contacts',
|
||||
'PUT',
|
||||
{ list_ids: lists, contacts },
|
||||
qs,
|
||||
);
|
||||
returnData.push(responseData as INodeExecutionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message } });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const deleteAll = this.getNodeParameter('deleteAll', i) as boolean;
|
||||
if (deleteAll) {
|
||||
qs.delete_all_contacts = 'true';
|
||||
}
|
||||
qs.ids = (this.getNodeParameter('ids', i) as string).replace(/\s/g, '');
|
||||
responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
'/marketing/contacts',
|
||||
'DELETE',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource === 'list') {
|
||||
if (operation === 'getAll') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
responseData = await sendGridApiRequestAllItems.call(
|
||||
this,
|
||||
'/marketing/lists',
|
||||
'GET',
|
||||
'result',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
if (!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i);
|
||||
responseData = responseData.splice(0, limit);
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'get') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const listId = this.getNodeParameter('listId', i) as string;
|
||||
qs.contact_sample = this.getNodeParameter('contactSample', i) as boolean;
|
||||
responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
`/marketing/lists/${listId}`,
|
||||
'GET',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'create') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
'/marketing/lists',
|
||||
'POST',
|
||||
{ name },
|
||||
qs,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const listId = this.getNodeParameter('listId', i) as string;
|
||||
qs.delete_contacts = this.getNodeParameter('deleteContacts', i) as boolean;
|
||||
responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
`/marketing/lists/${listId}`,
|
||||
'DELETE',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = { success: true };
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operation === 'update') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
const listId = this.getNodeParameter('listId', i) as string;
|
||||
responseData = await sendGridApiRequest.call(
|
||||
this,
|
||||
`/marketing/lists/${listId}`,
|
||||
'PATCH',
|
||||
{ name },
|
||||
qs,
|
||||
);
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource === 'mail') {
|
||||
if (operation === 'send') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
const toEmail = this.getNodeParameter('toEmail', i) as string;
|
||||
|
||||
const parsedToEmail = toEmail.includes(',')
|
||||
? toEmail.split(',').map((entry) => ({ email: entry.trim() }))
|
||||
: [{ email: toEmail.trim() }];
|
||||
|
||||
const {
|
||||
bccEmail,
|
||||
ccEmail,
|
||||
enableSandbox,
|
||||
sendAt,
|
||||
headers,
|
||||
attachments,
|
||||
categories,
|
||||
ipPoolName,
|
||||
replyToEmail,
|
||||
} = this.getNodeParameter('additionalFields', i) as {
|
||||
bccEmail: string;
|
||||
ccEmail: string;
|
||||
enableSandbox: boolean;
|
||||
sendAt: string;
|
||||
headers: { details: Array<{ key: string; value: string }> };
|
||||
attachments: string;
|
||||
categories: string;
|
||||
ipPoolName: string;
|
||||
replyToEmail: string;
|
||||
};
|
||||
|
||||
const body: SendMailBody = {
|
||||
personalizations: [
|
||||
{
|
||||
to: parsedToEmail,
|
||||
},
|
||||
],
|
||||
from: {
|
||||
email: (this.getNodeParameter('fromEmail', i) as string).trim(),
|
||||
name: this.getNodeParameter('fromName', i) as string,
|
||||
},
|
||||
mail_settings: {
|
||||
sandbox_mode: {
|
||||
enable: enableSandbox || false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dynamicTemplateEnabled = this.getNodeParameter('dynamicTemplate', i);
|
||||
|
||||
// dynamic template
|
||||
if (dynamicTemplateEnabled) {
|
||||
body.template_id = this.getNodeParameter('templateId', i) as string;
|
||||
|
||||
const { fields } = this.getNodeParameter('dynamicTemplateFields', i) as {
|
||||
fields: Array<{ [key: string]: string }>;
|
||||
};
|
||||
|
||||
if (fields) {
|
||||
body.personalizations[0].dynamic_template_data = {};
|
||||
fields.forEach((field) => {
|
||||
body.personalizations[0].dynamic_template_data![field.key] = field.value;
|
||||
});
|
||||
}
|
||||
|
||||
// message body
|
||||
} else {
|
||||
body.personalizations[0].subject = this.getNodeParameter('subject', i) as string;
|
||||
body.content = [
|
||||
{
|
||||
type: this.getNodeParameter('contentType', i) as string,
|
||||
value: this.getNodeParameter('contentValue', i) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (attachments) {
|
||||
const attachmentsToSend = [];
|
||||
const binaryProperties = attachments.split(',').map((p) => p.trim());
|
||||
|
||||
for (const property of binaryProperties) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, property);
|
||||
const dataBuffer = await this.helpers.getBinaryDataBuffer(i, property);
|
||||
|
||||
attachmentsToSend.push({
|
||||
content: dataBuffer.toString('base64'),
|
||||
filename: binaryData.fileName || 'unknown',
|
||||
type: binaryData.mimeType,
|
||||
});
|
||||
}
|
||||
|
||||
if (attachmentsToSend.length) {
|
||||
body.attachments = attachmentsToSend;
|
||||
}
|
||||
}
|
||||
|
||||
if (bccEmail) {
|
||||
body.personalizations[0].bcc = bccEmail
|
||||
.split(',')
|
||||
.map((entry) => ({ email: entry.trim() }));
|
||||
}
|
||||
|
||||
if (ccEmail) {
|
||||
body.personalizations[0].cc = ccEmail
|
||||
.split(',')
|
||||
.map((entry) => ({ email: entry.trim() }));
|
||||
}
|
||||
|
||||
if (headers?.details.length) {
|
||||
const parsedHeaders: { [key: string]: string } = {};
|
||||
headers.details.forEach((obj) => (parsedHeaders[obj.key] = obj.value));
|
||||
body.headers = parsedHeaders;
|
||||
}
|
||||
|
||||
if (categories) {
|
||||
body.categories = categories.split(',');
|
||||
}
|
||||
|
||||
if (ipPoolName) {
|
||||
body.ip_pool_name = ipPoolName;
|
||||
}
|
||||
|
||||
if (sendAt) {
|
||||
body.personalizations[0].send_at = moment.tz(sendAt, timezone).unix();
|
||||
}
|
||||
|
||||
if (replyToEmail) {
|
||||
body.reply_to_list = replyToEmail
|
||||
.split(',')
|
||||
.map((entry) => ({ email: entry.trim() }));
|
||||
}
|
||||
|
||||
const data = await sendGridApiRequest.call(this, '/mail/send', 'POST', body, qs, {
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ messageId: data.headers['x-message-id'] }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"self": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contact_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messageId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 66 65"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><g fill-rule="nonzero" stroke="none"><path d="M0 21.25h21.374v21.374H0z"/><path fill="#99e1f4" d="M0 21.25h21.374v21.374H0z"/><path d="M21.374 42.626h21.25v21.25h-21.25z"/><path fill="#99e1f4" d="M21.374 42.626h21.25v21.25h-21.25z"/><path fill="#1a82e2" d="M0 63.877h21.374V64H0zm0-21.25h21.374v21.25H0z"/><path fill="#00b3e3" d="M21.374 0h21.25v21.25h-21.25zm21.252 21.374H64v21.25H42.626z"/><path fill="#009dd9" d="M21.374 42.626h21.25V21.25h-21.25z"/><path fill="#1a82e2" d="M42.626 0H64v21.25H42.626zm0 21.25H64v.123H42.626z"/></g></symbol></svg>
|
||||
|
After Width: | Height: | Size: 816 B |
@@ -0,0 +1,20 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Test SendGrid Node', () => {
|
||||
describe('Mail', () => {
|
||||
const sendgridNock = nock('https://api.sendgrid.com/v3')
|
||||
.post(
|
||||
'/mail/send',
|
||||
(body: { reply_to_list?: [{ email: string }] }) =>
|
||||
body?.reply_to_list?.[0]?.email === 'test-reply-to@n8n.io',
|
||||
)
|
||||
.reply(202);
|
||||
|
||||
afterAll(() => sendgridNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['mail.workflow.json'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "mail",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "0b4c0843-6007-4687-8e91-cf3b97cb0734",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "mail",
|
||||
"fromEmail": "test@n8n.io",
|
||||
"fromName": "Test Sender",
|
||||
"toEmail": "test@n8n.io",
|
||||
"subject": "Test Subject",
|
||||
"contentValue": "Test Message",
|
||||
"additionalFields": {
|
||||
"replyToEmail": "test-reply-to@n8n.io"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.sendGrid",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "69ea4501-a90d-4067-b9f7-a7b7f2b79a96",
|
||||
"name": "SendGrid",
|
||||
"credentials": {
|
||||
"sendGridApi": {
|
||||
"id": "3D7B6nvmzW0SZkjG",
|
||||
"name": "SendGrid account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "SendGrid",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "7e4ed87a-64b8-4248-b78f-e2db21f1f603",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "35e7acdc2e5caa3b0a5fca77da6cc9361ca88ba3e6f6b97ecd8a8cf35cb2ea85"
|
||||
},
|
||||
"id": "YdfKbNlA37FExwxC",
|
||||
"tags": []
|
||||
}
|
||||
Reference in New Issue
Block a user