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,477 @@
import type { INodeProperties } from 'n8n-workflow';
export const emailOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['email'],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send a email',
action: 'Send an email',
},
{
name: 'Send Template',
value: 'sendTemplate',
description: 'Send a email template',
action: 'Send an email template',
},
],
default: 'send',
},
];
export const emailFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* email:send */
/* -------------------------------------------------------------------------- */
{
displayName: 'From Email',
name: 'fromEmail',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
placeholder: 'admin@example.com',
description: 'The title for the email',
},
{
displayName: 'To Email',
name: 'toEmail',
type: 'string',
default: '',
required: true,
placeholder: 'info@example.com',
description: 'Email address of the recipient. Multiple ones can be separated by comma.',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
placeholder: 'My subject line',
description: 'Subject line of the email',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
default: '',
description: 'Plain text message of email',
},
{
displayName: 'HTML',
name: 'html',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
default: '',
description: 'HTML text message of email',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
},
},
options: [
{
displayName: 'Bcc Email',
name: 'bccEmail',
type: 'string',
description: 'Bcc Email address of the recipient. Multiple ones can be separated by comma.',
default: '',
},
{
displayName: 'Cc Addresses',
name: 'ccAddresses',
type: 'string',
description: 'Cc Email address of the recipient. Multiple ones can be separated by comma.',
default: '',
},
{
displayName: 'From Name',
name: 'fromName',
type: 'string',
default: '',
},
{
displayName: 'Priority',
name: 'priority',
type: 'number',
default: 2,
},
{
displayName: 'Reply To',
name: 'replyTo',
type: 'string',
description: 'The reply-to email address. Multiple ones can be separated by comma.',
default: '',
},
{
displayName: 'Template Language',
name: 'templateLanguage',
type: 'boolean',
default: false,
},
{
displayName: 'Track Clicks',
name: 'trackClicks',
type: 'options',
options: [
{
name: 'Account Default',
value: 'account_default',
description: 'Use the values specified in the Mailjet account',
},
{
name: 'Disabled',
value: 'disabled',
description: 'Disable tracking for this message',
},
{
name: 'Enabled',
value: 'enabled',
description: 'Enable tracking for this message',
},
],
description: 'Enable or disable open tracking on this message',
default: 'account_default',
},
{
displayName: 'Track Opens',
name: 'trackOpens',
type: 'options',
options: [
{
name: 'Account Default',
value: 'account_default',
description: 'Use the values specified in the Mailjet account',
},
{
name: 'Disabled',
value: 'disabled',
description: 'Disable tracking for this message',
},
{
name: 'Enabled',
value: 'enabled',
description: 'Enable tracking for this message',
},
],
description: 'Enable or disable open tracking on this message',
default: 'account_default',
},
{
displayName: 'Custom Campaign',
name: 'customCampaign',
type: 'string',
default: '',
},
{
displayName: 'Deduplicate Campaign',
name: 'deduplicateCampaign',
type: 'boolean',
default: false,
},
],
},
{
displayName: 'Variables (JSON)',
name: 'variablesJson',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
jsonParameters: [true],
},
},
default: '',
description: 'HTML text message of email',
},
{
displayName: 'Variables',
name: 'variablesUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['email'],
operation: ['send'],
jsonParameters: [false],
},
},
placeholder: 'Add Variable',
default: {},
options: [
{
name: 'variablesValues',
displayName: 'Variable',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* email:sendTemplate */
/* -------------------------------------------------------------------------- */
{
displayName: 'From Email',
name: 'fromEmail',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
placeholder: 'admin@example.com',
description: 'The title for the email',
},
{
displayName: 'To Email',
name: 'toEmail',
type: 'string',
default: '',
required: true,
placeholder: 'info@example.com',
description: 'Email address of the recipient. Multiple ones can be separated by comma.',
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
},
{
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>',
typeOptions: {
loadOptionsMethod: 'getTemplates',
},
required: true,
default: '',
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
},
},
options: [
{
displayName: 'Bcc Email',
name: 'bccEmail',
type: 'string',
description: 'BCC Recipients of the email separated by ,',
default: '',
},
{
displayName: 'Cc Email',
name: 'ccEmail',
type: 'string',
description: 'Cc recipients of the email separated by ,',
default: '',
},
{
displayName: 'From Name',
name: 'fromName',
type: 'string',
default: '',
},
{
displayName: 'Priority',
name: 'priority',
type: 'number',
default: 2,
},
{
displayName: 'Reply To',
name: 'replyTo',
type: 'string',
description: 'The reply-to email address. Multiple ones can be separated by comma.',
default: '',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
},
{
displayName: 'Template Language',
name: 'templateLanguage',
type: 'boolean',
default: false,
},
{
displayName: 'Track Clicks',
name: 'trackClicks',
type: 'string',
description: 'Enable or disable open tracking on this message',
default: '',
},
{
displayName: 'Track Opens',
name: 'trackOpens',
type: 'string',
description: 'Enable or disable open tracking on this message',
default: '',
},
{
displayName: 'Custom Campaign',
name: 'customCampaign',
type: 'string',
default: '',
},
{
displayName: 'Deduplicate Campaign',
name: 'deduplicateCampaign',
type: 'boolean',
default: false,
},
],
},
{
displayName: 'Variables',
name: 'variablesUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
jsonParameters: [false],
},
},
placeholder: 'Add Variable',
default: {},
options: [
{
name: 'variablesValues',
displayName: 'Variable',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Variables (JSON)',
name: 'variablesJson',
type: 'string',
displayOptions: {
show: {
resource: ['email'],
operation: ['sendTemplate'],
jsonParameters: [true],
},
},
default: '',
description: 'HTML text message of email',
},
];
@@ -0,0 +1,108 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IDataObject,
IHookFunctions,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
export async function mailjetApiRequest(
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
path: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
option: IDataObject = {},
): Promise<any> {
const resource = this.getNodeParameter('resource', 0);
let credentialType;
if (resource === 'email' || this.getNode().type.includes('Trigger')) {
credentialType = 'mailjetEmailApi';
const { sandboxMode } = await this.getCredentials<{
sandboxMode: boolean;
}>('mailjetEmailApi');
if (!this.getNode().type.includes('Trigger')) {
Object.assign(body, { SandboxMode: sandboxMode });
}
} else {
credentialType = 'mailjetSmsApi';
}
let options: IRequestOptions = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method,
qs,
body,
uri: uri || `https://api.mailjet.com${path}`,
json: true,
};
options = Object.assign({}, options, option);
if (Object.keys(options.body as IDataObject).length === 0) {
delete options.body;
}
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
}
export async function mailjetApiRequestAllItems(
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
query.Limit = 1000;
query.Offset = 0;
do {
responseData = await mailjetApiRequest.call(this, method, endpoint, body, query, undefined, {
resolveWithFullResponse: true,
});
returnData.push.apply(returnData, responseData.body as IDataObject[]);
query.Offset = query.Offset + query.Limit;
} while (responseData.length !== 0);
return returnData;
}
export function validateJSON(json: string | undefined): IDataObject | undefined {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
export interface IMessage {
From?: { Email?: string; Name?: string };
Subject?: string;
To?: IDataObject[];
Cc?: IDataObject[];
Bcc?: IDataObject[];
Variables?: IDataObject;
TemplateLanguage?: boolean;
TemplateID?: number;
HTMLPart?: string;
TextPart?: string;
TrackOpens?: string;
ReplyTo?: IDataObject;
TrackClicks?: string;
Priority?: number;
CustomCampaign?: string;
DeduplicateCampaign?: boolean;
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.mailjet",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/mailjet/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.mailjet/"
}
]
}
}
@@ -0,0 +1,338 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { emailFields, emailOperations } from './EmailDescription';
import type { IMessage } from './GenericFunctions';
import { mailjetApiRequest, validateJSON } from './GenericFunctions';
import { smsFields, smsOperations } from './SmsDescription';
export class Mailjet implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mailjet',
name: 'mailjet',
icon: 'file:mailjet.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Mailjet API',
defaults: {
name: 'Mailjet',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'mailjetEmailApi',
required: true,
displayOptions: {
show: {
resource: ['email'],
},
},
},
{
name: 'mailjetSmsApi',
required: true,
displayOptions: {
show: {
resource: ['sms'],
},
},
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'SMS',
value: 'sms',
},
],
default: 'email',
},
...emailOperations,
...emailFields,
...smsOperations,
...smsFields,
],
};
methods = {
loadOptions: {
// Get all the available custom fields to display them to user so that they can
// select them easily
async getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { Data: templates } = await mailjetApiRequest.call(this, 'GET', '/v3/REST/template');
for (const template of templates) {
returnData.push({
name: template.Name,
value: template.ID,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
try {
if (resource === 'email') {
//https://dev.mailjet.com/email/guides/send-api-v31/#send-a-basic-email
if (operation === 'send') {
const fromEmail = this.getNodeParameter('fromEmail', i) as string;
const htmlBody = this.getNodeParameter('html', i) as string;
const textBody = this.getNodeParameter('text', i) as string;
const subject = this.getNodeParameter('subject', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const toEmail = (this.getNodeParameter('toEmail', i) as string).split(',');
const jsonParameters = this.getNodeParameter('jsonParameters', i);
const body: IMessage = {
From: {
Email: fromEmail,
},
Subject: subject,
To: [],
Cc: [],
Bcc: [],
Variables: {},
};
for (const email of toEmail) {
body.To?.push({
Email: email,
});
}
if (jsonParameters) {
const variablesJson = this.getNodeParameter('variablesJson', i) as string;
const parsedJson = validateJSON(variablesJson);
if (parsedJson === undefined) {
throw new NodeOperationError(
this.getNode(),
"Parameter 'Variables (JSON)' has a invalid JSON",
{ itemIndex: i },
);
}
body.Variables = parsedJson;
} else {
const variables =
((this.getNodeParameter('variablesUi', i) as IDataObject)
.variablesValues as IDataObject[]) || [];
for (const variable of variables) {
body.Variables![variable.name as string] = variable.value;
}
}
if (htmlBody) {
body.HTMLPart = htmlBody;
}
if (textBody) {
body.TextPart = textBody;
}
if (additionalFields.bccEmail) {
const bccEmail = (additionalFields.bccEmail as string).split(',');
for (const email of bccEmail) {
body.Bcc!.push({
Email: email,
});
}
}
if (additionalFields.ccAddresses) {
const ccEmail = (additionalFields.ccAddresses as string).split(',');
for (const email of ccEmail) {
body.Cc!.push({
Email: email,
});
}
}
if (additionalFields.trackOpens) {
body.TrackOpens = additionalFields.trackOpens as string;
}
if (additionalFields.replyTo) {
const replyTo = additionalFields.replyTo as string;
body.ReplyTo = {
Email: replyTo,
};
}
if (additionalFields.trackClicks) {
body.TrackClicks = additionalFields.trackClicks as string;
}
if (additionalFields.fromName) {
body.From!.Name = additionalFields.fromName as string;
}
if (additionalFields.templateLanguage) {
body.TemplateLanguage = additionalFields.templateLanguage as boolean;
}
if (additionalFields.priority) {
body.Priority = additionalFields.priority as number;
}
if (additionalFields.customCampaign) {
body.CustomCampaign = additionalFields.customCampaign as string;
}
if (additionalFields.deduplicateCampaign) {
body.DeduplicateCampaign = additionalFields.deduplicateCampaign as boolean;
}
responseData = await mailjetApiRequest.call(this, 'POST', '/v3.1/send', {
Messages: [body],
});
responseData = responseData.Messages;
}
//https://dev.mailjet.com/email/guides/send-api-v31/#use-a-template
if (operation === 'sendTemplate') {
const fromEmail = this.getNodeParameter('fromEmail', i) as string;
const templateId = parseInt(this.getNodeParameter('templateId', i) as string, 10);
const subject = this.getNodeParameter('subject', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const toEmail = (this.getNodeParameter('toEmail', i) as string).split(',');
const jsonParameters = this.getNodeParameter('jsonParameters', i);
const body: IMessage = {
From: {
Email: fromEmail,
},
Subject: subject,
To: [],
Cc: [],
Bcc: [],
Variables: {},
TemplateID: templateId,
};
for (const email of toEmail) {
body.To!.push({
Email: email,
});
}
if (jsonParameters) {
const variablesJson = this.getNodeParameter('variablesJson', i) as string;
const parsedJson = validateJSON(variablesJson);
if (parsedJson === undefined) {
throw new NodeOperationError(
this.getNode(),
"Parameter 'Variables (JSON)' has a invalid JSON",
{ itemIndex: i },
);
}
body.Variables = parsedJson;
} else {
const variables =
((this.getNodeParameter('variablesUi', i) as IDataObject)
.variablesValues as IDataObject[]) || [];
for (const variable of variables) {
body.Variables![variable.name as string] = variable.value;
}
}
if (additionalFields.bccEmail) {
const bccEmail = (additionalFields.bccEmail as string).split(',');
for (const email of bccEmail) {
body.Bcc!.push({
Email: email,
});
}
}
if (additionalFields.ccEmail) {
const ccEmail = (additionalFields.ccEmail as string).split(',');
for (const email of ccEmail) {
body.Cc!.push({
Email: email,
});
}
}
if (additionalFields.replyTo) {
const replyTo = additionalFields.replyTo as string;
body.ReplyTo = {
Email: replyTo,
};
}
if (additionalFields.trackOpens) {
body.TrackOpens = additionalFields.trackOpens as string;
}
if (additionalFields.trackClicks) {
body.TrackClicks = additionalFields.trackClicks as string;
}
if (additionalFields.fromName) {
body.From!.Name = additionalFields.fromName as string;
}
if (additionalFields.templateLanguage) {
body.TemplateLanguage = additionalFields.templateLanguage as boolean;
}
if (additionalFields.priority) {
body.Priority = additionalFields.priority as number;
}
if (additionalFields.customCampaign) {
body.CustomCampaign = additionalFields.customCampaign as string;
}
if (additionalFields.deduplicateCampaign) {
body.DeduplicateCampaign = additionalFields.deduplicateCampaign as boolean;
}
responseData = await mailjetApiRequest.call(this, 'POST', '/v3.1/send', {
Messages: [body],
});
responseData = responseData.Messages;
}
}
if (resource === 'sms') {
//https://dev.mailjet.com/sms/reference/send-message#v4_post_sms-send
if (operation === 'send') {
const from = this.getNodeParameter('from', i) as string;
const to = this.getNodeParameter('to', i) as boolean;
const text = this.getNodeParameter('text', i) as string;
const body: IDataObject = {
From: from,
To: to,
Text: text,
};
responseData = await mailjetApiRequest.call(this, 'POST', '/v4/sms-send', body);
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.mailjetTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/mailjet/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.mailjettrigger/"
}
]
}
}
@@ -0,0 +1,133 @@
import type {
IHookFunctions,
IWebhookFunctions,
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { mailjetApiRequest } from './GenericFunctions';
export class MailjetTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mailjet Trigger',
name: 'mailjetTrigger',
icon: 'file:mailjet.svg',
group: ['trigger'],
version: 1,
description: 'Handle Mailjet events via webhooks',
defaults: {
name: 'Mailjet Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'mailjetEmailApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Event',
name: 'event',
type: 'options',
required: true,
default: 'open',
options: [
{
name: 'email.blocked',
value: 'blocked',
},
{
name: 'email.bounce',
value: 'bounce',
},
{
name: 'email.open',
value: 'open',
},
{
name: 'email.sent',
value: 'sent',
},
{
name: 'email.spam',
value: 'spam',
},
{
name: 'email.unsub',
value: 'unsub',
},
],
description: 'Determines which resource events the webhook is triggered for',
},
],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const endpoint = '/v3/rest/eventcallbackurl';
const responseData = await mailjetApiRequest.call(this, 'GET', endpoint);
const event = this.getNodeParameter('event') as string;
const webhookUrl = this.getNodeWebhookUrl('default');
for (const webhook of responseData.Data) {
if (webhook.EventType === event && webhook.Url === webhookUrl) {
// Set webhook-id to be sure that it can be deleted
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = webhook.ID as string;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const event = this.getNodeParameter('event') as string;
const endpoint = '/v3/rest/eventcallbackurl';
const body: IDataObject = {
Url: webhookUrl,
EventType: event,
Status: 'alive',
isBackup: 'false',
};
const { Data } = await mailjetApiRequest.call(this, 'POST', endpoint, body);
webhookData.webhookId = Data[0].ID;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const endpoint = `/v3/rest/eventcallbackurl/${webhookData.webhookId}`;
try {
await mailjetApiRequest.call(this, 'DELETE', endpoint);
} catch (error) {
return false;
}
delete webhookData.webhookId;
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
return {
workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])],
};
}
}
@@ -0,0 +1,73 @@
import type { INodeProperties } from 'n8n-workflow';
export const smsOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['sms'],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send a sms',
action: 'Send an SMS',
},
],
default: 'send',
},
];
export const smsFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* sms:send */
/* -------------------------------------------------------------------------- */
{
displayName: 'From',
name: 'from',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['sms'],
operation: ['send'],
},
},
description:
'Customizable sender name. Should be between 3 and 11 characters in length, only alphanumeric characters are allowed.',
},
{
displayName: 'To',
name: 'to',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['sms'],
operation: ['send'],
},
},
description:
'Message recipient. Should be between 3 and 15 characters in length. The number always starts with a plus sign followed by a country code, followed by the number. Phone numbers are expected to comply with the E.164 format.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['sms'],
operation: ['send'],
},
},
},
];
@@ -0,0 +1,72 @@
{
"type": "object",
"properties": {
"Bcc": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Email": {
"type": "string"
},
"MessageHref": {
"type": "string"
},
"MessageID": {
"type": "integer"
},
"MessageUUID": {
"type": "string"
}
}
}
},
"Cc": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Email": {
"type": "string"
},
"MessageHref": {
"type": "string"
},
"MessageID": {
"type": "integer"
},
"MessageUUID": {
"type": "string"
}
}
}
},
"CustomID": {
"type": "string"
},
"Status": {
"type": "string"
},
"To": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Email": {
"type": "string"
},
"MessageHref": {
"type": "string"
},
"MessageID": {
"type": "integer"
},
"MessageUUID": {
"type": "string"
}
}
}
}
},
"version": 1
}
@@ -0,0 +1,72 @@
{
"type": "object",
"properties": {
"Bcc": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Email": {
"type": "string"
},
"MessageHref": {
"type": "string"
},
"MessageID": {
"type": "integer"
},
"MessageUUID": {
"type": "string"
}
}
}
},
"Cc": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Email": {
"type": "string"
},
"MessageHref": {
"type": "string"
},
"MessageID": {
"type": "integer"
},
"MessageUUID": {
"type": "string"
}
}
}
},
"CustomID": {
"type": "string"
},
"Status": {
"type": "string"
},
"To": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Email": {
"type": "string"
},
"MessageHref": {
"type": "string"
},
"MessageID": {
"type": "integer"
},
"MessageUUID": {
"type": "string"
}
}
}
}
},
"version": 1
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0.5 90 402.6 222.2"><path fill="#fead0c" d="m129.1 195.5-7 46.5-48 70.2 16.2-8.9L354 158.1l49.1-27zm-1.7-12.3 206.8-47-13.4-1.8-54.3-7.6-115.2-15.9L.5 90l55.3 40.6 69.5 51.1z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

@@ -0,0 +1,602 @@
import { mock, mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { Mailjet } from '../Mailjet.node';
import * as GenericFunctions from '../GenericFunctions';
describe('Mailjet Node', () => {
let mailjetNode: Mailjet;
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
const mailjetApiRequestSpy = jest.spyOn(GenericFunctions, 'mailjetApiRequest');
beforeEach(() => {
mailjetNode = new Mailjet();
mockExecuteFunctions = mockDeep<IExecuteFunctions>({
helpers: {
constructExecutionMetaData: jest.fn().mockImplementation((data) => data),
returnJsonArray: jest
.fn()
.mockImplementation((data) =>
Array.isArray(data) ? data.map((item) => ({ json: item })) : [{ json: data }],
),
},
});
jest.clearAllMocks();
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
mockExecuteFunctions.getNode.mockReturnValue(
mock<INode>({
id: 'test-mailjet-node',
name: 'Mailjet Test',
type: 'n8n-nodes-base.mailjet',
typeVersion: 1,
position: [0, 0],
parameters: {},
}),
);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
});
afterEach(() => {
jest.resetAllMocks();
});
describe('Email Resource - Send Operation', () => {
beforeEach(() => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string, _: number) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
additionalFields: {},
};
return params[paramName];
});
mailjetApiRequestSpy.mockResolvedValue({
Messages: [
{
Status: 'success',
To: [{ Email: 'recipient@example.com', MessageID: 123456789 }],
},
],
});
});
it('should send email with customCampaign field', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'my-custom-campaign',
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
CustomCampaign: 'my-custom-campaign',
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send email with deduplicateCampaign field set to true', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
deduplicateCampaign: true,
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
DeduplicateCampaign: true,
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should not include deduplicateCampaign field when set to false', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
deduplicateCampaign: false,
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.not.objectContaining({
DeduplicateCampaign: expect.anything(),
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send email with both customCampaign and deduplicateCampaign fields', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'my-campaign-2024',
deduplicateCampaign: true,
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
CustomCampaign: 'my-campaign-2024',
DeduplicateCampaign: true,
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send email without customCampaign and deduplicateCampaign when not provided', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.not.objectContaining({
CustomCampaign: expect.anything(),
DeduplicateCampaign: expect.anything(),
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send email with other additional fields alongside customCampaign', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test HTML Body</p>',
text: 'Test Text Body',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
fromName: 'Test Sender',
priority: 3,
customCampaign: 'multi-field-campaign',
trackOpens: 'enabled',
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
From: expect.objectContaining({
Name: 'Test Sender',
}),
Priority: 3,
CustomCampaign: 'multi-field-campaign',
TrackOpens: 'enabled',
}),
]),
}),
);
expect(result).toHaveLength(1);
});
});
describe('Email Resource - Send Template Operation', () => {
beforeEach(() => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'sendTemplate',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
templateId: '12345',
subject: 'Test Template Email',
jsonParameters: false,
additionalFields: {},
};
return params[paramName];
});
mailjetApiRequestSpy.mockResolvedValue({
Messages: [
{
Status: 'success',
To: [{ Email: 'recipient@example.com', MessageID: 987654321 }],
},
],
});
});
it('should send template email with customCampaign field', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'sendTemplate',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
templateId: '12345',
subject: 'Test Template Email',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'template-campaign',
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
TemplateID: 12345,
CustomCampaign: 'template-campaign',
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send template email with deduplicateCampaign field set to true', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'sendTemplate',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
templateId: '12345',
subject: 'Test Template Email',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
deduplicateCampaign: true,
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
TemplateID: 12345,
DeduplicateCampaign: true,
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send template email with customCampaign but not include deduplicateCampaign when false', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'sendTemplate',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
templateId: '12345',
subject: 'Test Template Email',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'template-campaign-v2',
deduplicateCampaign: false,
},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
TemplateID: 12345,
CustomCampaign: 'template-campaign-v2',
}),
]),
}),
);
const calls = mailjetApiRequestSpy.mock.calls;
const lastCall = calls[calls.length - 1];
expect(lastCall[2].Messages[0]).not.toHaveProperty('DeduplicateCampaign');
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
it('should send template email without customCampaign and deduplicateCampaign when not provided', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'sendTemplate',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
templateId: '12345',
subject: 'Test Template Email',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {},
};
return params[paramName];
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.not.objectContaining({
CustomCampaign: expect.anything(),
DeduplicateCampaign: expect.anything(),
}),
]),
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(1);
});
});
describe('Multiple Items Processing', () => {
it('should handle multiple items with different customCampaign values', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }, { json: {} }]);
mockExecuteFunctions.getNodeParameter.mockImplementation(
(paramName: string, index: number) => {
const paramsForIndex: Array<Record<string, any>> = [
{
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient1@example.com',
subject: 'Test Email 1',
html: '<p>Test 1</p>',
text: 'Test 1',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'campaign-1',
deduplicateCampaign: true,
},
},
{
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient2@example.com',
subject: 'Test Email 2',
html: '<p>Test 2</p>',
text: 'Test 2',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'campaign-2',
deduplicateCampaign: false,
},
},
];
return paramsForIndex[index][paramName];
},
);
mailjetApiRequestSpy.mockResolvedValue({
Messages: [{ Status: 'success' }],
});
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(mailjetApiRequestSpy).toHaveBeenCalledTimes(2);
expect(mailjetApiRequestSpy).toHaveBeenNthCalledWith(
1,
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
CustomCampaign: 'campaign-1',
DeduplicateCampaign: true,
}),
]),
}),
);
expect(mailjetApiRequestSpy).toHaveBeenNthCalledWith(
2,
'POST',
'/v3.1/send',
expect.objectContaining({
Messages: expect.arrayContaining([
expect.objectContaining({
CustomCampaign: 'campaign-2',
}),
]),
}),
);
const secondCall = mailjetApiRequestSpy.mock.calls[1];
expect(secondCall[2].Messages[0]).not.toHaveProperty('DeduplicateCampaign');
expect(result[0]).toHaveLength(2);
});
});
describe('Error Handling', () => {
it('should handle API errors gracefully when continueOnFail is true', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test</p>',
text: 'Test',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
customCampaign: 'error-campaign',
},
};
return params[paramName];
});
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
mailjetApiRequestSpy.mockRejectedValue(new Error('API Error'));
const result = await mailjetNode.execute.call(mockExecuteFunctions);
expect(result).toHaveLength(1);
expect(result[0][0].json).toHaveProperty('error', 'API Error');
});
it('should throw error when continueOnFail is false', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: { [key: string]: any } = {
resource: 'email',
operation: 'send',
fromEmail: 'sender@example.com',
toEmail: 'recipient@example.com',
subject: 'Test Email',
html: '<p>Test</p>',
text: 'Test',
jsonParameters: false,
variablesUi: { variablesValues: [] },
additionalFields: {
deduplicateCampaign: true,
},
};
return params[paramName];
});
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
mailjetApiRequestSpy.mockRejectedValue(new Error('API Error'));
await expect(mailjetNode.execute.call(mockExecuteFunctions)).rejects.toThrow('API Error');
});
});
});