From 832fb87954d480ed46913c8b0f8067c96db28aab Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Tue, 24 Jan 2023 12:32:31 +0200 Subject: [PATCH] feat(Send Email Node): Overhaul --- .../nodes/EmailSend/EmailSend.node.json | 3 +- .../nodes/EmailSend/EmailSend.node.ts | 246 ++--------------- .../nodes/EmailSend/v1/EmailSendV1.node.ts | 255 +++++++++++++++++ .../nodes/EmailSend/v2/EmailSendV2.node.ts | 81 ++++++ .../nodes/EmailSend/v2/send.operation.ts | 260 ++++++++++++++++++ .../nodes/Microsoft/Sql/MicrosoftSql.node.ts | 2 +- .../{nodes => }/utils/allCurrencies.ts | 0 .../nodes-base/{nodes => }/utils/utilities.ts | 16 ++ 8 files changed, 633 insertions(+), 230 deletions(-) create mode 100644 packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts create mode 100644 packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts create mode 100644 packages/nodes-base/nodes/EmailSend/v2/send.operation.ts rename packages/nodes-base/{nodes => }/utils/allCurrencies.ts (100%) rename packages/nodes-base/{nodes => }/utils/utilities.ts (77%) diff --git a/packages/nodes-base/nodes/EmailSend/EmailSend.node.json b/packages/nodes-base/nodes/EmailSend/EmailSend.node.json index b82091851c..51a9454610 100644 --- a/packages/nodes-base/nodes/EmailSend/EmailSend.node.json +++ b/packages/nodes-base/nodes/EmailSend/EmailSend.node.json @@ -29,5 +29,6 @@ }, "subcategories": { "Core Nodes": ["Helpers"] - } + }, + "alias": ["SMTP"] } diff --git a/packages/nodes-base/nodes/EmailSend/EmailSend.node.ts b/packages/nodes-base/nodes/EmailSend/EmailSend.node.ts index f6bbaf5eca..2c41a21bf1 100644 --- a/packages/nodes-base/nodes/EmailSend/EmailSend.node.ts +++ b/packages/nodes-base/nodes/EmailSend/EmailSend.node.ts @@ -1,234 +1,24 @@ -import { IExecuteFunctions } from 'n8n-core'; -import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow'; +import { INodeTypeBaseDescription, IVersionedNodeType, VersionedNodeType } from 'n8n-workflow'; -import { createTransport } from 'nodemailer'; -import SMTPTransport from 'nodemailer/lib/smtp-transport'; +import { EmailSendV1 } from './v1/EmailSendV1.node'; +import { EmailSendV2 } from './v2/EmailSendV2.node'; -export class EmailSend implements INodeType { - description: INodeTypeDescription = { - displayName: 'Send Email', - name: 'emailSend', - icon: 'fa:envelope', - group: ['output'], - version: 1, - description: 'Sends an Email', - defaults: { - name: 'Send Email', - color: '#00bb88', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'smtp', - required: true, - }, - ], - properties: [ - // TODO: Add choice for text as text or html (maybe also from name) - { - displayName: 'From Email', - name: 'fromEmail', - type: 'string', - default: '', - required: true, - placeholder: 'admin@example.com', - description: 'Email address of the sender optional with name', - }, - { - displayName: 'To Email', - name: 'toEmail', - type: 'string', - default: '', - required: true, - placeholder: 'info@example.com', - description: 'Email address of the recipient', - }, - { - displayName: 'CC Email', - name: 'ccEmail', - type: 'string', - default: '', - placeholder: 'cc@example.com', - description: 'Email address of CC recipient', - }, - { - displayName: 'BCC Email', - name: 'bccEmail', - type: 'string', - default: '', - placeholder: 'bcc@example.com', - description: 'Email address of BCC recipient', - }, - { - displayName: 'Subject', - name: 'subject', - type: 'string', - default: '', - placeholder: 'My subject line', - description: 'Subject line of the email', - }, - { - displayName: 'Text', - name: 'text', - type: 'string', - typeOptions: { - rows: 5, - }, - default: '', - description: 'Plain text message of email', - }, - { - displayName: 'HTML', - name: 'html', - type: 'string', - typeOptions: { - rows: 5, - }, - default: '', - description: 'HTML text message of email', - }, - { - displayName: 'Attachments', - name: 'attachments', - type: 'string', - default: '', - description: - 'Name of the binary properties that contain data to add to email as attachment. Multiple ones can be comma-separated.', - }, - { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add Option', - default: {}, - options: [ - { - displayName: 'Ignore SSL Issues', - name: 'allowUnauthorizedCerts', - type: 'boolean', - default: false, - description: 'Whether to connect even if SSL certificate validation is not possible', - }, - { - displayName: 'Reply To', - name: 'replyTo', - type: 'string', - default: '', - placeholder: 'info@example.com', - description: 'The email address to send the reply to', - }, - ], - }, - ], - }; +export class EmailSend extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Send Email', + name: 'emailSend', + icon: 'fa:envelope', + group: ['output'], + defaultVersion: 2, + description: 'Sends an email using SMTP protocol', + }; - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new EmailSendV1(baseDescription), + 2: new EmailSendV2(baseDescription), + }; - const returnData: INodeExecutionData[] = []; - const length = items.length; - let item: INodeExecutionData; - - for (let itemIndex = 0; itemIndex < length; itemIndex++) { - try { - item = items[itemIndex]; - - const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; - const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; - const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; - const bccEmail = this.getNodeParameter('bccEmail', itemIndex) as string; - const subject = this.getNodeParameter('subject', itemIndex) as string; - const text = this.getNodeParameter('text', itemIndex) as string; - const html = this.getNodeParameter('html', itemIndex) as string; - const attachmentPropertyString = this.getNodeParameter('attachments', itemIndex) as string; - const options = this.getNodeParameter('options', itemIndex, {}); - - const credentials = await this.getCredentials('smtp'); - - const connectionOptions: SMTPTransport.Options = { - host: credentials.host as string, - port: credentials.port as number, - secure: credentials.secure as boolean, - }; - - if (credentials.user || credentials.password) { - // @ts-ignore - connectionOptions.auth = { - user: credentials.user as string, - pass: credentials.password as string, - }; - } - - if (options.allowUnauthorizedCerts === true) { - connectionOptions.tls = { - rejectUnauthorized: false, - }; - } - - const transporter = createTransport(connectionOptions); - - // setup email data with unicode symbols - const mailOptions = { - from: fromEmail, - to: toEmail, - cc: ccEmail, - bcc: bccEmail, - subject, - text, - html, - replyTo: options.replyTo as string | undefined, - }; - - if (attachmentPropertyString && item.binary) { - const attachments = []; - const attachmentProperties: string[] = attachmentPropertyString - .split(',') - .map((propertyName) => { - return propertyName.trim(); - }); - - for (const propertyName of attachmentProperties) { - if (!item.binary.hasOwnProperty(propertyName)) { - continue; - } - attachments.push({ - filename: item.binary[propertyName].fileName || 'unknown', - content: await this.helpers.getBinaryDataBuffer(itemIndex, propertyName), - }); - } - - if (attachments.length) { - // @ts-ignore - mailOptions.attachments = attachments; - } - } - - // Send the email - const info = await transporter.sendMail(mailOptions); - - returnData.push({ - json: info as unknown as IDataObject, - pairedItem: { - item: itemIndex, - }, - }); - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ - json: { - error: error.message, - }, - pairedItem: { - item: itemIndex, - }, - }); - continue; - } - throw error; - } - } - - return this.prepareOutputData(returnData); + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts b/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts new file mode 100644 index 0000000000..9de9655f26 --- /dev/null +++ b/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts @@ -0,0 +1,255 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import { IExecuteFunctions } from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { createTransport } from 'nodemailer'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Send Email', + name: 'emailSend', + icon: 'fa:envelope', + group: ['output'], + version: 1, + description: 'Sends an Email', + defaults: { + name: 'Send Email', + color: '#00bb88', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'smtp', + required: true, + }, + ], + properties: [ + { + displayName: 'Version 1', + name: 'notice', + type: 'notice', + default: '', + }, + // TODO: Add choice for text as text or html (maybe also from name) + { + displayName: 'From Email', + name: 'fromEmail', + type: 'string', + default: '', + required: true, + placeholder: 'admin@example.com', + description: 'Email address of the sender optional with name', + }, + { + displayName: 'To Email', + name: 'toEmail', + type: 'string', + default: '', + required: true, + placeholder: 'info@example.com', + description: 'Email address of the recipient', + }, + { + displayName: 'CC Email', + name: 'ccEmail', + type: 'string', + default: '', + placeholder: 'cc@example.com', + description: 'Email address of CC recipient', + }, + { + displayName: 'BCC Email', + name: 'bccEmail', + type: 'string', + default: '', + placeholder: 'bcc@example.com', + description: 'Email address of BCC recipient', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + placeholder: 'My subject line', + description: 'Subject line of the email', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + rows: 5, + }, + default: '', + description: 'Plain text message of email', + }, + { + displayName: 'HTML', + name: 'html', + type: 'string', + typeOptions: { + rows: 5, + }, + default: '', + description: 'HTML text message of email', + }, + { + displayName: 'Attachments', + name: 'attachments', + type: 'string', + default: '', + description: + 'Name of the binary properties that contain data to add to email as attachment. Multiple ones can be comma-separated.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + default: false, + description: 'Whether to connect even if SSL certificate validation is not possible', + }, + { + displayName: 'Reply To', + name: 'replyTo', + type: 'string', + default: '', + placeholder: 'info@example.com', + description: 'The email address to send the reply to', + }, + ], + }, + ], +}; + +export class EmailSendV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const returnData: INodeExecutionData[] = []; + const length = items.length; + let item: INodeExecutionData; + + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + try { + item = items[itemIndex]; + + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; + const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; + const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; + const bccEmail = this.getNodeParameter('bccEmail', itemIndex) as string; + const subject = this.getNodeParameter('subject', itemIndex) as string; + const text = this.getNodeParameter('text', itemIndex) as string; + const html = this.getNodeParameter('html', itemIndex) as string; + const attachmentPropertyString = this.getNodeParameter('attachments', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}); + + const credentials = await this.getCredentials('smtp'); + + const connectionOptions: SMTPTransport.Options = { + host: credentials.host as string, + port: credentials.port as number, + secure: credentials.secure as boolean, + }; + + if (credentials.user || credentials.password) { + connectionOptions.auth = { + user: credentials.user as string, + pass: credentials.password as string, + }; + } + + if (options.allowUnauthorizedCerts === true) { + connectionOptions.tls = { + rejectUnauthorized: false, + }; + } + + const transporter = createTransport(connectionOptions); + + // setup email data with unicode symbols + const mailOptions: IDataObject = { + from: fromEmail, + to: toEmail, + cc: ccEmail, + bcc: bccEmail, + subject, + text, + html, + replyTo: options.replyTo as string | undefined, + }; + + if (attachmentPropertyString && item.binary) { + const attachments = []; + const attachmentProperties: string[] = attachmentPropertyString + .split(',') + .map((propertyName) => { + return propertyName.trim(); + }); + + for (const propertyName of attachmentProperties) { + if (!item.binary.hasOwnProperty(propertyName)) { + continue; + } + attachments.push({ + filename: item.binary[propertyName].fileName || 'unknown', + content: await this.helpers.getBinaryDataBuffer(itemIndex, propertyName), + }); + } + + if (attachments.length) { + mailOptions.attachments = attachments; + } + } + + // Send the email + const info = await transporter.sendMail(mailOptions); + + returnData.push({ + json: info as unknown as IDataObject, + pairedItem: { + item: itemIndex, + }, + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: itemIndex, + }, + }); + continue; + } + throw error; + } + } + + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts b/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts new file mode 100644 index 0000000000..0a940e3963 --- /dev/null +++ b/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts @@ -0,0 +1,81 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import { IExecuteFunctions } from 'n8n-core'; + +import { + INodeExecutionData, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, +} from 'n8n-workflow'; + +import * as send from './send.operation'; + +// eslint-disable-next-line n8n-nodes-base/node-class-description-missing-subtitle +const versionDescription: INodeTypeDescription = { + displayName: 'Send Email', + name: 'emailSend', + icon: 'fa:envelope', + group: ['output'], + version: 2, + description: 'Sends an email using SMTP protocol', + defaults: { + name: 'Send Email', + color: '#00bb88', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'smtp', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'hidden', + noDataExpression: true, + default: 'email', + options: [ + { + name: 'Email', + value: 'email', + }, + ], + }, + { + displayName: 'Operation', + name: 'operation', + type: 'hidden', + noDataExpression: true, + default: 'send', + options: [ + { + name: 'Send', + value: 'send', + }, + ], + }, + ...send.description, + ], +}; + +export class EmailSendV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + async execute(this: IExecuteFunctions): Promise { + let returnData: INodeExecutionData[][] = []; + + returnData = await send.execute.call(this); + + return returnData; + } +} diff --git a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts new file mode 100644 index 0000000000..eb3c499e73 --- /dev/null +++ b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts @@ -0,0 +1,260 @@ +import { IDataObject, IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import { createTransport } from 'nodemailer'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +import { updateDisplayOptions } from '../../../utils/utilities'; + +const properties: INodeProperties[] = [ + // TODO: Add choice for text as text or html (maybe also from name) + { + displayName: 'From Email', + name: 'fromEmail', + type: 'string', + default: '', + required: true, + placeholder: 'admin@example.com', + description: + 'Email address of the sender. You can also specify a name: Nathan Doe <nate@n8n.io>.', + }, + { + displayName: 'To Email', + name: 'toEmail', + type: 'string', + default: '', + required: true, + placeholder: 'info@example.com', + description: + 'Email address of the recipient. You can also specify a name: Nathan Doe <nate@n8n.io>.', + }, + + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + placeholder: 'My subject line', + description: 'Subject line of the email', + }, + { + displayName: 'Email Format', + name: 'emailFormat', + type: 'options', + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'HTML', + value: 'html', + }, + ], + default: 'text', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + rows: 5, + }, + default: '', + description: 'Plain text message of email', + displayOptions: { + show: { + emailFormat: ['text'], + }, + }, + }, + { + displayName: 'HTML', + name: 'html', + type: 'string', + typeOptions: { + rows: 5, + }, + default: '', + description: 'HTML text message of email', + displayOptions: { + show: { + emailFormat: ['html'], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'string', + default: '', + description: + 'Name of the binary properties that contain data to add to email as attachment. Multiple ones can be comma-separated.', + }, + { + displayName: 'CC Email', + name: 'ccEmail', + type: 'string', + default: '', + placeholder: 'cc@example.com', + description: 'Email address of CC recipient', + }, + { + displayName: 'BCC Email', + name: 'bccEmail', + type: 'string', + default: '', + placeholder: 'bcc@example.com', + description: 'Email address of BCC recipient', + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + default: false, + description: 'Whether to connect even if SSL certificate validation is not possible', + }, + { + displayName: 'Reply To', + name: 'replyTo', + type: 'string', + default: '', + placeholder: 'info@example.com', + description: 'The email address to send the reply to', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['email'], + operation: ['send'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +type EmailSendOptions = { + allowUnauthorizedCerts?: boolean; + attachments?: string; + ccEmail?: string; + bccEmail?: string; + replyTo?: string; +}; + +function configureTransport(credentials: IDataObject, options: EmailSendOptions) { + const connectionOptions: SMTPTransport.Options = { + host: credentials.host as string, + port: credentials.port as number, + secure: credentials.secure as boolean, + }; + + if (credentials.user || credentials.password) { + connectionOptions.auth = { + user: credentials.user as string, + pass: credentials.password as string, + }; + } + + if (options.allowUnauthorizedCerts === true) { + connectionOptions.tls = { + rejectUnauthorized: false, + }; + } + + return createTransport(connectionOptions); +} + +export async function execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const returnData: INodeExecutionData[] = []; + let item: INodeExecutionData; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + item = items[itemIndex]; + + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; + const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; + const subject = this.getNodeParameter('subject', itemIndex) as string; + const emailFormat = this.getNodeParameter('emailFormat', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}) as EmailSendOptions; + + const credentials = await this.getCredentials('smtp'); + + const transporter = configureTransport(credentials, options); + + const mailOptions: IDataObject = { + from: fromEmail, + to: toEmail, + cc: options.ccEmail, + bcc: options.bccEmail, + subject, + replyTo: options.replyTo, + }; + + if (emailFormat === 'text') { + mailOptions.text = this.getNodeParameter('text', itemIndex, ''); + } + + if (emailFormat === 'html') { + mailOptions.html = this.getNodeParameter('html', itemIndex, ''); + } + + if (options.attachments && item.binary) { + const attachments = []; + const attachmentProperties: string[] = options.attachments + .split(',') + .map((propertyName) => { + return propertyName.trim(); + }); + + for (const propertyName of attachmentProperties) { + if (!item.binary.hasOwnProperty(propertyName)) { + continue; + } + attachments.push({ + filename: item.binary[propertyName].fileName || 'unknown', + content: await this.helpers.getBinaryDataBuffer(itemIndex, propertyName), + }); + } + + if (attachments.length) { + mailOptions.attachments = attachments; + } + } + + const info = await transporter.sendMail(mailOptions); + + returnData.push({ + json: info as unknown as IDataObject, + pairedItem: { + item: itemIndex, + }, + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: itemIndex, + }, + }); + continue; + } + throw error; + } + } + + return this.prepareOutputData(returnData); +} diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index dc2fde8ffd..bcc173ae81 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -12,7 +12,7 @@ import { NodeOperationError, } from 'n8n-workflow'; -import { chunk, flatten } from '../../utils/utilities'; +import { chunk, flatten } from '../../../utils/utilities'; import mssql from 'mssql'; diff --git a/packages/nodes-base/nodes/utils/allCurrencies.ts b/packages/nodes-base/utils/allCurrencies.ts similarity index 100% rename from packages/nodes-base/nodes/utils/allCurrencies.ts rename to packages/nodes-base/utils/allCurrencies.ts diff --git a/packages/nodes-base/nodes/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts similarity index 77% rename from packages/nodes-base/nodes/utils/utilities.ts rename to packages/nodes-base/utils/utilities.ts index 920087e1e0..6cf759bbe4 100644 --- a/packages/nodes-base/nodes/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -1,3 +1,7 @@ +import { IDisplayOptions, INodeProperties } from 'n8n-workflow'; + +import { merge } from 'lodash'; + /** * Creates an array of elements split into groups the length of `size`. * If `array` can't be split evenly, the final chunk will be the remaining @@ -55,3 +59,15 @@ export function flatten(nestedArray: any[][]) { return result; } + +export function updateDisplayOptions( + displayOptions: IDisplayOptions, + properties: INodeProperties[], +) { + return properties.map((nodeProperty) => { + return { + ...nodeProperty, + displayOptions: merge({}, nodeProperty.displayOptions, displayOptions), + }; + }); +}