Add Force node reconnect option to IMAP trigger node (#1792)

* Adding reconnect feature to IMAP nodes

* Fixing clear interval and persisting last UID in static data
This commit is contained in:
Omar Ajoue 2021-05-16 02:33:15 +02:00 committed by GitHub
parent 0877b0779d
commit 3e98612881
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -24,6 +24,10 @@ import {
import * as lodash from 'lodash'; import * as lodash from 'lodash';
import {
LoggerProxy as Logger
} from 'n8n-workflow';
export class EmailReadImap implements INodeType { export class EmailReadImap implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'EmailReadImap', displayName: 'EmailReadImap',
@ -158,6 +162,13 @@ export class EmailReadImap implements INodeType {
default: false, default: false,
description: 'Do connect even if SSL certificate validation is not possible.', 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 postProcessAction = this.getNodeParameter('postProcessAction') as string;
const options = this.getNodeParameter('options', {}) as IDataObject; const options = this.getNodeParameter('options', {}) as IDataObject;
let searchCriteria = [ const staticData = this.getWorkflowStaticData('node');
'UNSEEN', Logger.debug('Loaded static data for node "EmailReadImap"', {staticData});
];
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.`);
}
}
// Returns the email text // Returns the email text
const getText = async (parts: any[], message: Message, subtype: string) => { // tslint:disable-line:no-any 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 // Returns all the new unseen messages
const getNewEmails = async (connection: ImapSimple, searchCriteria: string[]): Promise<INodeExecutionData[]> => { const getNewEmails = async (connection: ImapSimple, searchCriteria: Array<string | string[]>): Promise<INodeExecutionData[]> => {
const format = this.getNodeParameter('format', 0) as string; const format = this.getNodeParameter('format', 0) as string;
let fetchOptions = {}; let fetchOptions = {};
@ -277,6 +280,12 @@ export class EmailReadImap implements INodeType {
const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
for (const message of results) { 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: '' }); const part = lodash.find(message.parts, { which: '' });
if (part === undefined) { if (part === undefined) {
@ -295,6 +304,12 @@ export class EmailReadImap implements INodeType {
} }
for (const message of results) { 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!); const parts = getParts(message.attributes.struct!);
newEmail = { newEmail = {
@ -335,6 +350,12 @@ export class EmailReadImap implements INodeType {
} }
} else if (format === 'raw') { } else if (format === 'raw') {
for (const message of results) { 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' }); const part = lodash.find(message.parts, { which: 'TEXT' });
if (part === undefined) { if (part === undefined) {
@ -366,6 +387,33 @@ export class EmailReadImap implements INodeType {
}, },
onmail: async () => { onmail: async () => {
if (connection) { if (connection) {
let searchCriteria = [
'UNSEEN',
] as Array<string | string[]>;
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); const returnData = await getNewEmails(connection, searchCriteria);
if (returnData.length) { if (returnData.length) {
@ -386,7 +434,9 @@ export class EmailReadImap implements INodeType {
return imapConnect(config).then(async conn => { return imapConnect(config).then(async conn => {
conn.on('error', async err => { conn.on('error', async err => {
if (err.code.toUpperCase() === 'ECONNRESET') { if (err.code.toUpperCase() === 'ECONNRESET') {
Logger.verbose('IMAP connection was reset - reconnecting.');
connection = await establishConnection(); connection = await establishConnection();
await connection.openBox(mailbox);
} }
throw err; throw err;
}); });
@ -398,8 +448,22 @@ export class EmailReadImap implements INodeType {
await connection.openBox(mailbox); 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 // When workflow and so node gets set to inactive close the connectoin
async function closeFunction() { async function closeFunction() {
if (reconnectionInterval) {
clearInterval(reconnectionInterval);
}
await connection.end(); await connection.end();
} }