diff --git a/packages/nodes-base/credentials/Imap.credentials.ts b/packages/nodes-base/credentials/Imap.credentials.ts
index a36d46ef93..856835dfe4 100644
--- a/packages/nodes-base/credentials/Imap.credentials.ts
+++ b/packages/nodes-base/credentials/Imap.credentials.ts
@@ -38,5 +38,32 @@ export class Imap implements ICredentialType {
type: 'boolean',
default: true,
},
+ {
+ displayName: 'Allow Self-Signed Certificates',
+ name: 'allowUnauthorizedCerts',
+ type: 'boolean',
+ description: 'Whether to connect even if SSL certificate validation is not possible',
+ default: false,
+ },
];
}
+
+export interface ICredentialsDataImap {
+ host: string;
+ port: number;
+ user: string;
+ password: string;
+ secure: boolean;
+ allowUnauthorizedCerts: boolean;
+}
+
+export function isCredentialsDataImap(candidate: unknown): candidate is ICredentialsDataImap {
+ const o = candidate as ICredentialsDataImap;
+ return (
+ o.host !== undefined &&
+ o.password !== undefined &&
+ o.port !== undefined &&
+ o.secure !== undefined &&
+ o.user !== undefined
+ );
+}
diff --git a/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts
index 620783ed62..af0c7cb238 100644
--- a/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts
+++ b/packages/nodes-base/nodes/EmailReadImap/EmailReadImap.node.ts
@@ -1,616 +1,24 @@
-import { ITriggerFunctions } from 'n8n-core';
-import {
- createDeferredPromise,
- IBinaryData,
- IBinaryKeyData,
- ICredentialDataDecryptedObject,
- ICredentialsDecrypted,
- ICredentialTestFunctions,
- IDataObject,
- IDeferredPromise,
- INodeCredentialTestResult,
- INodeExecutionData,
- INodeType,
- INodeTypeDescription,
- ITriggerResponse,
- LoggerProxy as Logger,
- NodeOperationError,
-} from 'n8n-workflow';
+import { INodeTypeBaseDescription, INodeVersionedType } from 'n8n-workflow';
+import { NodeVersionedType } from '../../src/NodeVersionedType';
+import { EmailReadImapV1 } from './v1/EmailReadImapV1.node';
+import { EmailReadImapV2 } from './v2/EmailReadImapV2.node';
-import {
- connect as imapConnect,
- getParts,
- ImapSimple,
- ImapSimpleOptions,
- Message,
-} from 'imap-simple';
-import { simpleParser, Source as ParserSource } from 'mailparser';
-
-import _ from 'lodash';
-
-export class EmailReadImap implements INodeType {
- description: INodeTypeDescription = {
- displayName: 'Email Trigger (IMAP)',
- name: 'emailReadImap',
- icon: 'fa:inbox',
- group: ['trigger'],
- version: 1,
- description: 'Triggers the workflow when a new email is received',
- eventTriggerDescription: 'Waiting for you to receive an email',
- defaults: {
- name: 'Email Trigger',
- color: '#44AA22',
- },
- inputs: [],
- outputs: ['main'],
- credentials: [
- {
- name: 'imap',
- required: true,
- testedBy: 'imapConnectionTest',
- },
- ],
- properties: [
- {
- displayName: 'Mailbox Name',
- name: 'mailbox',
- type: 'string',
- default: 'INBOX',
- },
- {
- displayName: 'Action',
- name: 'postProcessAction',
- type: 'options',
- options: [
- {
- name: 'Mark as Read',
- value: 'read',
- },
- {
- name: 'Nothing',
- value: 'nothing',
- },
- ],
- default: 'read',
- description:
- 'What to do after the email has been received. If "nothing" gets selected it will be processed multiple times.',
- },
- {
- displayName: 'Download Attachments',
- name: 'downloadAttachments',
- type: 'boolean',
- default: false,
- displayOptions: {
- show: {
- format: ['simple'],
- },
- },
- description:
- 'Whether attachments of emails should be downloaded. Only set if needed as it increases processing.',
- },
- {
- displayName: 'Format',
- name: 'format',
- type: 'options',
- options: [
- {
- name: 'RAW',
- value: 'raw',
- description:
- 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used',
- },
- {
- name: 'Resolved',
- value: 'resolved',
- description:
- 'Returns the full email with all data resolved and attachments saved as binary data',
- },
- {
- name: 'Simple',
- value: 'simple',
- description:
- 'Returns the full email; do not use if you wish to gather inline attachments',
- },
- ],
- default: 'simple',
- description: 'The format to return the message in',
- },
- {
- displayName: 'Property Prefix Name',
- name: 'dataPropertyAttachmentsPrefixName',
- type: 'string',
- default: 'attachment_',
- displayOptions: {
- show: {
- format: ['resolved'],
- },
- },
- description:
- 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
- },
- {
- displayName: 'Property Prefix Name',
- name: 'dataPropertyAttachmentsPrefixName',
- type: 'string',
- default: 'attachment_',
- displayOptions: {
- show: {
- format: ['simple'],
- downloadAttachments: [true],
- },
- },
- description:
- 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
- },
- {
- displayName: 'Options',
- name: 'options',
- type: 'collection',
- placeholder: 'Add Option',
- default: {},
- options: [
- {
- displayName: 'Custom Email Rules',
- name: 'customEmailConfig',
- type: 'string',
- default: '["UNSEEN"]',
- description:
- 'Custom email fetching rules. See node-imap\'s search function for more details.',
- },
- {
- displayName: 'Ignore SSL Issues',
- name: 'allowUnauthorizedCerts',
- type: 'boolean',
- default: false,
- description: 'Whether to connect even if SSL certificate validation is not possible',
- },
- {
- displayName: 'Force Reconnect',
- name: 'forceReconnect',
- type: 'number',
- default: 60,
- description: 'Sets an interval (in minutes) to force a reconnection',
- },
- ],
- },
- ],
- };
-
- methods = {
- credentialTest: {
- async imapConnectionTest(
- this: ICredentialTestFunctions,
- credential: ICredentialsDecrypted,
- ): Promise {
- const credentials = credential.data as ICredentialDataDecryptedObject;
- try {
- const config: ImapSimpleOptions = {
- imap: {
- user: credentials.user as string,
- password: credentials.password as string,
- host: credentials.host as string,
- port: credentials.port as number,
- tls: credentials.secure as boolean,
- authTimeout: 20000,
- },
- };
- const tlsOptions: IDataObject = {};
-
- if (credentials.secure) {
- tlsOptions.servername = credentials.host as string;
- }
- if (!_.isEmpty(tlsOptions)) {
- config.imap.tlsOptions = tlsOptions;
- }
- const conn = imapConnect(config).then(async (conn) => {
- return conn;
- });
- (await conn).getBoxes((err, boxes) => {});
- } catch (error) {
- console.log(error);
- return {
- status: 'Error',
- message: error.message,
- };
- }
- return {
- status: 'OK',
- message: 'Connection successful!',
- };
- },
- },
- };
-
- async trigger(this: ITriggerFunctions): Promise {
- const credentials = await this.getCredentials('imap');
-
- const mailbox = this.getNodeParameter('mailbox') as string;
- const postProcessAction = this.getNodeParameter('postProcessAction') as string;
- const options = this.getNodeParameter('options', {}) as IDataObject;
-
- const staticData = this.getWorkflowStaticData('node');
- Logger.debug('Loaded static data for node "EmailReadImap"', { staticData });
-
- let connection: ImapSimple;
-
- // Returns the email text
- // tslint:disable-next-line:no-any
- const getText = async (parts: any[], message: Message, subtype: string) => {
- if (!message.attributes.struct) {
- return '';
- }
-
- const textParts = parts.filter((part) => {
- return (
- part.type.toUpperCase() === 'TEXT' && part.subtype.toUpperCase() === subtype.toUpperCase()
- );
- });
-
- if (textParts.length === 0) {
- return '';
- }
-
- try {
- return await connection.getPartData(message, textParts[0]);
- } catch {
- return '';
- }
+export class EmailReadImap extends NodeVersionedType {
+ constructor() {
+ const baseDescription: INodeTypeBaseDescription = {
+ displayName: 'Email Trigger (IMAP)',
+ name: 'emailReadImap',
+ icon: 'fa:inbox',
+ group: ['trigger'],
+ description: 'Triggers the workflow when a new email is received',
+ defaultVersion: 2,
};
- // Returns the email attachments
- const getAttachment = async (
- connection: ImapSimple,
- // tslint:disable-next-line:no-any
- parts: any[],
- message: Message,
- ): Promise => {
- if (!message.attributes.struct) {
- return [];
- }
-
- // Check if the message has attachments and if so get them
- const attachmentParts = parts.filter((part) => {
- return part.disposition && part.disposition.type.toUpperCase() === 'ATTACHMENT';
- });
-
- const attachmentPromises = [];
- let attachmentPromise;
- for (const attachmentPart of attachmentParts) {
- attachmentPromise = connection.getPartData(message, attachmentPart).then((partData) => {
- // Return it in the format n8n expects
- return this.helpers.prepareBinaryData(
- partData,
- attachmentPart.disposition.params.filename,
- );
- });
-
- attachmentPromises.push(attachmentPromise);
- }
-
- return Promise.all(attachmentPromises);
+ const nodeVersions: INodeVersionedType['nodeVersions'] = {
+ 1: new EmailReadImapV1(baseDescription),
+ 2: new EmailReadImapV2(baseDescription),
};
- // Returns all the new unseen messages
- const getNewEmails = async (
- connection: ImapSimple,
- searchCriteria: Array,
- ): Promise => {
- const format = this.getNodeParameter('format', 0) as string;
-
- let fetchOptions = {};
-
- if (format === 'simple' || format === 'raw') {
- fetchOptions = {
- bodies: ['TEXT', 'HEADER'],
- markSeen: postProcessAction === 'read',
- struct: true,
- };
- } else if (format === 'resolved') {
- fetchOptions = {
- bodies: [''],
- markSeen: postProcessAction === 'read',
- struct: true,
- };
- }
-
- const results = await connection.search(searchCriteria, fetchOptions);
-
- const newEmails: INodeExecutionData[] = [];
- let newEmail: INodeExecutionData, messageHeader, messageBody;
- let attachments: IBinaryData[];
- let propertyName: string;
-
- // All properties get by default moved to metadata except the ones
- // which are defined here which get set on the top level.
- const topLevelProperties = ['cc', 'date', 'from', 'subject', 'to'];
-
- if (format === 'resolved') {
- const dataPropertyAttachmentsPrefixName = this.getNodeParameter(
- 'dataPropertyAttachmentsPrefixName',
- ) as string;
-
- for (const message of results) {
- if (
- staticData.lastMessageUid !== undefined &&
- message.attributes.uid <= (staticData.lastMessageUid as number)
- ) {
- continue;
- }
- if (
- staticData.lastMessageUid === undefined ||
- (staticData.lastMessageUid as number) < message.attributes.uid
- ) {
- staticData.lastMessageUid = message.attributes.uid;
- }
- const part = _.find(message.parts, { which: '' });
-
- if (part === undefined) {
- throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
- }
- const parsedEmail = await parseRawEmail.call(
- this,
- part.body,
- dataPropertyAttachmentsPrefixName,
- );
-
- newEmails.push(parsedEmail);
- }
- } else if (format === 'simple') {
- const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
-
- let dataPropertyAttachmentsPrefixName = '';
- if (downloadAttachments === true) {
- dataPropertyAttachmentsPrefixName = this.getNodeParameter(
- 'dataPropertyAttachmentsPrefixName',
- ) as string;
- }
-
- for (const message of results) {
- if (
- staticData.lastMessageUid !== undefined &&
- message.attributes.uid <= (staticData.lastMessageUid as number)
- ) {
- continue;
- }
- if (
- staticData.lastMessageUid === undefined ||
- (staticData.lastMessageUid as number) < message.attributes.uid
- ) {
- staticData.lastMessageUid = message.attributes.uid;
- }
- const parts = getParts(message.attributes.struct!);
-
- newEmail = {
- json: {
- textHtml: await getText(parts, message, 'html'),
- textPlain: await getText(parts, message, 'plain'),
- metadata: {} as IDataObject,
- },
- };
-
- messageHeader = message.parts.filter((part) => {
- return part.which === 'HEADER';
- });
-
- messageBody = messageHeader[0].body;
- for (propertyName of Object.keys(messageBody)) {
- if (messageBody[propertyName].length) {
- if (topLevelProperties.includes(propertyName)) {
- newEmail.json[propertyName] = messageBody[propertyName][0];
- } else {
- (newEmail.json.metadata as IDataObject)[propertyName] =
- messageBody[propertyName][0];
- }
- }
- }
-
- if (downloadAttachments === true) {
- // Get attachments and add them if any get found
- attachments = await getAttachment(connection, parts, message);
- if (attachments.length) {
- newEmail.binary = {};
- for (let i = 0; i < attachments.length; i++) {
- newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
- }
- }
- }
-
- newEmails.push(newEmail);
- }
- } else if (format === 'raw') {
- for (const message of results) {
- if (
- staticData.lastMessageUid !== undefined &&
- message.attributes.uid <= (staticData.lastMessageUid as number)
- ) {
- continue;
- }
- if (
- staticData.lastMessageUid === undefined ||
- (staticData.lastMessageUid as number) < message.attributes.uid
- ) {
- staticData.lastMessageUid = message.attributes.uid;
- }
- const part = _.find(message.parts, { which: 'TEXT' });
-
- if (part === undefined) {
- throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
- }
- // Return base64 string
- newEmail = {
- json: {
- raw: part.body,
- },
- };
-
- newEmails.push(newEmail);
- }
- }
-
- return newEmails;
- };
-
- const returnedPromise: IDeferredPromise | undefined = await createDeferredPromise();
-
- const establishConnection = (): Promise => {
- let searchCriteria = ['UNSEEN'] as Array;
- if (options.customEmailConfig !== undefined) {
- try {
- searchCriteria = JSON.parse(options.customEmailConfig as string);
- } catch (error) {
- throw new NodeOperationError(this.getNode(), `Custom email config is not valid JSON.`);
- }
- }
-
- const config: ImapSimpleOptions = {
- imap: {
- user: credentials.user as string,
- password: credentials.password as string,
- host: credentials.host as string,
- port: credentials.port as number,
- tls: credentials.secure as boolean,
- authTimeout: 20000,
- },
- onmail: async () => {
- if (connection) {
- if (staticData.lastMessageUid !== undefined) {
- searchCriteria.push(['UID', `${staticData.lastMessageUid as number}:*`]);
- /**
- * A short explanation about UIDs and how they work
- * can be found here: https://dev.to/kehers/imap-new-messages-since-last-check-44gm
- * TL;DR:
- * - You cannot filter using ['UID', 'CURRENT ID + 1:*'] because IMAP
- * won't return correct results if current id + 1 does not yet exist.
- * - UIDs can change but this is not being treated here.
- * If the mailbox is recreated (lets say you remove all emails, remove
- * the mail box and create another with same name, UIDs will change)
- * - You can check if UIDs changed in the above example
- * by checking UIDValidity.
- */
- Logger.debug('Querying for new messages on node "EmailReadImap"', { searchCriteria });
- }
-
- try {
- const returnData = await getNewEmails(connection, searchCriteria);
- if (returnData.length) {
- this.emit([returnData]);
- }
- } catch (error) {
- Logger.error('Email Read Imap node encountered an error fetching new emails', {
- error,
- });
- // Wait with resolving till the returnedPromise got resolved, else n8n will be unhappy
- // if it receives an error before the workflow got activated
- returnedPromise.promise().then(() => {
- this.emitError(error as Error);
- });
- }
- }
- },
- };
-
- const tlsOptions: IDataObject = {};
-
- if (options.allowUnauthorizedCerts === true) {
- tlsOptions.rejectUnauthorized = false;
- }
-
- if (credentials.secure) {
- tlsOptions.servername = credentials.host as string;
- }
-
- if (!_.isEmpty(tlsOptions)) {
- config.imap.tlsOptions = tlsOptions;
- }
-
- // Connect to the IMAP server and open the mailbox
- // that we get informed whenever a new email arrives
- return imapConnect(config).then(async (conn) => {
- conn.on('error', async (error) => {
- const errorCode = error.code.toUpperCase();
- if (['ECONNRESET', 'EPIPE'].includes(errorCode)) {
- Logger.verbose(`IMAP connection was reset (${errorCode}) - reconnecting.`, { error });
- try {
- connection = await establishConnection();
- await connection.openBox(mailbox);
- return;
- } catch (e) {
- Logger.error('IMAP reconnect did fail', { error: e });
- // If something goes wrong we want to run emitError
- }
- } else {
- Logger.error('Email Read Imap node encountered a connection error', { error });
- this.emitError(error);
- }
- });
- return conn;
- });
- };
-
- connection = await establishConnection();
-
- await connection.openBox(mailbox);
-
- let reconnectionInterval: NodeJS.Timeout | undefined;
-
- if (options.forceReconnect !== undefined) {
- reconnectionInterval = setInterval(async () => {
- Logger.verbose('Forcing reconnection of IMAP node.');
- await connection.end();
- connection = await establishConnection();
- await connection.openBox(mailbox);
- }, (options.forceReconnect as number) * 1000 * 60);
- }
-
- // When workflow and so node gets set to inactive close the connectoin
- async function closeFunction() {
- if (reconnectionInterval) {
- clearInterval(reconnectionInterval);
- }
- await connection.end();
- }
-
- // Resolve returned-promise so that waiting errors can be emitted
- returnedPromise.resolve();
-
- return {
- closeFunction,
- };
+ super(nodeVersions, baseDescription);
}
}
-
-export async function parseRawEmail(
- this: ITriggerFunctions,
- messageEncoded: ParserSource,
- dataPropertyNameDownload: string,
-): Promise {
- const responseData = await simpleParser(messageEncoded);
- const headers: IDataObject = {};
- for (const header of responseData.headerLines) {
- headers[header.key] = header.line;
- }
-
- // @ts-ignore
- responseData.headers = headers;
- // @ts-ignore
- responseData.headerLines = undefined;
-
- const binaryData: IBinaryKeyData = {};
- if (responseData.attachments) {
- for (let i = 0; i < responseData.attachments.length; i++) {
- const attachment = responseData.attachments[i];
- binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(
- attachment.content,
- attachment.filename,
- attachment.contentType,
- );
- }
- // @ts-ignore
- responseData.attachments = undefined;
- }
-
- return {
- json: responseData as unknown as IDataObject,
- binary: Object.keys(binaryData).length ? binaryData : undefined,
- } as INodeExecutionData;
-}
diff --git a/packages/nodes-base/nodes/EmailReadImap/v1/EmailReadImapV1.node.ts b/packages/nodes-base/nodes/EmailReadImap/v1/EmailReadImapV1.node.ts
new file mode 100644
index 0000000000..bee4de0099
--- /dev/null
+++ b/packages/nodes-base/nodes/EmailReadImap/v1/EmailReadImapV1.node.ts
@@ -0,0 +1,628 @@
+/* eslint-disable n8n-nodes-base/node-filename-against-convention */
+import { ITriggerFunctions } from 'n8n-core';
+import {
+ createDeferredPromise,
+ IBinaryData,
+ IBinaryKeyData,
+ ICredentialDataDecryptedObject,
+ ICredentialsDecrypted,
+ ICredentialTestFunctions,
+ IDataObject,
+ IDeferredPromise,
+ INodeCredentialTestResult,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeBaseDescription,
+ INodeTypeDescription,
+ ITriggerResponse,
+ LoggerProxy as Logger,
+ NodeOperationError,
+} from 'n8n-workflow';
+
+import {
+ connect as imapConnect,
+ getParts,
+ ImapSimple,
+ ImapSimpleOptions,
+ Message,
+} from 'imap-simple';
+import { simpleParser, Source as ParserSource } from 'mailparser';
+
+import _ from 'lodash';
+
+const versionDescription: INodeTypeDescription = {
+ displayName: 'Email Trigger (IMAP)',
+ name: 'emailReadImap',
+ icon: 'fa:inbox',
+ group: ['trigger'],
+ version: 1,
+ description: 'Triggers the workflow when a new email is received',
+ eventTriggerDescription: 'Waiting for you to receive an email',
+ defaults: {
+ name: 'IMAP Email',
+ color: '#44AA22',
+ },
+ // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
+ inputs: [],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'imap',
+ required: true,
+ testedBy: 'imapConnectionTest',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Mailbox Name',
+ name: 'mailbox',
+ type: 'string',
+ default: 'INBOX',
+ },
+ {
+ displayName: 'Action',
+ name: 'postProcessAction',
+ type: 'options',
+ options: [
+ {
+ name: 'Mark as Read',
+ value: 'read',
+ },
+ {
+ name: 'Nothing',
+ value: 'nothing',
+ },
+ ],
+ default: 'read',
+ description:
+ 'What to do after the email has been received. If "nothing" gets selected it will be processed multiple times.',
+ },
+ {
+ displayName: 'Download Attachments',
+ name: 'downloadAttachments',
+ type: 'boolean',
+ default: false,
+ displayOptions: {
+ show: {
+ format: ['simple'],
+ },
+ },
+ description:
+ 'Whether attachments of emails should be downloaded. Only set if needed as it increases processing.',
+ },
+ {
+ displayName: 'Format',
+ name: 'format',
+ type: 'options',
+ options: [
+ {
+ name: 'RAW',
+ value: 'raw',
+ description:
+ 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used',
+ },
+ {
+ name: 'Resolved',
+ value: 'resolved',
+ description:
+ 'Returns the full email with all data resolved and attachments saved as binary data',
+ },
+ {
+ name: 'Simple',
+ value: 'simple',
+ description:
+ 'Returns the full email; do not use if you wish to gather inline attachments',
+ },
+ ],
+ default: 'simple',
+ description: 'The format to return the message in',
+ },
+ {
+ displayName: 'Property Prefix Name',
+ name: 'dataPropertyAttachmentsPrefixName',
+ type: 'string',
+ default: 'attachment_',
+ displayOptions: {
+ show: {
+ format: ['resolved'],
+ },
+ },
+ description:
+ 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
+ },
+ {
+ displayName: 'Property Prefix Name',
+ name: 'dataPropertyAttachmentsPrefixName',
+ type: 'string',
+ default: 'attachment_',
+ displayOptions: {
+ show: {
+ format: ['simple'],
+ downloadAttachments: [true],
+ },
+ },
+ description:
+ 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ displayName: 'Custom Email Rules',
+ name: 'customEmailConfig',
+ type: 'string',
+ default: '["UNSEEN"]',
+ description:
+ 'Custom email fetching rules. See node-imap\'s search function for more details.',
+ },
+ {
+ displayName: 'Ignore SSL Issues',
+ name: 'allowUnauthorizedCerts',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to connect even if SSL certificate validation is not possible',
+ },
+ {
+ displayName: 'Force Reconnect',
+ name: 'forceReconnect',
+ type: 'number',
+ default: 60,
+ description: 'Sets an interval (in minutes) to force a reconnection',
+ },
+ ],
+ },
+ ],
+};
+
+export class EmailReadImapV1 implements INodeType {
+ description: INodeTypeDescription;
+
+ constructor(baseDescription: INodeTypeBaseDescription) {
+ this.description = {
+ ...baseDescription,
+ ...versionDescription,
+ };
+ }
+
+ methods = {
+ credentialTest: {
+ async imapConnectionTest(
+ this: ICredentialTestFunctions,
+ credential: ICredentialsDecrypted,
+ ): Promise {
+ const credentials = credential.data as ICredentialDataDecryptedObject;
+ try {
+ const config: ImapSimpleOptions = {
+ imap: {
+ user: credentials.user as string,
+ password: credentials.password as string,
+ host: credentials.host as string,
+ port: credentials.port as number,
+ tls: credentials.secure as boolean,
+ authTimeout: 20000,
+ },
+ };
+ const tlsOptions: IDataObject = {};
+
+ if (credentials.secure) {
+ tlsOptions.servername = credentials.host as string;
+ }
+ if (!_.isEmpty(tlsOptions)) {
+ config.imap.tlsOptions = tlsOptions;
+ }
+ const conn = imapConnect(config).then(async (conn) => {
+ return conn;
+ });
+ (await conn).getBoxes((err, boxes) => {});
+ } catch (error) {
+ console.log(error);
+ return {
+ status: 'Error',
+ message: error.message,
+ };
+ }
+ return {
+ status: 'OK',
+ message: 'Connection successful!',
+ };
+ },
+ },
+ };
+
+ async trigger(this: ITriggerFunctions): Promise {
+ const credentials = await this.getCredentials('imap');
+
+ const mailbox = this.getNodeParameter('mailbox') as string;
+ const postProcessAction = this.getNodeParameter('postProcessAction') as string;
+ const options = this.getNodeParameter('options', {}) as IDataObject;
+
+ const staticData = this.getWorkflowStaticData('node');
+ Logger.debug('Loaded static data for node "EmailReadImap"', { staticData });
+
+ let connection: ImapSimple;
+
+ // Returns the email text
+ // tslint:disable-next-line:no-any
+ const getText = async (parts: any[], message: Message, subtype: string) => {
+ if (!message.attributes.struct) {
+ return '';
+ }
+
+ const textParts = parts.filter((part) => {
+ return (
+ part.type.toUpperCase() === 'TEXT' && part.subtype.toUpperCase() === subtype.toUpperCase()
+ );
+ });
+
+ if (textParts.length === 0) {
+ return '';
+ }
+
+ try {
+ return await connection.getPartData(message, textParts[0]);
+ } catch {
+ return '';
+ }
+ };
+
+ // Returns the email attachments
+ const getAttachment = async (
+ connection: ImapSimple,
+ // tslint:disable-next-line:no-any
+ parts: any[],
+ message: Message,
+ ): Promise => {
+ if (!message.attributes.struct) {
+ return [];
+ }
+
+ // Check if the message has attachments and if so get them
+ const attachmentParts = parts.filter((part) => {
+ return part.disposition && part.disposition.type.toUpperCase() === 'ATTACHMENT';
+ });
+
+ const attachmentPromises = [];
+ let attachmentPromise;
+ for (const attachmentPart of attachmentParts) {
+ attachmentPromise = connection.getPartData(message, attachmentPart).then((partData) => {
+ // Return it in the format n8n expects
+ return this.helpers.prepareBinaryData(
+ partData,
+ attachmentPart.disposition.params.filename,
+ );
+ });
+
+ attachmentPromises.push(attachmentPromise);
+ }
+
+ return Promise.all(attachmentPromises);
+ };
+
+ // Returns all the new unseen messages
+ const getNewEmails = async (
+ connection: ImapSimple,
+ searchCriteria: Array,
+ ): Promise => {
+ const format = this.getNodeParameter('format', 0) as string;
+
+ let fetchOptions = {};
+
+ if (format === 'simple' || format === 'raw') {
+ fetchOptions = {
+ bodies: ['TEXT', 'HEADER'],
+ markSeen: postProcessAction === 'read',
+ struct: true,
+ };
+ } else if (format === 'resolved') {
+ fetchOptions = {
+ bodies: [''],
+ markSeen: postProcessAction === 'read',
+ struct: true,
+ };
+ }
+
+ const results = await connection.search(searchCriteria, fetchOptions);
+
+ const newEmails: INodeExecutionData[] = [];
+ let newEmail: INodeExecutionData, messageHeader, messageBody;
+ let attachments: IBinaryData[];
+ let propertyName: string;
+
+ // All properties get by default moved to metadata except the ones
+ // which are defined here which get set on the top level.
+ const topLevelProperties = ['cc', 'date', 'from', 'subject', 'to'];
+
+ if (format === 'resolved') {
+ const dataPropertyAttachmentsPrefixName = this.getNodeParameter(
+ 'dataPropertyAttachmentsPrefixName',
+ ) as string;
+
+ for (const message of results) {
+ if (
+ staticData.lastMessageUid !== undefined &&
+ message.attributes.uid <= (staticData.lastMessageUid as number)
+ ) {
+ continue;
+ }
+ if (
+ staticData.lastMessageUid === undefined ||
+ (staticData.lastMessageUid as number) < message.attributes.uid
+ ) {
+ staticData.lastMessageUid = message.attributes.uid;
+ }
+ const part = _.find(message.parts, { which: '' });
+
+ if (part === undefined) {
+ throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
+ }
+ const parsedEmail = await parseRawEmail.call(
+ this,
+ part.body,
+ dataPropertyAttachmentsPrefixName,
+ );
+
+ newEmails.push(parsedEmail);
+ }
+ } else if (format === 'simple') {
+ const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
+
+ let dataPropertyAttachmentsPrefixName = '';
+ if (downloadAttachments === true) {
+ dataPropertyAttachmentsPrefixName = this.getNodeParameter(
+ 'dataPropertyAttachmentsPrefixName',
+ ) as string;
+ }
+
+ for (const message of results) {
+ if (
+ staticData.lastMessageUid !== undefined &&
+ message.attributes.uid <= (staticData.lastMessageUid as number)
+ ) {
+ continue;
+ }
+ if (
+ staticData.lastMessageUid === undefined ||
+ (staticData.lastMessageUid as number) < message.attributes.uid
+ ) {
+ staticData.lastMessageUid = message.attributes.uid;
+ }
+ const parts = getParts(message.attributes.struct!);
+
+ newEmail = {
+ json: {
+ textHtml: await getText(parts, message, 'html'),
+ textPlain: await getText(parts, message, 'plain'),
+ metadata: {} as IDataObject,
+ },
+ };
+
+ messageHeader = message.parts.filter((part) => {
+ return part.which === 'HEADER';
+ });
+
+ messageBody = messageHeader[0].body;
+ for (propertyName of Object.keys(messageBody)) {
+ if (messageBody[propertyName].length) {
+ if (topLevelProperties.includes(propertyName)) {
+ newEmail.json[propertyName] = messageBody[propertyName][0];
+ } else {
+ (newEmail.json.metadata as IDataObject)[propertyName] =
+ messageBody[propertyName][0];
+ }
+ }
+ }
+
+ if (downloadAttachments === true) {
+ // Get attachments and add them if any get found
+ attachments = await getAttachment(connection, parts, message);
+ if (attachments.length) {
+ newEmail.binary = {};
+ for (let i = 0; i < attachments.length; i++) {
+ newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
+ }
+ }
+ }
+
+ newEmails.push(newEmail);
+ }
+ } else if (format === 'raw') {
+ for (const message of results) {
+ if (
+ staticData.lastMessageUid !== undefined &&
+ message.attributes.uid <= (staticData.lastMessageUid as number)
+ ) {
+ continue;
+ }
+ if (
+ staticData.lastMessageUid === undefined ||
+ (staticData.lastMessageUid as number) < message.attributes.uid
+ ) {
+ staticData.lastMessageUid = message.attributes.uid;
+ }
+ const part = _.find(message.parts, { which: 'TEXT' });
+
+ if (part === undefined) {
+ throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
+ }
+ // Return base64 string
+ newEmail = {
+ json: {
+ raw: part.body,
+ },
+ };
+
+ newEmails.push(newEmail);
+ }
+ }
+
+ return newEmails;
+ };
+
+ const returnedPromise: IDeferredPromise | undefined = await createDeferredPromise();
+
+ const establishConnection = (): Promise => {
+ let searchCriteria = ['UNSEEN'] as Array;
+ if (options.customEmailConfig !== undefined) {
+ try {
+ searchCriteria = JSON.parse(options.customEmailConfig as string);
+ } catch (error) {
+ throw new NodeOperationError(this.getNode(), `Custom email config is not valid JSON.`);
+ }
+ }
+
+ const config: ImapSimpleOptions = {
+ imap: {
+ user: credentials.user as string,
+ password: credentials.password as string,
+ host: credentials.host as string,
+ port: credentials.port as number,
+ tls: credentials.secure as boolean,
+ authTimeout: 20000,
+ },
+ onmail: async () => {
+ if (connection) {
+ if (staticData.lastMessageUid !== undefined) {
+ searchCriteria.push(['UID', `${staticData.lastMessageUid as number}:*`]);
+ /**
+ * A short explanation about UIDs and how they work
+ * can be found here: https://dev.to/kehers/imap-new-messages-since-last-check-44gm
+ * TL;DR:
+ * - You cannot filter using ['UID', 'CURRENT ID + 1:*'] because IMAP
+ * won't return correct results if current id + 1 does not yet exist.
+ * - UIDs can change but this is not being treated here.
+ * If the mailbox is recreated (lets say you remove all emails, remove
+ * the mail box and create another with same name, UIDs will change)
+ * - You can check if UIDs changed in the above example
+ * by checking UIDValidity.
+ */
+ Logger.debug('Querying for new messages on node "EmailReadImap"', { searchCriteria });
+ }
+
+ try {
+ const returnData = await getNewEmails(connection, searchCriteria);
+ if (returnData.length) {
+ this.emit([returnData]);
+ }
+ } catch (error) {
+ Logger.error('Email Read Imap node encountered an error fetching new emails', {
+ error,
+ });
+ // Wait with resolving till the returnedPromise got resolved, else n8n will be unhappy
+ // if it receives an error before the workflow got activated
+ returnedPromise.promise().then(() => {
+ this.emitError(error as Error);
+ });
+ }
+ }
+ },
+ };
+
+ const tlsOptions: IDataObject = {};
+
+ if (options.allowUnauthorizedCerts === true) {
+ tlsOptions.rejectUnauthorized = false;
+ }
+
+ if (credentials.secure) {
+ tlsOptions.servername = credentials.host as string;
+ }
+
+ if (!_.isEmpty(tlsOptions)) {
+ config.imap.tlsOptions = tlsOptions;
+ }
+
+ // Connect to the IMAP server and open the mailbox
+ // that we get informed whenever a new email arrives
+ return imapConnect(config).then(async (conn) => {
+ conn.on('error', async (error) => {
+ const errorCode = error.code.toUpperCase();
+ if (['ECONNRESET', 'EPIPE'].includes(errorCode)) {
+ Logger.verbose(`IMAP connection was reset (${errorCode}) - reconnecting.`, { error });
+ try {
+ connection = await establishConnection();
+ await connection.openBox(mailbox);
+ return;
+ } catch (e) {
+ Logger.error('IMAP reconnect did fail', { error: e });
+ // If something goes wrong we want to run emitError
+ }
+ } else {
+ Logger.error('Email Read Imap node encountered a connection error', { error });
+ this.emitError(error);
+ }
+ });
+ return conn;
+ });
+ };
+
+ connection = await establishConnection();
+
+ await connection.openBox(mailbox);
+
+ let reconnectionInterval: NodeJS.Timeout | undefined;
+
+ if (options.forceReconnect !== undefined) {
+ reconnectionInterval = setInterval(async () => {
+ Logger.verbose('Forcing reconnection of IMAP node.');
+ await connection.end();
+ connection = await establishConnection();
+ await connection.openBox(mailbox);
+ }, (options.forceReconnect as number) * 1000 * 60);
+ }
+
+ // When workflow and so node gets set to inactive close the connectoin
+ async function closeFunction() {
+ if (reconnectionInterval) {
+ clearInterval(reconnectionInterval);
+ }
+ await connection.end();
+ }
+
+ // Resolve returned-promise so that waiting errors can be emitted
+ returnedPromise.resolve();
+
+ return {
+ closeFunction,
+ };
+ }
+}
+
+export async function parseRawEmail(
+ this: ITriggerFunctions,
+ messageEncoded: ParserSource,
+ dataPropertyNameDownload: string,
+): Promise {
+ const responseData = await simpleParser(messageEncoded);
+ const headers: IDataObject = {};
+ for (const header of responseData.headerLines) {
+ headers[header.key] = header.line;
+ }
+
+ // @ts-ignore
+ responseData.headers = headers;
+ // @ts-ignore
+ responseData.headerLines = undefined;
+
+ const binaryData: IBinaryKeyData = {};
+ if (responseData.attachments) {
+ for (let i = 0; i < responseData.attachments.length; i++) {
+ const attachment = responseData.attachments[i];
+ binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(
+ attachment.content,
+ attachment.filename,
+ attachment.contentType,
+ );
+ }
+ // @ts-ignore
+ responseData.attachments = undefined;
+ }
+
+ return {
+ json: responseData as unknown as IDataObject,
+ binary: Object.keys(binaryData).length ? binaryData : undefined,
+ } as INodeExecutionData;
+}
diff --git a/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts b/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts
new file mode 100644
index 0000000000..e8ec50d66a
--- /dev/null
+++ b/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts
@@ -0,0 +1,654 @@
+/* eslint-disable n8n-nodes-base/node-filename-against-convention */
+import { ITriggerFunctions } from 'n8n-core';
+import {
+ createDeferredPromise,
+ IBinaryData,
+ IBinaryKeyData,
+ ICredentialsDecrypted,
+ ICredentialTestFunctions,
+ IDataObject,
+ IDeferredPromise,
+ INodeCredentialTestResult,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeBaseDescription,
+ INodeTypeDescription,
+ ITriggerResponse,
+ LoggerProxy as Logger,
+ NodeOperationError,
+} from 'n8n-workflow';
+
+import {
+ connect as imapConnect,
+ getParts,
+ ImapSimple,
+ ImapSimpleOptions,
+ Message,
+} from 'imap-simple';
+import { simpleParser, Source as ParserSource } from 'mailparser';
+
+import _ from 'lodash';
+import { ICredentialsDataImap, isCredentialsDataImap } from '../../../credentials/Imap.credentials';
+
+const versionDescription: INodeTypeDescription = {
+ displayName: 'Email Trigger (IMAP)',
+ name: 'emailReadImap',
+ icon: 'fa:inbox',
+ group: ['trigger'],
+ version: 2,
+ description: 'Triggers the workflow when a new email is received',
+ eventTriggerDescription: 'Waiting for you to receive an email',
+ defaults: {
+ name: 'IMAP Email',
+ color: '#44AA22',
+ },
+ // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
+ inputs: [],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'imap',
+ required: true,
+ testedBy: 'imapConnectionTest',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Mailbox Name',
+ name: 'mailbox',
+ type: 'string',
+ default: 'INBOX',
+ },
+ {
+ displayName: 'Action',
+ name: 'postProcessAction',
+ type: 'options',
+ options: [
+ {
+ name: 'Mark as Read',
+ value: 'read',
+ },
+ {
+ name: 'Nothing',
+ value: 'nothing',
+ },
+ ],
+ default: 'read',
+ description:
+ 'What to do after the email has been received. If "nothing" gets selected it will be processed multiple times.',
+ },
+ {
+ displayName: 'Download Attachments',
+ name: 'downloadAttachments',
+ type: 'boolean',
+ default: false,
+ displayOptions: {
+ show: {
+ format: ['simple'],
+ },
+ },
+ description:
+ 'Whether attachments of emails should be downloaded. Only set if needed as it increases processing.',
+ },
+ {
+ displayName: 'Format',
+ name: 'format',
+ type: 'options',
+ options: [
+ {
+ name: 'RAW',
+ value: 'raw',
+ description:
+ 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used',
+ },
+ {
+ name: 'Resolved',
+ value: 'resolved',
+ description:
+ 'Returns the full email with all data resolved and attachments saved as binary data',
+ },
+ {
+ name: 'Simple',
+ value: 'simple',
+ description:
+ 'Returns the full email; do not use if you wish to gather inline attachments',
+ },
+ ],
+ default: 'simple',
+ description: 'The format to return the message in',
+ },
+ {
+ displayName: 'Property Prefix Name',
+ name: 'dataPropertyAttachmentsPrefixName',
+ type: 'string',
+ default: 'attachment_',
+ displayOptions: {
+ show: {
+ format: ['resolved'],
+ },
+ },
+ description:
+ 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
+ },
+ {
+ displayName: 'Property Prefix Name',
+ name: 'dataPropertyAttachmentsPrefixName',
+ type: 'string',
+ default: 'attachment_',
+ displayOptions: {
+ show: {
+ format: ['simple'],
+ downloadAttachments: [true],
+ },
+ },
+ description:
+ 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ options: [
+ {
+ displayName: 'Custom Email Rules',
+ name: 'customEmailConfig',
+ type: 'string',
+ default: '["UNSEEN"]',
+ description:
+ 'Custom email fetching rules. See node-imap\'s search function for more details.',
+ },
+ {
+ displayName: 'Force Reconnect Every Minutes',
+ name: 'forceReconnect',
+ type: 'number',
+ default: 60,
+ description: 'Sets an interval (in minutes) to force a reconnection',
+ },
+ ],
+ },
+ ],
+};
+
+export class EmailReadImapV2 implements INodeType {
+ description: INodeTypeDescription;
+
+ constructor(baseDescription: INodeTypeBaseDescription) {
+ this.description = {
+ ...baseDescription,
+ ...versionDescription,
+ };
+ }
+
+ methods = {
+ credentialTest: {
+ async imapConnectionTest(
+ this: ICredentialTestFunctions,
+ credential: ICredentialsDecrypted,
+ ): Promise {
+ if (isCredentialsDataImap(credential.data)) {
+ const credentials = credential.data as ICredentialsDataImap;
+ try {
+ const config: ImapSimpleOptions = {
+ imap: {
+ user: credentials.user,
+ password: credentials.password,
+ host: credentials.host,
+ port: credentials.port,
+ tls: credentials.secure,
+ authTimeout: 20000,
+ },
+ };
+ const tlsOptions: IDataObject = {};
+
+ if (credentials.allowUnauthorizedCerts === true) {
+ tlsOptions.rejectUnauthorized = false;
+ }
+
+ if (credentials.secure) {
+ tlsOptions.servername = credentials.host;
+ }
+ if (!_.isEmpty(tlsOptions)) {
+ config.imap.tlsOptions = tlsOptions;
+ }
+ const connection = await imapConnect(config);
+ await connection.getBoxes();
+ connection.end();
+ } catch (error) {
+ return {
+ status: 'Error',
+ message: error.message,
+ };
+ }
+ return {
+ status: 'OK',
+ message: 'Connection successful!',
+ };
+ } else {
+ return {
+ status: 'Error',
+ message: 'Credentials are no IMAP credentials.',
+ };
+ }
+ },
+ },
+ };
+
+ async trigger(this: ITriggerFunctions): Promise {
+ const credentialsObject = await this.getCredentials('imap');
+ const credentials = isCredentialsDataImap(credentialsObject) ? credentialsObject : undefined;
+ if (!credentials) {
+ throw new NodeOperationError(this.getNode(), `Credentials are not valid for imap node.`);
+ }
+ const mailbox = this.getNodeParameter('mailbox') as string;
+ const postProcessAction = this.getNodeParameter('postProcessAction') as string;
+ const options = this.getNodeParameter('options', {}) as IDataObject;
+
+ const staticData = this.getWorkflowStaticData('node');
+ Logger.debug('Loaded static data for node "EmailReadImap"', { staticData });
+
+ let connection: ImapSimple;
+ let closeFunctionWasCalled = false;
+ let isCurrentlyReconnecting = false;
+
+ // Returns the email text
+ // tslint:disable-next-line:no-any
+ const getText = async (parts: any[], message: Message, subtype: string) => {
+ if (!message.attributes.struct) {
+ return '';
+ }
+
+ const textParts = parts.filter((part) => {
+ return (
+ part.type.toUpperCase() === 'TEXT' && part.subtype.toUpperCase() === subtype.toUpperCase()
+ );
+ });
+
+ if (textParts.length === 0) {
+ return '';
+ }
+
+ try {
+ return await connection.getPartData(message, textParts[0]);
+ } catch {
+ return '';
+ }
+ };
+
+ // Returns the email attachments
+ const getAttachment = async (
+ connection: ImapSimple,
+ // tslint:disable-next-line:no-any
+ parts: any[],
+ message: Message,
+ ): Promise => {
+ if (!message.attributes.struct) {
+ return [];
+ }
+
+ // Check if the message has attachments and if so get them
+ const attachmentParts = parts.filter((part) => {
+ return part.disposition && part.disposition.type.toUpperCase() === 'ATTACHMENT';
+ });
+
+ const attachmentPromises = [];
+ let attachmentPromise;
+ for (const attachmentPart of attachmentParts) {
+ attachmentPromise = connection.getPartData(message, attachmentPart).then((partData) => {
+ // Return it in the format n8n expects
+ return this.helpers.prepareBinaryData(
+ partData,
+ attachmentPart.disposition.params.filename,
+ );
+ });
+
+ attachmentPromises.push(attachmentPromise);
+ }
+
+ return Promise.all(attachmentPromises);
+ };
+
+ // Returns all the new unseen messages
+ const getNewEmails = async (
+ connection: ImapSimple,
+ searchCriteria: Array,
+ ): Promise => {
+ const format = this.getNodeParameter('format', 0) as string;
+
+ let fetchOptions = {};
+
+ if (format === 'simple' || format === 'raw') {
+ fetchOptions = {
+ bodies: ['TEXT', 'HEADER'],
+ markSeen: false,
+ struct: true,
+ };
+ } else if (format === 'resolved') {
+ fetchOptions = {
+ bodies: [''],
+ markSeen: false,
+ struct: true,
+ };
+ }
+
+ const results = await connection.search(searchCriteria, fetchOptions);
+
+ const newEmails: INodeExecutionData[] = [];
+ let newEmail: INodeExecutionData, messageHeader, messageBody;
+ let attachments: IBinaryData[];
+ let propertyName: string;
+
+ // All properties get by default moved to metadata except the ones
+ // which are defined here which get set on the top level.
+ const topLevelProperties = ['cc', 'date', 'from', 'subject', 'to'];
+
+ if (format === 'resolved') {
+ const dataPropertyAttachmentsPrefixName = this.getNodeParameter(
+ 'dataPropertyAttachmentsPrefixName',
+ ) as string;
+
+ for (const message of results) {
+ if (
+ staticData.lastMessageUid !== undefined &&
+ message.attributes.uid <= (staticData.lastMessageUid as number)
+ ) {
+ continue;
+ }
+ if (
+ staticData.lastMessageUid === undefined ||
+ (staticData.lastMessageUid as number) < message.attributes.uid
+ ) {
+ staticData.lastMessageUid = message.attributes.uid;
+ }
+ const part = _.find(message.parts, { which: '' });
+
+ if (part === undefined) {
+ throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
+ }
+ const parsedEmail = await parseRawEmail.call(
+ this,
+ part.body,
+ dataPropertyAttachmentsPrefixName,
+ );
+
+ newEmails.push(parsedEmail);
+ }
+ } else if (format === 'simple') {
+ const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
+
+ let dataPropertyAttachmentsPrefixName = '';
+ if (downloadAttachments === true) {
+ dataPropertyAttachmentsPrefixName = this.getNodeParameter(
+ 'dataPropertyAttachmentsPrefixName',
+ ) as string;
+ }
+
+ for (const message of results) {
+ if (
+ staticData.lastMessageUid !== undefined &&
+ message.attributes.uid <= (staticData.lastMessageUid as number)
+ ) {
+ continue;
+ }
+ if (
+ staticData.lastMessageUid === undefined ||
+ (staticData.lastMessageUid as number) < message.attributes.uid
+ ) {
+ staticData.lastMessageUid = message.attributes.uid;
+ }
+ const parts = getParts(message.attributes.struct!);
+
+ newEmail = {
+ json: {
+ textHtml: await getText(parts, message, 'html'),
+ textPlain: await getText(parts, message, 'plain'),
+ metadata: {} as IDataObject,
+ },
+ };
+
+ messageHeader = message.parts.filter((part) => {
+ return part.which === 'HEADER';
+ });
+
+ messageBody = messageHeader[0].body;
+ for (propertyName of Object.keys(messageBody)) {
+ if (messageBody[propertyName].length) {
+ if (topLevelProperties.includes(propertyName)) {
+ newEmail.json[propertyName] = messageBody[propertyName][0];
+ } else {
+ (newEmail.json.metadata as IDataObject)[propertyName] =
+ messageBody[propertyName][0];
+ }
+ }
+ }
+
+ if (downloadAttachments === true) {
+ // Get attachments and add them if any get found
+ attachments = await getAttachment(connection, parts, message);
+ if (attachments.length) {
+ newEmail.binary = {};
+ for (let i = 0; i < attachments.length; i++) {
+ newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
+ }
+ }
+ }
+
+ newEmails.push(newEmail);
+ }
+ } else if (format === 'raw') {
+ for (const message of results) {
+ if (
+ staticData.lastMessageUid !== undefined &&
+ message.attributes.uid <= (staticData.lastMessageUid as number)
+ ) {
+ continue;
+ }
+ if (
+ staticData.lastMessageUid === undefined ||
+ (staticData.lastMessageUid as number) < message.attributes.uid
+ ) {
+ staticData.lastMessageUid = message.attributes.uid;
+ }
+ const part = _.find(message.parts, { which: 'TEXT' });
+
+ if (part === undefined) {
+ throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
+ }
+ // Return base64 string
+ newEmail = {
+ json: {
+ raw: part.body,
+ },
+ };
+
+ newEmails.push(newEmail);
+ }
+ }
+
+ // only mark messages as seen once processing has finished
+ if (postProcessAction === 'read') {
+ const uidList = results.map((e) => e.attributes.uid);
+ if (uidList.length > 0) {
+ connection.addFlags(uidList, '\\SEEN');
+ }
+ }
+ return newEmails;
+ };
+
+ const returnedPromise: IDeferredPromise | undefined = await createDeferredPromise();
+
+ const establishConnection = (): Promise => {
+ let searchCriteria = ['UNSEEN'] as Array;
+ if (options.customEmailConfig !== undefined) {
+ try {
+ searchCriteria = JSON.parse(options.customEmailConfig as string);
+ } catch (error) {
+ throw new NodeOperationError(this.getNode(), `Custom email config is not valid JSON.`);
+ }
+ }
+
+ const config: ImapSimpleOptions = {
+ imap: {
+ user: credentials.user,
+ password: credentials.password,
+ host: credentials.host,
+ port: credentials.port,
+ tls: credentials.secure,
+ authTimeout: 20000,
+ },
+ onmail: async () => {
+ if (connection) {
+ if (staticData.lastMessageUid !== undefined) {
+ searchCriteria.push(['UID', `${staticData.lastMessageUid as number}:*`]);
+ /**
+ * A short explanation about UIDs and how they work
+ * can be found here: https://dev.to/kehers/imap-new-messages-since-last-check-44gm
+ * TL;DR:
+ * - You cannot filter using ['UID', 'CURRENT ID + 1:*'] because IMAP
+ * won't return correct results if current id + 1 does not yet exist.
+ * - UIDs can change but this is not being treated here.
+ * If the mailbox is recreated (lets say you remove all emails, remove
+ * the mail box and create another with same name, UIDs will change)
+ * - You can check if UIDs changed in the above example
+ * by checking UIDValidity.
+ */
+ Logger.debug('Querying for new messages on node "EmailReadImap"', { searchCriteria });
+ }
+
+ try {
+ const returnData = await getNewEmails(connection, searchCriteria);
+ if (returnData.length) {
+ this.emit([returnData]);
+ }
+ } catch (error) {
+ Logger.error('Email Read Imap node encountered an error fetching new emails', {
+ error,
+ });
+ // Wait with resolving till the returnedPromise got resolved, else n8n will be unhappy
+ // if it receives an error before the workflow got activated
+ returnedPromise.promise().then(() => {
+ this.emitError(error as Error);
+ });
+ }
+ }
+ },
+ onupdate: async (seqno: number, info) => {
+ Logger.verbose(`Email Read Imap:update ${seqno}`, info);
+ },
+ };
+
+ const tlsOptions: IDataObject = {};
+
+ if (credentials.allowUnauthorizedCerts === true) {
+ tlsOptions.rejectUnauthorized = false;
+ }
+
+ if (credentials.secure) {
+ tlsOptions.servername = credentials.host as string;
+ }
+
+ if (!_.isEmpty(tlsOptions)) {
+ config.imap.tlsOptions = tlsOptions;
+ }
+
+ // Connect to the IMAP server and open the mailbox
+ // that we get informed whenever a new email arrives
+ return imapConnect(config).then(async (conn) => {
+ conn.on('close', async (hadError: boolean) => {
+ if (isCurrentlyReconnecting === true) {
+ Logger.debug(`Email Read Imap: Connected closed for forced reconnecting`);
+ } else if (closeFunctionWasCalled === true) {
+ Logger.debug(`Email Read Imap: Shutting down workflow - connected closed`);
+ } else {
+ Logger.error(`Email Read Imap: Connected closed unexpectedly`);
+ this.emitError(new Error('Imap connection closed unexpectedly'));
+ }
+ });
+ conn.on('error', async (error) => {
+ const errorCode = error.code.toUpperCase();
+ Logger.verbose(`IMAP connection experienced an error: (${errorCode})`, { error });
+ await closeFunction();
+ this.emitError(error);
+ });
+ return conn;
+ });
+ };
+
+ connection = await establishConnection();
+
+ await connection.openBox(mailbox);
+
+ let reconnectionInterval: NodeJS.Timeout | undefined;
+
+ if (options.forceReconnect !== undefined) {
+ reconnectionInterval = setInterval(async () => {
+ Logger.verbose(`Forcing reconnect to IMAP server`);
+ try {
+ isCurrentlyReconnecting = true;
+ if (connection.closeBox) connection.closeBox(false);
+ connection.end();
+ connection = await establishConnection();
+ await connection.openBox(mailbox);
+ } catch (error) {
+ Logger.error(error);
+ } finally {
+ isCurrentlyReconnecting = false;
+ }
+ }, (options.forceReconnect as number) * 1000 * 60);
+ }
+
+ // When workflow and so node gets set to inactive close the connectoin
+ async function closeFunction() {
+ closeFunctionWasCalled = true;
+ if (reconnectionInterval) {
+ clearInterval(reconnectionInterval);
+ }
+ if (connection.closeBox) connection.closeBox(false);
+ connection.end();
+ }
+
+ // Resolve returned-promise so that waiting errors can be emitted
+ returnedPromise.resolve();
+
+ return {
+ closeFunction,
+ };
+ }
+}
+
+export async function parseRawEmail(
+ this: ITriggerFunctions,
+ messageEncoded: ParserSource,
+ dataPropertyNameDownload: string,
+): Promise {
+ const responseData = await simpleParser(messageEncoded);
+ const headers: IDataObject = {};
+ for (const header of responseData.headerLines) {
+ headers[header.key] = header.line;
+ }
+
+ // @ts-ignore
+ responseData.headers = headers;
+ // @ts-ignore
+ responseData.headerLines = undefined;
+
+ const binaryData: IBinaryKeyData = {};
+ if (responseData.attachments) {
+ for (let i = 0; i < responseData.attachments.length; i++) {
+ const attachment = responseData.attachments[i];
+ binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(
+ attachment.content,
+ attachment.filename,
+ attachment.contentType,
+ );
+ }
+ // @ts-ignore
+ responseData.attachments = undefined;
+ }
+
+ return {
+ json: responseData as unknown as IDataObject,
+ binary: Object.keys(binaryData).length ? binaryData : undefined,
+ } as INodeExecutionData;
+}