-
Notifications
You must be signed in to change notification settings - Fork 13.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
64 changes: 64 additions & 0 deletions
64
packages/nodes-base/nodes/EmailReadImap/test/v2/EmailReadImapV2.node.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { mock } from 'jest-mock-extended'; | ||
import { INodeTypeBaseDescription, ITriggerFunctions } from 'n8n-workflow'; | ||
import { ICredentialsDataImap } from '../../../../credentials/Imap.credentials'; | ||
import { EmailReadImapV2 } from '../../v2/EmailReadImapV2.node'; | ||
|
||
jest.mock('@n8n/imap', () => { | ||
const originalModule = jest.requireActual('@n8n/imap'); | ||
|
||
return { | ||
...originalModule, | ||
connect: jest.fn().mockImplementation(() => ({ | ||
then: jest.fn().mockImplementation(() => ({ | ||
openBox: jest.fn().mockResolvedValue({}), | ||
})), | ||
})), | ||
}; | ||
}); | ||
|
||
describe('Test IMap V2', () => { | ||
const triggerFunctions = mock<ITriggerFunctions>({ | ||
helpers: { | ||
createDeferredPromise: jest.fn().mockImplementation(() => { | ||
let resolve, reject; | ||
const promise = new Promise((res, rej) => { | ||
resolve = res; | ||
reject = rej; | ||
}); | ||
return { promise, resolve, reject }; | ||
}), | ||
}, | ||
}); | ||
|
||
const credentials: ICredentialsDataImap = { | ||
host: 'imap.gmail.com', | ||
port: 993, | ||
user: 'user', | ||
password: 'password', | ||
secure: false, | ||
allowUnauthorizedCerts: false, | ||
}; | ||
|
||
triggerFunctions.getCredentials.calledWith('imap').mockResolvedValue(credentials); | ||
triggerFunctions.logger.debug = jest.fn(); | ||
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({ | ||
name: 'Mark as Read', | ||
value: 'read', | ||
}); | ||
|
||
const baseDescription: INodeTypeBaseDescription = { | ||
displayName: 'EmailReadImapV2', | ||
name: 'emailReadImapV2', | ||
icon: 'file:removeDuplicates.svg', | ||
group: ['transform'], | ||
description: 'Delete items with matching field values', | ||
}; | ||
|
||
afterEach(() => jest.resetAllMocks()); | ||
|
||
it('should run return a close function on success', async () => { | ||
const result = await new EmailReadImapV2(baseDescription).trigger.call(triggerFunctions); | ||
|
||
expect(result.closeFunction).toBeDefined(); | ||
}); | ||
}); |
90 changes: 90 additions & 0 deletions
90
packages/nodes-base/nodes/EmailReadImap/test/v2/utils.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { IDataObject, ITriggerFunctions } from 'n8n-workflow'; | ||
import { mock } from 'jest-mock-extended'; | ||
import { ImapSimple } from '@n8n/imap'; | ||
import { returnJsonArray } from 'n8n-core'; | ||
import { getNewEmails } from '../../v2/utils'; | ||
|
||
describe('Test IMap V2 utils', () => { | ||
afterEach(() => jest.resetAllMocks()); | ||
|
||
describe('getNewEmails', () => { | ||
const triggerFunctions = mock<ITriggerFunctions>({ | ||
helpers: { returnJsonArray }, | ||
}); | ||
|
||
const message = { | ||
attributes: { | ||
uuid: 1, | ||
struct: {}, | ||
}, | ||
parts: [ | ||
{ which: '', body: 'Body content' }, | ||
{ which: 'HEADER', body: 'h' }, | ||
{ which: 'TEXT', body: 'txt' }, | ||
], | ||
}; | ||
|
||
const staticData: IDataObject = {}; | ||
const imapConnection = mock<ImapSimple>({ | ||
search: jest.fn().mockReturnValue(Promise.resolve([message])), | ||
}); | ||
const getText = jest.fn().mockReturnValue('text'); | ||
const getAttachment = jest.fn().mockReturnValue(['attachment']); | ||
|
||
it('should return new emails', async () => { | ||
const expectedResults = [ | ||
{ | ||
format: 'resolved', | ||
expected: { | ||
json: { | ||
attachments: undefined, | ||
headers: { '': 'Body content' }, | ||
headerLines: undefined, | ||
html: false, | ||
}, | ||
binary: undefined, | ||
}, | ||
}, | ||
{ | ||
format: 'simple', | ||
expected: { | ||
json: { | ||
textHtml: 'text', | ||
textPlain: 'text', | ||
metadata: { | ||
'0': 'h', | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
format: 'raw', | ||
expected: { | ||
json: { raw: 'txt' }, | ||
}, | ||
}, | ||
]; | ||
|
||
expectedResults.forEach(async (expectedResult) => { | ||
triggerFunctions.getNodeParameter | ||
.calledWith('format') | ||
.mockReturnValue(expectedResult.format); | ||
triggerFunctions.getNodeParameter | ||
.calledWith('dataPropertyAttachmentsPrefixName') | ||
.mockReturnValue('resolved'); | ||
|
||
const result = getNewEmails.call( | ||
triggerFunctions, | ||
imapConnection, | ||
[], | ||
staticData, | ||
'', | ||
getText, | ||
getAttachment, | ||
); | ||
|
||
expect(result).resolves.toEqual([expectedResult.expected]); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import { getParts, ImapSimple, Message, MessagePart } from '@n8n/imap'; | ||
|
||
import { IBinaryData, IDataObject, ITriggerFunctions, NodeOperationError } from 'n8n-workflow'; | ||
import { find } from 'lodash'; | ||
import { INodeExecutionData } from 'n8n-workflow'; | ||
import { parseRawEmail } from './EmailReadImapV2.node'; | ||
|
||
export async function getNewEmails( | ||
this: ITriggerFunctions, | ||
imapConnection: ImapSimple, | ||
searchCriteria: Array<string | string[]>, | ||
staticData: IDataObject, | ||
postProcessAction: string, | ||
getText: (parts: MessagePart[], message: Message, subtype: string) => Promise<string>, | ||
getAttachment: ( | ||
imapConnection: ImapSimple, | ||
parts: MessagePart[], | ||
message: Message, | ||
) => Promise<IBinaryData[]>, | ||
): Promise<INodeExecutionData[]> { | ||
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 imapConnection.search(searchCriteria, fetchOptions); | ||
|
||
const newEmails: INodeExecutionData[] = []; | ||
let newEmail: INodeExecutionData; | ||
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 as Buffer, | ||
dataPropertyAttachmentsPrefixName, | ||
); | ||
|
||
newEmails.push(parsedEmail); | ||
} | ||
} else if (format === 'simple') { | ||
const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean; | ||
|
||
let dataPropertyAttachmentsPrefixName = ''; | ||
if (downloadAttachments) { | ||
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 as IDataObject[]); | ||
|
||
newEmail = { | ||
json: { | ||
textHtml: await getText(parts, message, 'html'), | ||
textPlain: await getText(parts, message, 'plain'), | ||
metadata: {} as IDataObject, | ||
}, | ||
}; | ||
|
||
const messageHeader = message.parts.filter((part) => part.which === 'HEADER'); | ||
|
||
const messageBody = messageHeader[0].body as Record<string, string[]>; | ||
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) { | ||
// Get attachments and add them if any get found | ||
attachments = await getAttachment(imapConnection, 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 as string, | ||
}, | ||
}; | ||
|
||
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) { | ||
await imapConnection.addFlags(uidList, '\\SEEN'); | ||
} | ||
} | ||
return newEmails; | ||
} |