diff --git a/packages/nodes-base/nodes/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap.node.ts index 6c9f18bf31..fb90534a7c 100644 --- a/packages/nodes-base/nodes/EmailReadImap.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap.node.ts @@ -24,6 +24,10 @@ import { import * as lodash from 'lodash'; +import { + LoggerProxy as Logger +} from 'n8n-workflow'; + export class EmailReadImap implements INodeType { description: INodeTypeDescription = { displayName: 'EmailReadImap', @@ -158,6 +162,13 @@ export class EmailReadImap implements INodeType { default: false, description: 'Do 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.', + }, ], }, ], @@ -176,16 +187,8 @@ export class EmailReadImap implements INodeType { const postProcessAction = this.getNodeParameter('postProcessAction') as string; const options = this.getNodeParameter('options', {}) as IDataObject; - let searchCriteria = [ - 'UNSEEN', - ]; - 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 staticData = this.getWorkflowStaticData('node'); + Logger.debug('Loaded static data for node "EmailReadImap"', {staticData}); // Returns the email text const getText = async (parts: any[], message: Message, subtype: string) => { // tslint:disable-line:no-any @@ -237,7 +240,7 @@ export class EmailReadImap implements INodeType { // Returns all the new unseen messages - const getNewEmails = async (connection: ImapSimple, searchCriteria: string[]): Promise => { + const getNewEmails = async (connection: ImapSimple, searchCriteria: Array): Promise => { const format = this.getNodeParameter('format', 0) as string; let fetchOptions = {}; @@ -277,6 +280,12 @@ export class EmailReadImap implements INodeType { 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 = lodash.find(message.parts, { which: '' }); if (part === undefined) { @@ -295,6 +304,12 @@ export class EmailReadImap implements INodeType { } 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 = { @@ -335,6 +350,12 @@ export class EmailReadImap implements INodeType { } } 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 = lodash.find(message.parts, { which: 'TEXT' }); if (part === undefined) { @@ -366,6 +387,33 @@ export class EmailReadImap implements INodeType { }, onmail: async () => { if (connection) { + 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.`); + } + } + 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}); + } + const returnData = await getNewEmails(connection, searchCriteria); if (returnData.length) { @@ -386,7 +434,9 @@ export class EmailReadImap implements INodeType { return imapConnect(config).then(async conn => { conn.on('error', async err => { if (err.code.toUpperCase() === 'ECONNRESET') { + Logger.verbose('IMAP connection was reset - reconnecting.'); connection = await establishConnection(); + await connection.openBox(mailbox); } throw err; }); @@ -398,8 +448,22 @@ export class EmailReadImap implements INodeType { 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(); }