diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index b70e121fd0..a33f16156f 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -135,7 +135,6 @@ describe('Node Creator', () => { 'OpenThesaurus', 'Spontit', 'Vonage', - 'Send Email', 'Toggl Trigger', ]; const doubleActionNode = 'OpenWeatherMap'; diff --git a/packages/nodes-base/nodes/EmailSend/test/v2/sendAndWait.operation.test.ts b/packages/nodes-base/nodes/EmailSend/test/v2/sendAndWait.operation.test.ts new file mode 100644 index 0000000000..f0dfea2042 --- /dev/null +++ b/packages/nodes-base/nodes/EmailSend/test/v2/sendAndWait.operation.test.ts @@ -0,0 +1,68 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import { SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow'; + +import { EmailSendV2, versionDescription } from '../../v2/EmailSendV2.node'; +import * as utils from '../../v2/utils'; + +const transporter = { sendMail: jest.fn() }; + +jest.mock('../../v2/utils', () => { + const originalModule = jest.requireActual('../../v2/utils'); + return { + ...originalModule, + configureTransport: jest.fn(() => transporter), + }; +}); + +describe('Test EmailSendV2, email => sendAndWait', () => { + let emailSendV2: EmailSendV2; + let mockExecuteFunctions: MockProxy; + + beforeEach(() => { + emailSendV2 = new EmailSendV2(versionDescription); + mockExecuteFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should send message and put execution to wait', async () => { + const items = [{ json: { data: 'test' } }]; + //node + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION); + + //operation + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('from@mail.com'); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('to@mail.com'); + mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId'); + mockExecuteFunctions.getCredentials.mockResolvedValue({}); + mockExecuteFunctions.putExecutionToWait.mockImplementation(); + mockExecuteFunctions.getInputData.mockReturnValue(items); + + //getSendAndWaitConfig + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message'); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject'); + mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('http://localhost/waiting-webhook'); + mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('nodeID'); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval'); + + // configureWaitTillDate + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options.limitWaitTime.values + + const result = await emailSendV2.execute.call(mockExecuteFunctions); + + expect(result).toEqual([items]); + expect(utils.configureTransport).toHaveBeenCalledTimes(1); + expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1); + + expect(transporter.sendMail).toHaveBeenCalledWith({ + from: 'from@mail.com', + html: expect.stringContaining('href="http://localhost/waiting-webhook/nodeID?approved=true"'), + subject: 'my subject', + to: 'to@mail.com', + }); + }); +}); diff --git a/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts b/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts index 13caad5b5d..1b1a363875 100644 --- a/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts +++ b/packages/nodes-base/nodes/EmailSend/v2/EmailSendV2.node.ts @@ -5,11 +5,15 @@ import type { INodeTypeBaseDescription, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeConnectionType } from 'n8n-workflow'; +import { NodeConnectionType, SEND_AND_WAIT_OPERATION } from 'n8n-workflow'; import * as send from './send.operation'; +import * as sendAndWait from './sendAndWait.operation'; +import { smtpConnectionTest } from './utils'; +import { sendAndWaitWebhooksDescription } from '../../../utils/sendAndWait/descriptions'; +import { sendAndWaitWebhook } from '../../../utils/sendAndWait/utils'; -const versionDescription: INodeTypeDescription = { +export const versionDescription: INodeTypeDescription = { displayName: 'Send Email', name: 'emailSend', icon: 'fa:envelope', @@ -30,6 +34,7 @@ const versionDescription: INodeTypeDescription = { testedBy: 'smtpConnectionTest', }, ], + webhooks: sendAndWaitWebhooksDescription, properties: [ { displayName: 'Resource', @@ -47,7 +52,7 @@ const versionDescription: INodeTypeDescription = { { displayName: 'Operation', name: 'operation', - type: 'hidden', + type: 'options', noDataExpression: true, default: 'send', options: [ @@ -56,9 +61,15 @@ const versionDescription: INodeTypeDescription = { value: 'send', action: 'Send an Email', }, + { + name: 'Send and Wait for Response', + value: SEND_AND_WAIT_OPERATION, + action: 'Send message and wait for response', + }, ], }, ...send.description, + ...sendAndWait.description, ], }; @@ -73,13 +84,22 @@ export class EmailSendV2 implements INodeType { } methods = { - credentialTest: { smtpConnectionTest: send.smtpConnectionTest }, + credentialTest: { smtpConnectionTest }, }; + webhook = sendAndWaitWebhook; + async execute(this: IExecuteFunctions): Promise { let returnData: INodeExecutionData[][] = []; + const operation = this.getNodeParameter('operation', 0) as string; - returnData = await send.execute.call(this); + if (operation === SEND_AND_WAIT_OPERATION) { + returnData = await sendAndWait.execute.call(this); + } + + if (operation === 'send') { + returnData = await send.execute.call(this); + } return returnData; } diff --git a/packages/nodes-base/nodes/EmailSend/v2/descriptions.ts b/packages/nodes-base/nodes/EmailSend/v2/descriptions.ts new file mode 100644 index 0000000000..0416813ae3 --- /dev/null +++ b/packages/nodes-base/nodes/EmailSend/v2/descriptions.ts @@ -0,0 +1,23 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const fromEmailProperty: INodeProperties = { + 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>.', +}; + +export const toEmailProperty: INodeProperties = { + 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>.', +}; diff --git a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts index b8f065a8b7..32f33566fb 100644 --- a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts +++ b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts @@ -1,43 +1,22 @@ import type { - ICredentialsDecrypted, - ICredentialTestFunctions, IDataObject, IExecuteFunctions, - INodeCredentialTestResult, INodeExecutionData, INodeProperties, JsonObject, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; -import { createTransport } from 'nodemailer'; -import type SMTPTransport from 'nodemailer/lib/smtp-transport'; import { updateDisplayOptions } from '@utils/utilities'; +import { fromEmailProperty, toEmailProperty } from './descriptions'; +import { configureTransport, type EmailSendOptions } from './utils'; import { appendAttributionOption } from '../../../utils/descriptions'; 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>.', - }, + fromEmailProperty, + toEmailProperty, { displayName: 'Subject', @@ -194,72 +173,11 @@ const displayOptions = { export const description = updateDisplayOptions(displayOptions, properties); -type EmailSendOptions = { - appendAttribution?: boolean; - 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.secure === false) { - connectionOptions.ignoreTLS = credentials.disableStartTls as boolean; - } - - if (typeof credentials.hostName === 'string' && credentials.hostName) { - connectionOptions.name = credentials.hostName; - } - - 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 smtpConnectionTest( - this: ICredentialTestFunctions, - credential: ICredentialsDecrypted, -): Promise { - const credentials = credential.data!; - const transporter = configureTransport(credentials, {}); - try { - await transporter.verify(); - return { - status: 'OK', - message: 'Connection successful!', - }; - } catch (error) { - return { - status: 'Error', - message: error.message, - }; - } finally { - transporter.close(); - } -} - export async function execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const nodeVersion = this.getNode().typeVersion; const instanceId = this.getInstanceId(); + const credentials = await this.getCredentials('smtp'); const returnData: INodeExecutionData[] = []; let item: INodeExecutionData; @@ -274,8 +192,6 @@ export async function execute(this: IExecuteFunctions): Promise { + const fromEmail = this.getNodeParameter('fromEmail', 0) as string; + const toEmail = this.getNodeParameter('toEmail', 0) as string; + + const config = getSendAndWaitConfig(this); + const buttons: string[] = []; + for (const option of config.options) { + buttons.push(createButton(config.url, option.label, option.value, option.style)); + } + + const instanceId = this.getInstanceId(); + + const htmlBody = createEmailBody(config.message, buttons.join('\n'), instanceId); + + const mailOptions: IDataObject = { + from: fromEmail, + to: toEmail, + subject: config.title, + html: htmlBody, + }; + + const credentials = await this.getCredentials('smtp'); + const transporter = configureTransport(credentials, {}); + + await transporter.sendMail(mailOptions); + + const waitTill = configureWaitTillDate(this); + + await this.putExecutionToWait(waitTill); + return [this.getInputData()]; +} diff --git a/packages/nodes-base/nodes/EmailSend/v2/utils.ts b/packages/nodes-base/nodes/EmailSend/v2/utils.ts new file mode 100644 index 0000000000..ce680ab1ab --- /dev/null +++ b/packages/nodes-base/nodes/EmailSend/v2/utils.ts @@ -0,0 +1,70 @@ +import type { + IDataObject, + ICredentialsDecrypted, + ICredentialTestFunctions, + INodeCredentialTestResult, +} from 'n8n-workflow'; +import { createTransport } from 'nodemailer'; +import type SMTPTransport from 'nodemailer/lib/smtp-transport'; + +export type EmailSendOptions = { + appendAttribution?: boolean; + allowUnauthorizedCerts?: boolean; + attachments?: string; + ccEmail?: string; + bccEmail?: string; + replyTo?: string; +}; + +export 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.secure === false) { + connectionOptions.ignoreTLS = credentials.disableStartTls as boolean; + } + + if (typeof credentials.hostName === 'string' && credentials.hostName) { + connectionOptions.name = credentials.hostName; + } + + 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 smtpConnectionTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, +): Promise { + const credentials = credential.data!; + const transporter = configureTransport(credentials, {}); + try { + await transporter.verify(); + return { + status: 'OK', + message: 'Connection successful!', + }; + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } finally { + transporter.close(); + } +} diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts index b6c117296c..7c55c11eff 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -13,7 +13,7 @@ import { labelFields, labelOperations } from './LabelDescription'; import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions'; import { messageFields, messageOperations } from './MessageDescription'; import { threadFields, threadOperations } from './ThreadDescription'; -import { sendAndWaitWebhooks } from '../../../../utils/sendAndWait/descriptions'; +import { sendAndWaitWebhooksDescription } from '../../../../utils/sendAndWait/descriptions'; import type { IEmail } from '../../../../utils/sendAndWait/interfaces'; import { configureWaitTillDate, @@ -69,7 +69,7 @@ const versionDescription: INodeTypeDescription = { }, }, ], - webhooks: sendAndWaitWebhooks, + webhooks: sendAndWaitWebhooksDescription, properties: [ { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/sendAndWait.test.ts b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/sendAndWait.test.ts index 7446118275..0375f1d40f 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/sendAndWait.test.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/test/v2/node/message/sendAndWait.test.ts @@ -51,6 +51,9 @@ describe('Test MicrosoftOutlookV2, message => sendAndWait', () => { mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval'); + // configureWaitTillDate + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options.limitWaitTime.values + const result = await microsoftOutlook.execute.call(mockExecuteFunctions); expect(result).toEqual([items]); diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts index 5b9596c9d1..1c163d731d 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts @@ -9,7 +9,7 @@ import * as folder from './folder'; import * as folderMessage from './folderMessage'; import * as message from './message'; import * as messageAttachment from './messageAttachment'; -import { sendAndWaitWebhooks } from '../../../../../utils/sendAndWait/descriptions'; +import { sendAndWaitWebhooksDescription } from '../../../../../utils/sendAndWait/descriptions'; export const description: INodeTypeDescription = { displayName: 'Microsoft Outlook', @@ -31,7 +31,7 @@ export const description: INodeTypeDescription = { required: true, }, ], - webhooks: sendAndWaitWebhooks, + webhooks: sendAndWaitWebhooksDescription, properties: [ { displayName: 'Resource', diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts index 64b1bffc65..8a05975e56 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/router.ts @@ -1,10 +1,5 @@ import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; -import { - NodeApiError, - NodeOperationError, - SEND_AND_WAIT_OPERATION, - WAIT_INDEFINITELY, -} from 'n8n-workflow'; +import { NodeApiError, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow'; import * as calendar from './calendar'; import * as contact from './contact'; @@ -15,6 +10,7 @@ import * as folderMessage from './folderMessage'; import * as message from './message'; import * as messageAttachment from './messageAttachment'; import type { MicrosoftOutlook } from './node.type'; +import { configureWaitTillDate } from '../../../../../utils/sendAndWait/utils'; export async function router(this: IExecuteFunctions) { const items = this.getInputData(); @@ -36,7 +32,9 @@ export async function router(this: IExecuteFunctions) { ) { await message[microsoftOutlook.operation].execute.call(this, 0, items); - await this.putExecutionToWait(WAIT_INDEFINITELY); + const waitTill = configureWaitTillDate(this); + + await this.putExecutionToWait(waitTill); return [items]; } diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 1ff61e3372..15d8e7bfbd 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -41,7 +41,7 @@ import { reactionFields, reactionOperations } from './ReactionDescription'; import { starFields, starOperations } from './StarDescription'; import { userFields, userOperations } from './UserDescription'; import { userGroupFields, userGroupOperations } from './UserGroupDescription'; -import { sendAndWaitWebhooks } from '../../../utils/sendAndWait/descriptions'; +import { sendAndWaitWebhooksDescription } from '../../../utils/sendAndWait/descriptions'; import { configureWaitTillDate, getSendAndWaitProperties, @@ -81,7 +81,7 @@ export class SlackV2 implements INodeType { }, }, ], - webhooks: sendAndWaitWebhooks, + webhooks: sendAndWaitWebhooksDescription, properties: [ { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index a51a5f3f3b..6f7e4906e3 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -21,7 +21,7 @@ import { getPropertyName, } from './GenericFunctions'; import { appendAttributionOption } from '../../utils/descriptions'; -import { sendAndWaitWebhooks } from '../../utils/sendAndWait/descriptions'; +import { sendAndWaitWebhooksDescription } from '../../utils/sendAndWait/descriptions'; import { configureWaitTillDate, getSendAndWaitProperties, @@ -49,7 +49,7 @@ export class Telegram implements INodeType { required: true, }, ], - webhooks: sendAndWaitWebhooks, + webhooks: sendAndWaitWebhooksDescription, properties: [ { displayName: 'Resource', diff --git a/packages/nodes-base/utils/sendAndWait/descriptions.ts b/packages/nodes-base/utils/sendAndWait/descriptions.ts index 3ec38e6ddd..1549a6cc98 100644 --- a/packages/nodes-base/utils/sendAndWait/descriptions.ts +++ b/packages/nodes-base/utils/sendAndWait/descriptions.ts @@ -1,6 +1,6 @@ import type { IWebhookDescription } from 'n8n-workflow'; -export const sendAndWaitWebhooks: IWebhookDescription[] = [ +export const sendAndWaitWebhooksDescription: IWebhookDescription[] = [ { name: 'default', httpMethod: 'GET',