Add Autosend node with Mail and Contact resources

Implement n8n node for Autosend API with support for:
- Mail resource: Send single and bulk emails with template or custom content
- Contact resource: Create/update contacts (upsert) and get contacts by ID or email
- API key authentication
- Declarative routing following n8n best practices
- Full TypeScript support with proper typing
- Passing all linting checks
This commit is contained in:
Claude 2025-11-11 17:56:01 +00:00
commit 2f1ffde4a5
No known key found for this signature in database
15 changed files with 1142 additions and 7 deletions

View file

@ -0,0 +1,53 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class AutosendApi implements ICredentialType {
name = 'autosendApi';
displayName = 'Autosend API';
documentationUrl = 'https://docs.autosend.com/';
icon = 'file:../icons/autosend.svg' as const;
httpRequestNode = {
name: 'Autosend',
docsUrl: 'https://docs.autosend.com/',
apiBaseUrl: 'https://api.autosend.com/',
};
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: {
password: true,
},
default: '',
required: true,
description: 'API key from your Autosend account settings',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.apiKey}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.autosend.com',
url: '/contacts',
method: 'GET',
},
};
}

6
icons/autosend.dark.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
<path d="m2 13 4 3"/>
<path d="m22 13-4 3"/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

6
icons/autosend.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
<path d="m2 13 4 3"/>
<path d="m22 13-4 3"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View file

@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.autosend",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.autosend.com/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.autosend.com/"
}
]
}
}

View file

@ -0,0 +1,71 @@
import type {
INodeType,
INodeTypeDescription,
INodeExecutionData,
IExecuteFunctions,
} from 'n8n-workflow';
import { mailFields, mailOperations } from './resources/mail';
import { contactFields, contactOperations } from './resources/contact';
export class Autosend implements INodeType {
description: INodeTypeDescription = {
displayName: 'Autosend',
name: 'autosend',
icon: 'file:autosend.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Autosend API to send emails and manage contacts',
defaults: {
name: 'Autosend',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'autosendApi',
required: true,
},
],
requestDefaults: {
baseURL: 'https://api.autosend.com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Mail',
value: 'mail',
description: 'Send emails using Autosend',
},
{
name: 'Contact',
value: 'contact',
description: 'Manage contacts in Autosend',
},
],
default: 'mail',
},
...mailOperations,
...mailFields,
...contactOperations,
...contactFields,
],
usableAsTool: true,
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// This method is required but the declarative routing handles everything
// This is only called if routing doesn't handle the request
const items = this.getInputData();
return [items];
}
}

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
<path d="m2 13 4 3"/>
<path d="m22 13-4 3"/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
<path d="m2 13 4 3"/>
<path d="m22 13-4 3"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View file

@ -0,0 +1,80 @@
import type { INodeProperties } from 'n8n-workflow';
export const getOperation: INodeProperties[] = [
{
displayName: 'Search By',
name: 'searchBy',
type: 'options',
required: true,
default: 'email',
options: [
{
name: 'Email',
value: 'email',
description: 'Search contact by email address',
},
{
name: 'ID',
value: 'id',
description: 'Search contact by ID',
},
],
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
},
},
description: 'How to search for the contact',
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
searchBy: ['id'],
},
},
routing: {
request: {
url: '=/contacts/{{$parameter.contactId}}',
},
},
description: 'The ID of the contact to retrieve',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
placeholder: 'contact@example.com',
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['get'],
searchBy: ['email'],
},
},
routing: {
send: {
type: 'body',
property: 'emails',
preSend: [
async function (this, requestOptions) {
const email = this.getNodeParameter('email') as string;
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).emails = [email];
return requestOptions;
},
],
},
},
description: 'The email address of the contact to search for',
},
];

View file

@ -0,0 +1,64 @@
import type { INodeProperties } from 'n8n-workflow';
import { upsertOperation } from './upsert';
import { getOperation } from './get';
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 record, or update the current one if it already exists (upsert)',
action: 'Create or update a contact',
routing: {
request: {
method: 'POST' as const,
url: '/contacts/email',
},
},
},
{
name: 'Get',
value: 'get',
description: 'Get a contact by ID or email',
action: 'Get a contact',
routing: {
request: {
method: 'GET' as const,
url: '/contacts/{{$parameter.contactId}}',
},
},
},
],
default: 'upsert',
},
];
export const contactFields: INodeProperties[] = [
...upsertOperation,
...getOperation.map((field) => {
// Override routing for email search to use the search endpoint
if (field.name === 'email' && field.routing) {
return {
...field,
routing: {
...field.routing,
request: {
method: 'POST' as const,
url: '/contacts/search/emails',
},
},
} as INodeProperties;
}
return field;
}),
];

View file

@ -0,0 +1,142 @@
import type { INodeProperties } from 'n8n-workflow';
export const upsertOperation: INodeProperties[] = [
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
placeholder: 'contact@example.com',
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
routing: {
send: {
type: 'body',
property: 'email',
},
},
description: 'The email address of the contact',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
routing: {
send: {
type: 'body',
property: 'userId',
},
},
description: 'External user ID from your system',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['contact'],
operation: ['upsert'],
},
},
options: [
{
displayName: 'Company',
name: 'company',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'company',
},
},
description: 'Company name',
},
{
displayName: 'Custom Fields (JSON)',
name: 'customFields',
type: 'json',
default: '{}',
routing: {
send: {
type: 'body',
property: 'customFields',
preSend: [
async function (this, requestOptions) {
const customFieldsStr = this.getNodeParameter(
'additionalFields.customFields',
'',
) as string;
if (customFieldsStr) {
try {
const customFields = JSON.parse(customFieldsStr);
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).customFields = customFields;
} catch {
throw new Error('Custom Fields must be valid JSON');
}
}
return requestOptions;
},
],
},
},
description: 'Additional custom fields as a JSON object',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'firstName',
},
},
description: 'First name of the contact',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'lastName',
},
},
description: 'Last name of the contact',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
routing: {
send: {
type: 'body',
property: 'phone',
},
},
description: 'Phone number of the contact',
},
],
},
];

View file

@ -0,0 +1,46 @@
import type { INodeProperties } from 'n8n-workflow';
import { sendOperation } from './send';
import { sendBulkOperation } from './sendBulk';
export const mailOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['mail'],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send a single email',
action: 'Send an email',
routing: {
request: {
method: 'POST',
url: '/mails/send',
},
},
},
{
name: 'Send Bulk',
value: 'sendBulk',
description: 'Send emails to multiple recipients',
action: 'Send bulk emails',
routing: {
request: {
method: 'POST',
url: '/mails/bulk',
},
},
},
],
default: 'send',
},
];
export const mailFields: INodeProperties[] = [...sendOperation, ...sendBulkOperation];

View file

@ -0,0 +1,322 @@
import type { INodeProperties } from 'n8n-workflow';
export const sendOperation: INodeProperties[] = [
{
displayName: 'From Email',
name: 'fromEmail',
type: 'string',
required: true,
placeholder: 'noreply@example.com',
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
},
},
routing: {
send: {
type: 'body',
property: 'from.email',
},
},
description: 'The email address to send from',
},
{
displayName: 'From Name',
name: 'fromName',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
},
},
routing: {
send: {
type: 'body',
property: 'from.name',
},
},
description: 'The name to display as the sender',
},
{
displayName: 'To Email',
name: 'toEmail',
type: 'string',
required: true,
placeholder: 'recipient@example.com',
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
},
},
routing: {
send: {
type: 'body',
property: 'to.email',
},
},
description: 'The email address to send to',
},
{
displayName: 'To Name',
name: 'toName',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
},
},
routing: {
send: {
type: 'body',
property: 'to.name',
},
},
description: 'The name of the recipient',
},
{
displayName: 'Email Content Type',
name: 'contentType',
type: 'options',
required: true,
default: 'template',
options: [
{
name: 'Template',
value: 'template',
description: 'Use a pre-defined email template',
},
{
name: 'Custom',
value: 'custom',
description: 'Provide custom HTML and text content',
},
],
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
},
},
description: 'Whether to use a template or custom content',
},
// Template-based fields
{
displayName: 'Template ID',
name: 'templateId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
contentType: ['template'],
},
},
routing: {
send: {
type: 'body',
property: 'templateId',
},
},
description: 'The ID of the email template to use',
},
{
displayName: 'Template Variables',
name: 'templateVariables',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
contentType: ['template'],
},
},
options: [
{
name: 'variables',
displayName: 'Variable',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Variable name',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Variable value',
},
],
},
],
routing: {
send: {
type: 'body',
property: 'variables',
preSend: [
// Transform the fixed collection into a simple object
async function (this, requestOptions) {
const variables = this.getNodeParameter('templateVariables') as {
variables?: Array<{ key: string; value: string }>;
};
if (variables?.variables && Array.isArray(variables.variables)) {
const variablesObj: Record<string, string> = {};
for (const variable of variables.variables) {
if (variable.key) {
variablesObj[variable.key] = variable.value;
}
}
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).variables = variablesObj;
}
return requestOptions;
},
],
},
},
description: 'Variables to use in the email template',
},
// Custom content fields
{
displayName: 'Subject',
name: 'subject',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
contentType: ['custom'],
},
},
routing: {
send: {
type: 'body',
property: 'subject',
},
},
description: 'The email subject line',
},
{
displayName: 'HTML Content',
name: 'html',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
contentType: ['custom'],
},
},
routing: {
send: {
type: 'body',
property: 'html',
},
},
description: 'The HTML content of the email',
},
{
displayName: 'Text Content',
name: 'text',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
contentType: ['custom'],
},
},
routing: {
send: {
type: 'body',
property: 'text',
},
},
description: 'The plain text content of the email (fallback for non-HTML clients)',
},
// Additional options
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['mail'],
operation: ['send'],
},
},
options: [
{
displayName: 'Reply To',
name: 'replyTo',
type: 'string',
default: '',
placeholder: 'reply@example.com',
routing: {
send: {
type: 'body',
property: 'replyTo',
},
},
description: 'Email address for replies',
},
{
displayName: 'CC',
name: 'cc',
type: 'string',
default: '',
placeholder: 'cc@example.com',
routing: {
send: {
type: 'body',
property: 'cc',
},
},
description: 'Carbon copy recipients (comma-separated)',
},
{
displayName: 'BCC',
name: 'bcc',
type: 'string',
default: '',
placeholder: 'bcc@example.com',
routing: {
send: {
type: 'body',
property: 'bcc',
},
},
description: 'Blind carbon copy recipients (comma-separated)',
},
],
},
];

View file

@ -0,0 +1,313 @@
import type { INodeProperties } from 'n8n-workflow';
export const sendBulkOperation: INodeProperties[] = [
{
displayName: 'From Email',
name: 'fromEmail',
type: 'string',
required: true,
placeholder: 'noreply@example.com',
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
},
},
routing: {
send: {
type: 'body',
property: 'from.email',
},
},
description: 'The email address to send from',
},
{
displayName: 'From Name',
name: 'fromName',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
},
},
routing: {
send: {
type: 'body',
property: 'from.name',
},
},
description: 'The name to display as the sender',
},
{
displayName: 'Recipients',
name: 'recipients',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Recipient',
},
required: true,
default: {},
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
},
},
options: [
{
name: 'recipientValues',
displayName: 'Recipient',
values: [
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
default: '',
placeholder: 'recipient@example.com',
description: 'Recipient email address',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Recipient name',
},
],
},
],
routing: {
send: {
type: 'body',
property: 'to',
preSend: [
async function (this, requestOptions) {
const recipients = this.getNodeParameter('recipients') as {
recipientValues?: Array<{ email: string; name?: string }>;
};
if (recipients?.recipientValues && Array.isArray(recipients.recipientValues)) {
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).to = recipients.recipientValues;
}
return requestOptions;
},
],
},
},
description: 'List of recipients (up to 100 per request)',
},
{
displayName: 'Email Content Type',
name: 'contentType',
type: 'options',
required: true,
default: 'template',
options: [
{
name: 'Template',
value: 'template',
description: 'Use a pre-defined email template',
},
{
name: 'Custom',
value: 'custom',
description: 'Provide custom HTML and text content',
},
],
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
},
},
description: 'Whether to use a template or custom content',
},
// Template-based fields
{
displayName: 'Template ID',
name: 'templateId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
contentType: ['template'],
},
},
routing: {
send: {
type: 'body',
property: 'templateId',
},
},
description: 'The ID of the email template to use',
},
{
displayName: 'Template Variables',
name: 'templateVariables',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
contentType: ['template'],
},
},
options: [
{
name: 'variables',
displayName: 'Variable',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Variable name',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Variable value',
},
],
},
],
routing: {
send: {
type: 'body',
property: 'variables',
preSend: [
async function (this, requestOptions) {
const variables = this.getNodeParameter('templateVariables') as {
variables?: Array<{ key: string; value: string }>;
};
if (variables?.variables && Array.isArray(variables.variables)) {
const variablesObj: Record<string, string> = {};
for (const variable of variables.variables) {
if (variable.key) {
variablesObj[variable.key] = variable.value;
}
}
requestOptions.body = requestOptions.body || {};
(requestOptions.body as Record<string, unknown>).variables = variablesObj;
}
return requestOptions;
},
],
},
},
description: 'Variables to use in the email template',
},
// Custom content fields
{
displayName: 'Subject',
name: 'subject',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
contentType: ['custom'],
},
},
routing: {
send: {
type: 'body',
property: 'subject',
},
},
description: 'The email subject line',
},
{
displayName: 'HTML Content',
name: 'html',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
contentType: ['custom'],
},
},
routing: {
send: {
type: 'body',
property: 'html',
},
},
description: 'The HTML content of the email',
},
{
displayName: 'Text Content',
name: 'text',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
contentType: ['custom'],
},
},
routing: {
send: {
type: 'body',
property: 'text',
},
},
description: 'The plain text content of the email (fallback for non-HTML clients)',
},
// Additional options
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['mail'],
operation: ['sendBulk'],
},
},
options: [
{
displayName: 'Reply To',
name: 'replyTo',
type: 'string',
default: '',
placeholder: 'reply@example.com',
routing: {
send: {
type: 'body',
property: 'replyTo',
},
},
description: 'Email address for replies',
},
],
},
];

4
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "n8n-nodes-<...>",
"name": "n8n-nodes-autosend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "n8n-nodes-<...>",
"name": "n8n-nodes-autosend",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {

View file

@ -1,19 +1,19 @@
{
"name": "n8n-nodes-<...>",
"name": "n8n-nodes-autosend",
"version": "0.1.0",
"description": "",
"description": "n8n node for Autosend - Send emails and manage contacts",
"license": "MIT",
"homepage": "",
"keywords": [
"n8n-community-node-package"
],
"author": {
"name": "",
"email": ""
"name": "Your Name",
"email": "your.email@example.com"
},
"repository": {
"type": "git",
"url": "https://github.com/<...>/n8n-nodes-<...>.git"
"url": "https://github.com/codebuster22/autosend-n8n-nodes.git"
},
"scripts": {
"build": "n8n-node build",
@ -31,10 +31,12 @@
"n8nNodesApiVersion": 1,
"strict": true,
"credentials": [
"dist/credentials/AutosendApi.credentials.js",
"dist/credentials/GithubIssuesApi.credentials.js",
"dist/credentials/GithubIssuesOAuth2Api.credentials.js"
],
"nodes": [
"dist/nodes/Autosend/Autosend.node.js",
"dist/nodes/GithubIssues/GithubIssues.node.js",
"dist/nodes/Example/Example.node.js"
]