diff --git a/packages/nodes-base/nodes/EmailReadImap.node.ts b/packages/nodes-base/nodes/EmailReadImap.node.ts index aa976970c48..0703d23a1d5 100644 --- a/packages/nodes-base/nodes/EmailReadImap.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap.node.ts @@ -6,9 +6,16 @@ import { INodeType, INodeTypeDescription, ITriggerResponse, + IBinaryKeyData, } from 'n8n-workflow'; import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple'; +import { + simpleParser, + Source as ParserSource, +} from 'mailparser'; + +import * as lodash from 'lodash'; export class EmailReadImap implements INodeType { description: INodeTypeDescription = { @@ -59,8 +66,39 @@ export class EmailReadImap implements INodeType { name: 'downloadAttachments', type: 'boolean', default: false, + displayOptions: { + show: { + format: [ + 'simple' + ], + }, + }, description: 'If 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', @@ -68,6 +106,23 @@ export class EmailReadImap implements INodeType { 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 ], @@ -105,7 +160,6 @@ export class EmailReadImap implements INodeType { const mailbox = this.getNodeParameter('mailbox') as string; const postProcessAction = this.getNodeParameter('postProcessAction') as string; - const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean; const options = this.getNodeParameter('options', {}) as IDataObject; @@ -156,16 +210,26 @@ export class EmailReadImap implements INodeType { // Returns all the new unseen messages const getNewEmails = async (connection: ImapSimple): Promise => { - + const format = this.getNodeParameter('format', 0) as string; const searchCriteria = [ 'UNSEEN' ]; - const fetchOptions = { - bodies: ['HEADER', 'TEXT'], - markSeen: postProcessAction === 'read', - struct: true, - }; + 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); @@ -174,10 +238,7 @@ export class EmailReadImap implements INodeType { let attachments: IBinaryData[]; let propertyName: string; - let dataPropertyAttachmentsPrefixName = ''; - if (downloadAttachments === true) { - dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; - } + // All properties get by default moved to metadata except the ones // which are defined here which get set on the top level. @@ -188,45 +249,83 @@ export class EmailReadImap implements INodeType { 'subject', 'to', ]; - for (const message of results) { - const parts = getParts(message.attributes.struct!); + if (format === 'resolved') { + const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; - newEmail = { - json: { - textHtml: await getText(parts, message, 'html'), - textPlain: await getText(parts, message, 'plain'), - metadata: {} as IDataObject, + for (const message of results) { + const part = lodash.find(message.parts, {which: ''}); + + if (part === undefined) { + throw new Error('Email part could not be parsed.'); } - }; + const parsedEmail = await parseRawEmail.call(this, part.body, dataPropertyAttachmentsPrefixName); - 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]; - } - } + newEmails.push(parsedEmail); } + } else if (format === 'simple') { + const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean; + let dataPropertyAttachmentsPrefixName = ''; 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]; - } - } + dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; } - newEmails.push(newEmail); + for (const message of results) { + 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) { + const part = lodash.find(message.parts, {which: 'TEXT'}); + + if (part === undefined) { + throw new Error('Email part could not be parsed.'); + } + // Return base64 string + newEmail = { + json: { + raw: part.body + } + }; + + newEmails.push(newEmail); + } } return newEmails; @@ -277,3 +376,32 @@ export class EmailReadImap implements INodeType { } } + +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; +}