2022-10-24 02:06:43 -07:00
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
2023-01-27 03:22:44 -08:00
import type {
2023-03-09 09:13:15 -08:00
ITriggerFunctions ,
2022-10-24 02:06:43 -07:00
IBinaryData ,
IBinaryKeyData ,
ICredentialsDecrypted ,
ICredentialTestFunctions ,
IDataObject ,
INodeCredentialTestResult ,
INodeExecutionData ,
INodeType ,
INodeTypeBaseDescription ,
INodeTypeDescription ,
ITriggerResponse ,
2023-08-07 03:33:06 -07:00
JsonObject ,
2022-10-24 02:06:43 -07:00
} from 'n8n-workflow' ;
2024-08-29 06:55:53 -07:00
import { NodeConnectionType , NodeOperationError } from 'n8n-workflow' ;
2022-10-24 02:06:43 -07:00
2024-04-09 02:33:10 -07:00
import type { ImapSimple , ImapSimpleOptions , Message , MessagePart } from '@n8n/imap' ;
import { connect as imapConnect , getParts } from '@n8n/imap' ;
2023-01-27 03:22:44 -08:00
import type { Source as ParserSource } from 'mailparser' ;
import { simpleParser } from 'mailparser' ;
2023-08-07 03:33:06 -07:00
import rfc2047 from 'rfc2047' ;
2023-06-16 07:26:35 -07:00
import isEmpty from 'lodash/isEmpty' ;
import find from 'lodash/find' ;
2023-08-07 03:33:06 -07:00
2023-01-27 03:22:44 -08:00
import type { ICredentialsDataImap } from '../../../credentials/Imap.credentials' ;
import { isCredentialsDataImap } from '../../../credentials/Imap.credentials' ;
2022-10-24 02:06:43 -07:00
2023-01-13 09:11:56 -08:00
export async function parseRawEmail (
this : ITriggerFunctions ,
messageEncoded : ParserSource ,
dataPropertyNameDownload : string ,
) : Promise < INodeExecutionData > {
const responseData = await simpleParser ( messageEncoded ) ;
const headers : IDataObject = { } ;
2023-08-10 00:58:51 -07:00
const additionalData : IDataObject = { } ;
2023-08-07 03:33:06 -07:00
2023-01-13 09:11:56 -08:00
for ( const header of responseData . headerLines ) {
headers [ header . key ] = header . line ;
}
2023-08-10 00:58:51 -07:00
additionalData . headers = headers ;
additionalData . headerLines = undefined ;
2023-01-13 09:11:56 -08:00
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 ,
) ;
}
2023-08-07 03:33:06 -07:00
2023-08-10 00:58:51 -07:00
additionalData . attachments = undefined ;
2023-01-13 09:11:56 -08:00
}
return {
2023-08-10 00:58:51 -07:00
json : { . . . responseData , . . . additionalData } ,
2023-01-13 09:11:56 -08:00
binary : Object.keys ( binaryData ) . length ? binaryData : undefined ,
} as INodeExecutionData ;
}
2022-10-24 02:06:43 -07:00
const versionDescription : INodeTypeDescription = {
displayName : 'Email Trigger (IMAP)' ,
name : 'emailReadImap' ,
icon : 'fa:inbox' ,
2024-06-27 03:09:43 -07:00
iconColor : 'green' ,
2022-10-24 02:06:43 -07:00
group : [ 'trigger' ] ,
version : 2 ,
description : 'Triggers the workflow when a new email is received' ,
eventTriggerDescription : 'Waiting for you to receive an email' ,
defaults : {
feat(editor): Node creator actions (#4696)
* WIP: Node Actions List UI
* WIP: Recommended Actions and preseting of fields
* WIP: Resource category
* :art: Moved actions categorisation to the server
* :label: Add missing INodeAction type
* :sparkles: Improve SSR categorisation, fix adding of mixed actions
* :recycle: Refactor CategorizedItems to composition api, style fixes
* WIP: Adding multiple nodes
* :recycle: Refactor rest of the NodeCreator component to composition API, conver globalLinkActions to composable
* :sparkles: Allow actions dragging, fix search and refactor passing of actions to categorized items
* :lipstick: Fix node actions title
* Migrate to the pinia store, add posthog feature and various fixes
* :bug: Fix filtering of trigger actions when not merged
* fix: N8N-5439 — Do not use simple node item when at NodeHelperPanel root
* :bug: Design review fixes
* :bug: Fix disabling of merged actions
* Fix trigger root filtering
* :sparkles: Allow for custom node actions parser, introduce hubspot parser
* :bug: Fix initial node params validation, fix position of second added node
* :bug: Introduce operations category, removed canvas node names overrride, fix API actions display and prevent dragging of action nodes
* :sparkles: Prevent NDV auto-open feature flag
* :bug: Inject recommened action for trigger nodes without actions
* Refactored NodeCreatorNode to Storybook, change filtering of merged nodes for the trigger helper panel, minor fixes
* Improve rendering of app nodes and animation
* Cleanup, any only enable accordion transition on triggerhelperpanel
* Hide node creator scrollbars in Firefox
* Minor styles fixes
* Do not copy the array in rendering method
* Removed unused props
* Fix memory leak
* Fix categorisation of regular nodes with a single resource
* Implement telemetry calls for node actions
* Move categorization to FE
* Fix client side actions categorisation
* Skip custom action show
* Only load tooltip for NodeIcon if necessary
* Fix lodash startCase import
* Remove lodash.startcase
* Cleanup
* Fix node creator autofocus on "tab"
* Prevent posthog getFeatureFlag from crashing
* Debugging preview env search issues
* Remove logs
* Make sure the pre-filled params are update not overwritten
* Get rid of transition in itemiterator
* WIP: Rough version of NodeActions keyboard navigation, replace nodeCreator composable with Pinia store module
* Rewrite to add support for ActionItem to ItemIterator and make CategorizedItems accept items props
* Fix category item counter & cleanup
* Add APIHint to actions search no-result, clean up NodeCreatorNode
* Improve node actions no results message
* Remove logging, fix filtering of recommended placeholder category
* Remove unused NodeActions component and node merging feature falg
* Do not show regular nodes without actions
* Make sure to add manual trigger when adding http node via actions hint
* Fixed api hint footer line height
* Prevent pointer-events od NodeIcon img and remove "this" from template
* Address PR points
* Fix e2e specs
* Make sure canvas ia loaded
* Make sure canvas ia loaded before opening nodeCreator in e2e spec
* Fix flaky workflows tags e2e getter
* Imrpove node creator click outside UX, add manual node to regular nodes added from trigger panel
* Add manual trigger node if dragging regular from trigger panel
2022-12-09 01:56:36 -08:00
name : 'Email Trigger (IMAP)' ,
2022-10-24 02:06:43 -07:00
color : '#44AA22' ,
} ,
2023-06-23 03:29:24 -07:00
triggerPanel : {
header : '' ,
executionsHelp : {
inactive :
"<b>While building your workflow</b>, click the 'listen' 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>, <a data-key='activate'>activate</a> 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 'listen' 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, <a data-key='activate'>activate</a> it to have it also listen continuously (you just won’ t see those executions here)." ,
} ,
2022-10-24 02:06:43 -07:00
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs : [ ] ,
2024-08-29 06:55:53 -07:00
outputs : [ NodeConnectionType . Main ] ,
2022-10-24 02:06:43 -07:00
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' ,
2024-07-29 05:27:23 -07:00
placeholder : 'Add option' ,
2022-10-24 02:06:43 -07:00
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' ,
} ,
] ,
} ,
] ,
} ;
export class EmailReadImapV2 implements INodeType {
description : INodeTypeDescription ;
constructor ( baseDescription : INodeTypeBaseDescription ) {
this . description = {
. . . baseDescription ,
. . . versionDescription ,
} ;
}
methods = {
credentialTest : {
async imapConnectionTest (
this : ICredentialTestFunctions ,
credential : ICredentialsDecrypted ,
) : Promise < INodeCredentialTestResult > {
if ( isCredentialsDataImap ( credential . data ) ) {
const credentials = credential . data as ICredentialsDataImap ;
try {
const config : ImapSimpleOptions = {
imap : {
user : credentials.user ,
password : credentials.password ,
2023-08-10 00:58:51 -07:00
host : credentials.host.trim ( ) ,
2022-10-24 02:06:43 -07:00
port : credentials.port ,
tls : credentials.secure ,
authTimeout : 20000 ,
} ,
} ;
const tlsOptions : IDataObject = { } ;
2022-12-02 12:54:28 -08:00
if ( credentials . allowUnauthorizedCerts ) {
2022-10-24 02:06:43 -07:00
tlsOptions . rejectUnauthorized = false ;
}
if ( credentials . secure ) {
2023-08-10 00:58:51 -07:00
tlsOptions . servername = credentials . host . trim ( ) ;
2022-10-24 02:06:43 -07:00
}
2023-02-23 07:16:05 -08:00
if ( ! isEmpty ( tlsOptions ) ) {
2022-10-24 02:06:43 -07:00
config . imap . tlsOptions = tlsOptions ;
}
const connection = await imapConnect ( config ) ;
await connection . getBoxes ( ) ;
connection . end ( ) ;
} catch ( error ) {
return {
status : 'Error' ,
2023-08-07 03:33:06 -07:00
message : ( error as Error ) . message ,
2022-10-24 02:06:43 -07:00
} ;
}
return {
status : 'OK' ,
message : 'Connection successful!' ,
} ;
} else {
return {
status : 'Error' ,
message : 'Credentials are no IMAP credentials.' ,
} ;
}
} ,
} ,
} ;
async trigger ( this : ITriggerFunctions ) : Promise < ITriggerResponse > {
const credentialsObject = await this . getCredentials ( 'imap' ) ;
const credentials = isCredentialsDataImap ( credentialsObject ) ? credentialsObject : undefined ;
if ( ! credentials ) {
2022-12-29 03:20:43 -08:00
throw new NodeOperationError ( this . getNode ( ) , 'Credentials are not valid for imap node.' ) ;
2022-10-24 02:06:43 -07:00
}
const mailbox = this . getNodeParameter ( 'mailbox' ) as string ;
const postProcessAction = this . getNodeParameter ( 'postProcessAction' ) as string ;
const options = this . getNodeParameter ( 'options' , { } ) as IDataObject ;
const staticData = this . getWorkflowStaticData ( 'node' ) ;
2023-03-22 06:04:15 -07:00
this . logger . debug ( 'Loaded static data for node "EmailReadImap"' , { staticData } ) ;
2022-10-24 02:06:43 -07:00
let connection : ImapSimple ;
let closeFunctionWasCalled = false ;
let isCurrentlyReconnecting = false ;
// Returns the email text
2022-12-02 06:25:21 -08:00
2024-05-15 06:50:53 -07:00
const getText = async (
parts : MessagePart [ ] ,
message : Message ,
subtype : string ,
) : Promise < string > = > {
2022-10-24 02:06:43 -07:00
if ( ! message . attributes . struct ) {
return '' ;
}
const textParts = parts . filter ( ( part ) = > {
return (
2024-04-09 02:33:10 -07:00
part . type . toUpperCase ( ) === 'TEXT' && part . subtype . toUpperCase ( ) === subtype . toUpperCase ( )
2022-10-24 02:06:43 -07:00
) ;
} ) ;
2024-05-15 06:50:53 -07:00
const part = textParts [ 0 ] ;
if ( ! part ) {
2022-10-24 02:06:43 -07:00
return '' ;
}
try {
2024-05-15 06:50:53 -07:00
const partData = await connection . getPartData ( message , part ) ;
return partData . toString ( ) ;
2022-10-24 02:06:43 -07:00
} catch {
return '' ;
}
} ;
// Returns the email attachments
const getAttachment = async (
2022-12-02 12:54:28 -08:00
imapConnection : ImapSimple ,
2024-04-09 02:33:10 -07:00
parts : MessagePart [ ] ,
2022-10-24 02:06:43 -07:00
message : Message ,
) : Promise < IBinaryData [ ] > = > {
if ( ! message . attributes . struct ) {
return [ ] ;
}
// Check if the message has attachments and if so get them
2024-04-09 02:33:10 -07:00
const attachmentParts = parts . filter (
( part ) = > part . disposition ? . type ? . toUpperCase ( ) === 'ATTACHMENT' ,
) ;
2022-10-24 02:06:43 -07:00
2023-08-07 03:33:06 -07:00
const decodeFilename = ( filename : string ) = > {
const regex = /=\?([\w-]+)\?Q\?.*\?=/i ;
if ( regex . test ( filename ) ) {
return rfc2047 . decode ( filename ) ;
}
return filename ;
} ;
2022-10-24 02:06:43 -07:00
const attachmentPromises = [ ] ;
let attachmentPromise ;
for ( const attachmentPart of attachmentParts ) {
2022-12-02 12:54:28 -08:00
attachmentPromise = imapConnection
. getPartData ( message , attachmentPart )
. then ( async ( partData ) = > {
2023-08-07 03:33:06 -07:00
// if filename contains utf-8 encoded characters, decode it
const fileName = decodeFilename (
( ( attachmentPart . disposition as IDataObject ) ? . params as IDataObject )
? . filename as string ,
2022-12-02 12:54:28 -08:00
) ;
2023-08-07 03:33:06 -07:00
// Return it in the format n8n expects
2024-05-15 06:50:53 -07:00
return await this . helpers . prepareBinaryData ( partData . buffer , fileName ) ;
2022-12-02 12:54:28 -08:00
} ) ;
2022-10-24 02:06:43 -07:00
attachmentPromises . push ( attachmentPromise ) ;
}
2024-01-17 07:08:50 -08:00
return await Promise . all ( attachmentPromises ) ;
2022-10-24 02:06:43 -07:00
} ;
// Returns all the new unseen messages
const getNewEmails = async (
2022-12-02 12:54:28 -08:00
imapConnection : ImapSimple ,
2022-10-24 02:06:43 -07:00
searchCriteria : Array < string | string [ ] > ,
) : 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 ,
} ;
}
2022-12-02 12:54:28 -08:00
const results = await imapConnection . search ( searchCriteria , fetchOptions ) ;
2022-10-24 02:06:43 -07:00
const newEmails : INodeExecutionData [ ] = [ ] ;
2024-04-09 02:33:10 -07:00
let newEmail : INodeExecutionData ;
2022-10-24 02:06:43 -07:00
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 ;
}
2023-02-23 07:16:05 -08:00
const part = find ( message . parts , { which : '' } ) ;
2022-10-24 02:06:43 -07:00
if ( part === undefined ) {
throw new NodeOperationError ( this . getNode ( ) , 'Email part could not be parsed.' ) ;
}
const parsedEmail = await parseRawEmail . call (
this ,
2023-02-27 19:39:43 -08:00
part . body as Buffer ,
2022-10-24 02:06:43 -07:00
dataPropertyAttachmentsPrefixName ,
) ;
newEmails . push ( parsedEmail ) ;
}
} else if ( format === 'simple' ) {
const downloadAttachments = this . getNodeParameter ( 'downloadAttachments' ) as boolean ;
let dataPropertyAttachmentsPrefixName = '' ;
2022-12-02 12:54:28 -08:00
if ( downloadAttachments ) {
2022-10-24 02:06:43 -07:00
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 ;
}
2024-04-09 02:33:10 -07:00
const parts = getParts ( message . attributes . struct as IDataObject [ ] ) ;
2022-10-24 02:06:43 -07:00
newEmail = {
json : {
textHtml : await getText ( parts , message , 'html' ) ,
textPlain : await getText ( parts , message , 'plain' ) ,
metadata : { } as IDataObject ,
} ,
} ;
2024-04-09 02:33:10 -07:00
const messageHeader = message . parts . filter ( ( part ) = > part . which === 'HEADER' ) ;
2022-10-24 02:06:43 -07:00
2024-04-09 02:33:10 -07:00
const messageBody = messageHeader [ 0 ] . body as Record < string , string [ ] > ;
2023-08-07 03:33:06 -07:00
for ( propertyName of Object . keys ( messageBody ) ) {
2024-04-09 02:33:10 -07:00
if ( messageBody [ propertyName ] . length ) {
2022-10-24 02:06:43 -07:00
if ( topLevelProperties . includes ( propertyName ) ) {
2024-04-09 02:33:10 -07:00
newEmail . json [ propertyName ] = messageBody [ propertyName ] [ 0 ] ;
2022-10-24 02:06:43 -07:00
} else {
2024-04-09 02:33:10 -07:00
( newEmail . json . metadata as IDataObject ) [ propertyName ] =
messageBody [ propertyName ] [ 0 ] ;
2022-10-24 02:06:43 -07:00
}
}
}
2022-12-02 12:54:28 -08:00
if ( downloadAttachments ) {
2022-10-24 02:06:43 -07:00
// Get attachments and add them if any get found
2022-12-02 12:54:28 -08:00
attachments = await getAttachment ( imapConnection , parts , message ) ;
2022-10-24 02:06:43 -07:00
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 ;
}
2023-02-23 07:16:05 -08:00
const part = find ( message . parts , { which : 'TEXT' } ) ;
2022-10-24 02:06:43 -07:00
if ( part === undefined ) {
throw new NodeOperationError ( this . getNode ( ) , 'Email part could not be parsed.' ) ;
}
// Return base64 string
newEmail = {
json : {
2023-08-07 03:33:06 -07:00
raw : part.body as string ,
2022-10-24 02:06:43 -07:00
} ,
} ;
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 ) {
2022-12-02 12:54:28 -08:00
await imapConnection . addFlags ( uidList , '\\SEEN' ) ;
2022-10-24 02:06:43 -07:00
}
}
return newEmails ;
} ;
2023-03-22 06:04:15 -07:00
const returnedPromise = await this . helpers . createDeferredPromise ( ) ;
2022-10-24 02:06:43 -07:00
2022-12-02 12:54:28 -08:00
const establishConnection = async ( ) : Promise < ImapSimple > = > {
2022-10-24 02:06:43 -07:00
let searchCriteria = [ 'UNSEEN' ] as Array < string | string [ ] > ;
if ( options . customEmailConfig !== undefined ) {
try {
2023-08-07 03:33:06 -07:00
searchCriteria = JSON . parse ( options . customEmailConfig as string ) as Array <
string | string [ ]
> ;
2022-10-24 02:06:43 -07:00
} catch ( error ) {
2022-12-29 03:20:43 -08:00
throw new NodeOperationError ( this . getNode ( ) , 'Custom email config is not valid JSON.' ) ;
2022-10-24 02:06:43 -07:00
}
}
const config : ImapSimpleOptions = {
imap : {
user : credentials.user ,
password : credentials.password ,
2023-08-10 00:58:51 -07:00
host : credentials.host.trim ( ) ,
2022-10-24 02:06:43 -07:00
port : credentials.port ,
tls : credentials.secure ,
authTimeout : 20000 ,
} ,
2024-04-09 02:33:10 -07:00
onMail : async ( ) = > {
2022-10-24 02:06:43 -07:00
if ( connection ) {
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 .
* /
2023-03-22 06:04:15 -07:00
this . logger . debug ( 'Querying for new messages on node "EmailReadImap"' , {
searchCriteria ,
} ) ;
2022-10-24 02:06:43 -07:00
}
try {
const returnData = await getNewEmails ( connection , searchCriteria ) ;
if ( returnData . length ) {
this . emit ( [ returnData ] ) ;
}
} catch ( error ) {
2023-03-22 06:04:15 -07:00
this . logger . error ( 'Email Read Imap node encountered an error fetching new emails' , {
2023-08-07 03:33:06 -07:00
error : error as Error ,
2022-10-24 02:06:43 -07:00
} ) ;
// Wait with resolving till the returnedPromise got resolved, else n8n will be unhappy
// if it receives an error before the workflow got activated
2022-12-02 12:54:28 -08:00
await returnedPromise . promise ( ) . then ( ( ) = > {
2022-10-24 02:06:43 -07:00
this . emitError ( error as Error ) ;
} ) ;
}
}
} ,
2024-04-09 02:33:10 -07:00
onUpdate : async ( seqNo : number , info ) = > {
2024-08-28 00:32:53 -07:00
this . logger . debug ( ` Email Read Imap:update ${ seqNo } ` , info ) ;
2022-10-24 02:06:43 -07:00
} ,
} ;
const tlsOptions : IDataObject = { } ;
2022-12-02 12:54:28 -08:00
if ( credentials . allowUnauthorizedCerts ) {
2022-10-24 02:06:43 -07:00
tlsOptions . rejectUnauthorized = false ;
}
if ( credentials . secure ) {
2023-08-10 00:58:51 -07:00
tlsOptions . servername = credentials . host . trim ( ) ;
2022-10-24 02:06:43 -07:00
}
2023-02-23 07:16:05 -08:00
if ( ! isEmpty ( tlsOptions ) ) {
2022-10-24 02:06:43 -07:00
config . imap . tlsOptions = tlsOptions ;
}
// Connect to the IMAP server and open the mailbox
// that we get informed whenever a new email arrives
2024-01-17 07:08:50 -08:00
return await imapConnect ( config ) . then ( async ( conn ) = > {
2022-11-08 06:28:21 -08:00
conn . on ( 'close' , async ( _hadError : boolean ) = > {
2022-12-02 12:54:28 -08:00
if ( isCurrentlyReconnecting ) {
2023-03-22 06:04:15 -07:00
this . logger . debug ( 'Email Read Imap: Connected closed for forced reconnecting' ) ;
2022-12-02 12:54:28 -08:00
} else if ( closeFunctionWasCalled ) {
2023-03-22 06:04:15 -07:00
this . logger . debug ( 'Email Read Imap: Shutting down workflow - connected closed' ) ;
2022-10-24 02:06:43 -07:00
} else {
2023-03-22 06:04:15 -07:00
this . logger . error ( 'Email Read Imap: Connected closed unexpectedly' ) ;
2022-10-24 02:06:43 -07:00
this . emitError ( new Error ( 'Imap connection closed unexpectedly' ) ) ;
}
} ) ;
conn . on ( 'error' , async ( error ) = > {
2023-08-07 03:33:06 -07:00
const errorCode = ( ( error as JsonObject ) . code as string ) . toUpperCase ( ) ;
2024-08-28 00:32:53 -07:00
this . logger . debug ( ` IMAP connection experienced an error: ( ${ errorCode } ) ` , {
2023-08-07 03:33:06 -07:00
error : error as Error ,
} ) ;
2023-02-27 19:39:43 -08:00
this . emitError ( error as Error ) ;
2022-10-24 02:06:43 -07:00
} ) ;
return conn ;
} ) ;
} ;
connection = await establishConnection ( ) ;
await connection . openBox ( mailbox ) ;
let reconnectionInterval : NodeJS.Timeout | undefined ;
2023-08-10 00:58:51 -07:00
const handleReconnect = async ( ) = > {
2024-08-28 00:32:53 -07:00
this . logger . debug ( 'Forcing reconnect to IMAP server' ) ;
2023-08-07 03:33:06 -07:00
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 as string ) ;
} finally {
isCurrentlyReconnecting = false ;
}
} ;
2022-10-24 02:06:43 -07:00
if ( options . forceReconnect !== undefined ) {
2023-07-28 09:28:17 -07:00
reconnectionInterval = setInterval (
2023-08-10 00:58:51 -07:00
handleReconnect ,
2023-07-28 09:28:17 -07:00
( options . forceReconnect as number ) * 1000 * 60 ,
) ;
2022-10-24 02:06:43 -07:00
}
2023-08-10 00:58:51 -07:00
// When workflow and so node gets set to inactive close the connection
2022-10-24 02:06:43 -07:00
async function closeFunction() {
closeFunctionWasCalled = true ;
if ( reconnectionInterval ) {
clearInterval ( reconnectionInterval ) ;
}
2022-12-02 12:54:28 -08:00
if ( connection . closeBox ) await connection . closeBox ( false ) ;
2022-10-24 02:06:43 -07:00
connection . end ( ) ;
}
// Resolve returned-promise so that waiting errors can be emitted
returnedPromise . resolve ( ) ;
return {
closeFunction ,
} ;
}
}