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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -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": []
}