UNPKG

n8n-nodes-base

Version:

Base nodes of n8n

462 lines 20.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmailReadImapV2 = void 0; const Imap_credentials_1 = require("../../../credentials/Imap.credentials"); const imap_1 = require("@n8n/imap"); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const luxon_1 = require("luxon"); const n8n_workflow_1 = require("n8n-workflow"); const rfc2047_1 = __importDefault(require("rfc2047")); const utils_1 = require("./utils"); const versionDescription = { displayName: 'Email Trigger (IMAP)', name: 'emailReadImap', icon: 'fa:inbox', iconColor: 'green', group: ['trigger'], version: [2, 2.1], description: 'Triggers the workflow when a new email is received', eventTriggerDescription: 'Waiting for you to receive an email', defaults: { name: 'Email Trigger (IMAP)', color: '#44AA22', }, triggerPanel: { header: '', executionsHelp: { inactive: "<b>While building your workflow</b>, click the 'execute step' button, then send an email to make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /><b>Once you're happy with your workflow</b>, publish it. Then every time an email is received, the workflow will execute. These executions will show up in the <a data-key='executions'>executions list</a>, but not in the editor.", active: "<b>While building your workflow</b>, click the 'execute step' button, then send an email to make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /><b>Your workflow will also execute automatically</b>, since it's activated. Every time an email is received, this node will trigger an execution. These executions will show up in the <a data-key='executions'>executions list</a>, but not in the editor.", }, activationHint: 'Once you’ve finished building your workflow, publish it to have it also listen continuously (you just won’t see those executions here).', }, inputs: [], outputs: [n8n_workflow_1.NodeConnectionTypes.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 <a href="https://github.com/mscdex/node-imap">node-imap</a>\'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', }, { displayName: 'Fetch Only New Emails', name: 'trackLastMessageId', type: 'boolean', default: true, description: 'Whether to fetch only new emails since the last run, or all emails that match the "Custom Email Rules" (["UNSEEN"] by default)', displayOptions: { show: { '@version': [{ _cnd: { gte: 2.1 } }], }, }, }, ], }, ], }; class EmailReadImapV2 { description; constructor(baseDescription) { this.description = { ...baseDescription, ...versionDescription, }; } methods = { credentialTest: { async imapConnectionTest(credential) { if ((0, Imap_credentials_1.isCredentialsDataImap)(credential.data)) { const credentials = credential.data; try { const config = { imap: { user: credentials.user, password: credentials.password, host: credentials.host.trim(), port: credentials.port, tls: credentials.secure, authTimeout: 20000, }, }; const tlsOptions = {}; if (credentials.allowUnauthorizedCerts) { tlsOptions.rejectUnauthorized = false; } if (credentials.secure) { tlsOptions.servername = credentials.host.trim(); } if (!(0, isEmpty_1.default)(tlsOptions)) { config.imap.tlsOptions = tlsOptions; } const connection = await (0, imap_1.connect)(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() { const node = this.getNode(); const credentialsObject = await this.getCredentials('imap'); const credentials = (0, Imap_credentials_1.isCredentialsDataImap)(credentialsObject) ? credentialsObject : undefined; if (!credentials) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Credentials are not valid for imap node.'); } const mailbox = this.getNodeParameter('mailbox'); const postProcessAction = this.getNodeParameter('postProcessAction'); const options = this.getNodeParameter('options', {}); const activatedAt = luxon_1.DateTime.now(); const staticData = this.getWorkflowStaticData('node'); if (node.typeVersion <= 2) { // before v 2.1 staticData.lastMessageUid was never set, preserve that behavior staticData.lastMessageUid = undefined; } if (options.trackLastMessageId === false) { staticData.lastMessageUid = undefined; } this.logger.debug('Loaded static data for node "EmailReadImap"', { staticData }); let connection; let closeFunctionWasCalled = false; let isCurrentlyReconnecting = false; // Returns the email text const getText = async (parts, message, subtype) => { if (!message.attributes.struct) { return ''; } const textParts = parts.filter((part) => { return (part.type.toUpperCase() === 'TEXT' && part.subtype.toUpperCase() === subtype.toUpperCase()); }); const part = textParts[0]; if (!part) { return ''; } try { const partData = await connection.getPartData(message, part); return partData.toString(); } catch { return ''; } }; // Returns the email attachments const getAttachment = async (imapConnection, parts, message) => { if (!message.attributes.struct) { return []; } // Check if the message has attachments and if so get them const attachmentParts = parts.filter((part) => part.disposition?.type?.toUpperCase() === 'ATTACHMENT'); const decodeFilename = (filename) => { const regex = /=\?([\w-]+)\?Q\?.*\?=/i; if (regex.test(filename)) { return rfc2047_1.default.decode(filename); } return filename; }; const attachmentPromises = []; let attachmentPromise; for (const attachmentPart of attachmentParts) { attachmentPromise = imapConnection .getPartData(message, attachmentPart) .then(async (partData) => { // if filename contains utf-8 encoded characters, decode it const fileName = decodeFilename(attachmentPart.disposition?.params ?.filename); // Return it in the format n8n expects return await this.helpers.prepareBinaryData(partData.buffer, fileName); }); attachmentPromises.push(attachmentPromise); } return await Promise.all(attachmentPromises); }; const returnedPromise = this.helpers.createDeferredPromise(); const establishConnection = async () => { let searchCriteria = ['UNSEEN']; if (options.customEmailConfig !== undefined) { try { searchCriteria = JSON.parse(options.customEmailConfig); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Custom email config is not valid JSON.'); } } const config = { imap: { user: credentials.user, password: credentials.password, host: credentials.host.trim(), port: credentials.port, tls: credentials.secure, authTimeout: 20000, }, onMail: async (numEmails) => { this.logger.debug('New emails received in node "EmailReadImap"', { numEmails, }); if (connection) { /** * Only process new emails: * - If we've seen emails before (lastMessageUid is set), fetch messages higher UID. * - Otherwise, fetch emails received since the workflow activation date. * * Note: IMAP 'SINCE' only filters by date (not time), * so it may include emails from earlier on the activation day. */ if (staticData.lastMessageUid !== undefined) { /** * 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. */ searchCriteria.push(['UID', `${staticData.lastMessageUid}:*`]); } else if (node.typeVersion > 2 && options.trackLastMessageId !== false) { searchCriteria.push(['SINCE', activatedAt.toFormat('dd-LLL-yyyy')]); } this.logger.debug('Querying for new messages on node "EmailReadImap"', { searchCriteria, }); try { await utils_1.getNewEmails.call(this, { imapConnection: connection, searchCriteria, postProcessAction, getText, getAttachment, onEmailBatch: async (returnData) => { if (returnData.length) { this.emit([returnData]); } }, }); } catch (error) { this.logger.error('Email Read Imap node encountered an error fetching new emails', { error: error, }); // Wait with resolving till the returnedPromise got resolved, else n8n will be unhappy // if it receives an error before the workflow got activated await returnedPromise.promise.then(() => { this.emitError(error); }); } } }, onUpdate: (seqNo, info) => { this.logger.debug(`Email Read Imap:update ${seqNo}`, info); }, }; const tlsOptions = {}; if (credentials.allowUnauthorizedCerts) { tlsOptions.rejectUnauthorized = false; } if (credentials.secure) { tlsOptions.servername = credentials.host.trim(); } if (!(0, isEmpty_1.default)(tlsOptions)) { config.imap.tlsOptions = tlsOptions; } // Connect to the IMAP server and open the mailbox // that we get informed whenever a new email arrives return await (0, imap_1.connect)(config).then((conn) => { conn.on('close', (_hadError) => { if (isCurrentlyReconnecting) { this.logger.debug('Email Read Imap: Connected closed for forced reconnecting'); } else if (closeFunctionWasCalled) { this.logger.debug('Email Read Imap: Shutting down workflow - connected closed'); } else { this.logger.error('Email Read Imap: Connected closed unexpectedly'); this.emitError(new Error('Imap connection closed unexpectedly')); } }); conn.on('error', (error) => { const errorCode = error.code.toUpperCase(); this.logger.debug(`IMAP connection experienced an error: (${errorCode})`, { error: error, }); this.emitError(error); }); return conn; }); }; connection = await establishConnection(); await connection.openBox(mailbox); let reconnectionInterval; const handleReconnect = async () => { this.logger.debug('Forcing reconnect to IMAP server'); try { isCurrentlyReconnecting = true; if (connection.closeBox) await connection.closeBox(false); connection.end(); connection = await establishConnection(); await connection.openBox(mailbox); } catch (error) { this.logger.error(error); } finally { isCurrentlyReconnecting = false; } }; if (options.forceReconnect !== undefined) { reconnectionInterval = setInterval(handleReconnect, options.forceReconnect * 1000 * 60); } // When workflow and so node gets set to inactive close the connection const closeFunction = async () => { closeFunctionWasCalled = true; if (reconnectionInterval) { clearInterval(reconnectionInterval); } try { if (connection.closeBox) await connection.closeBox(false); connection.end(); } catch (error) { throw new n8n_workflow_1.TriggerCloseError(this.getNode(), { cause: error, level: 'warning' }); } }; // Resolve returned-promise so that waiting errors can be emitted returnedPromise.resolve(); return { closeFunction, }; } } exports.EmailReadImapV2 = EmailReadImapV2; //# sourceMappingURL=EmailReadImapV2.node.js.map