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 { ITriggerFunctions } from 'n8n-core' ;
import type {
2022-10-24 02:06:43 -07:00
IBinaryData ,
IBinaryKeyData ,
ICredentialDataDecryptedObject ,
ICredentialsDecrypted ,
ICredentialTestFunctions ,
IDataObject ,
IDeferredPromise ,
INodeCredentialTestResult ,
INodeExecutionData ,
INodeType ,
INodeTypeBaseDescription ,
INodeTypeDescription ,
ITriggerResponse ,
} from 'n8n-workflow' ;
2023-01-27 03:22:44 -08:00
import { createDeferredPromise , LoggerProxy as Logger , NodeOperationError } from 'n8n-workflow' ;
2022-10-24 02:06:43 -07:00
2023-01-27 03:22:44 -08:00
import type { ImapSimple , ImapSimpleOptions , Message } from 'imap-simple' ;
import { connect as imapConnect , getParts } from 'imap-simple' ;
import type { Source as ParserSource } from 'mailparser' ;
import { simpleParser } from 'mailparser' ;
2022-10-24 02:06:43 -07:00
2023-02-23 07:16:05 -08:00
import isEmpty from 'lodash.isempty' ;
import find from 'lodash.find' ;
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 = { } ;
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 ;
}
2022-10-24 02:06:43 -07:00
const versionDescription : INodeTypeDescription = {
displayName : 'Email Trigger (IMAP)' ,
name : 'emailReadImap' ,
icon : 'fa:inbox' ,
group : [ 'trigger' ] ,
version : 1 ,
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' ,
} ,
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs : [ ] ,
outputs : [ '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 : 'Ignore SSL Issues' ,
name : 'allowUnauthorizedCerts' ,
type : 'boolean' ,
default : false ,
description : 'Whether to 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' ,
} ,
] ,
} ,
] ,
} ;
export class EmailReadImapV1 implements INodeType {
description : INodeTypeDescription ;
constructor ( baseDescription : INodeTypeBaseDescription ) {
this . description = {
. . . baseDescription ,
. . . versionDescription ,
} ;
}
methods = {
credentialTest : {
async imapConnectionTest (
this : ICredentialTestFunctions ,
credential : ICredentialsDecrypted ,
) : Promise < INodeCredentialTestResult > {
const credentials = credential . data as ICredentialDataDecryptedObject ;
try {
const config : ImapSimpleOptions = {
imap : {
user : credentials.user as string ,
password : credentials.password as string ,
host : credentials.host as string ,
port : credentials.port as number ,
tls : credentials.secure as boolean ,
authTimeout : 20000 ,
} ,
} ;
const tlsOptions : IDataObject = { } ;
if ( credentials . secure ) {
tlsOptions . servername = credentials . host as string ;
}
2023-02-23 07:16:05 -08:00
if ( ! isEmpty ( tlsOptions ) ) {
2022-10-24 02:06:43 -07:00
config . imap . tlsOptions = tlsOptions ;
}
2022-12-02 12:54:28 -08:00
const conn = imapConnect ( config ) . then ( async ( entry ) = > {
return entry ;
2022-10-24 02:06:43 -07:00
} ) ;
2022-11-08 06:28:21 -08:00
( await conn ) . getBoxes ( ( _err , _boxes ) = > { } ) ;
2022-10-24 02:06:43 -07:00
} catch ( error ) {
console . log ( error ) ;
return {
status : 'Error' ,
message : error.message ,
} ;
}
return {
status : 'OK' ,
message : 'Connection successful!' ,
} ;
} ,
} ,
} ;
async trigger ( this : ITriggerFunctions ) : Promise < ITriggerResponse > {
const credentials = await this . getCredentials ( 'imap' ) ;
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' ) ;
Logger . debug ( 'Loaded static data for node "EmailReadImap"' , { staticData } ) ;
let connection : ImapSimple ;
// Returns the email text
2022-12-02 06:25:21 -08:00
2022-10-24 02:06:43 -07:00
const getText = async ( parts : any [ ] , message : Message , subtype : string ) = > {
if ( ! message . attributes . struct ) {
return '' ;
}
const textParts = parts . filter ( ( part ) = > {
return (
part . type . toUpperCase ( ) === 'TEXT' && part . subtype . toUpperCase ( ) === subtype . toUpperCase ( )
) ;
} ) ;
if ( textParts . length === 0 ) {
return '' ;
}
try {
return await connection . getPartData ( message , textParts [ 0 ] ) ;
} catch {
return '' ;
}
} ;
// Returns the email attachments
const getAttachment = async (
2022-12-02 12:54:28 -08:00
imapConnection : ImapSimple ,
2022-10-24 02:06:43 -07:00
parts : any [ ] ,
message : Message ,
) : Promise < IBinaryData [ ] > = > {
if ( ! message . attributes . struct ) {
return [ ] ;
}
// Check if the message has attachments and if so get them
const attachmentParts = parts . filter ( ( part ) = > {
return part . disposition && part . disposition . type . toUpperCase ( ) === 'ATTACHMENT' ;
} ) ;
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 ) = > {
// Return it in the format n8n expects
return this . helpers . prepareBinaryData (
2023-02-27 19:39:43 -08:00
partData as Buffer ,
attachmentPart . disposition . params . filename as string ,
2022-12-02 12:54:28 -08:00
) ;
} ) ;
2022-10-24 02:06:43 -07:00
attachmentPromises . push ( attachmentPromise ) ;
}
return Promise . all ( attachmentPromises ) ;
} ;
// 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' ] ,
2022-10-25 03:53:06 -07:00
markSeen : false ,
2022-10-24 02:06:43 -07:00
struct : true ,
} ;
} else if ( format === 'resolved' ) {
fetchOptions = {
bodies : [ '' ] ,
2022-10-25 03:53:06 -07:00
markSeen : false ,
2022-10-24 02:06:43 -07:00
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 [ ] = [ ] ;
let newEmail : INodeExecutionData , messageHeader , messageBody ;
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 ;
}
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 ;
2023-02-27 19:39:43 -08:00
for ( propertyName of Object . keys ( messageBody as IDataObject ) ) {
2022-10-24 02:06:43 -07:00
if ( messageBody [ propertyName ] . length ) {
if ( topLevelProperties . includes ( propertyName ) ) {
newEmail . json [ propertyName ] = messageBody [ propertyName ] [ 0 ] ;
} else {
( newEmail . json . metadata as IDataObject ) [ propertyName ] =
messageBody [ propertyName ] [ 0 ] ;
}
}
}
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 : {
raw : part.body ,
} ,
} ;
newEmails . push ( newEmail ) ;
}
}
2022-10-25 03:53:06 -07:00
// 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-25 03:53:06 -07:00
}
}
2022-10-24 02:06:43 -07:00
return newEmails ;
} ;
2022-12-02 12:54:28 -08:00
const returnedPromise : IDeferredPromise < void > | undefined = await 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 {
searchCriteria = JSON . parse ( options . customEmailConfig as string ) ;
} 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 as string ,
password : credentials.password as string ,
host : credentials.host as string ,
port : credentials.port as number ,
tls : credentials.secure as boolean ,
authTimeout : 20000 ,
} ,
onmail : async ( ) = > {
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 .
* /
Logger . debug ( 'Querying for new messages on node "EmailReadImap"' , { searchCriteria } ) ;
}
try {
const returnData = await getNewEmails ( connection , searchCriteria ) ;
if ( returnData . length ) {
this . emit ( [ returnData ] ) ;
}
} catch ( error ) {
Logger . error ( 'Email Read Imap node encountered an error fetching new emails' , {
error ,
} ) ;
// 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 ) ;
} ) ;
}
}
} ,
} ;
const tlsOptions : IDataObject = { } ;
if ( options . allowUnauthorizedCerts === true ) {
tlsOptions . rejectUnauthorized = false ;
}
if ( credentials . secure ) {
tlsOptions . servername = credentials . host as string ;
}
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
return imapConnect ( config ) . then ( async ( conn ) = > {
conn . on ( 'error' , async ( error ) = > {
const errorCode = error . code . toUpperCase ( ) ;
2023-02-27 19:39:43 -08:00
if ( [ 'ECONNRESET' , 'EPIPE' ] . includes ( errorCode as string ) ) {
2022-10-24 02:06:43 -07:00
Logger . verbose ( ` IMAP connection was reset ( ${ errorCode } ) - reconnecting. ` , { error } ) ;
try {
connection = await establishConnection ( ) ;
await connection . openBox ( mailbox ) ;
return ;
} catch ( e ) {
Logger . error ( 'IMAP reconnect did fail' , { error : e } ) ;
// If something goes wrong we want to run emitError
}
} else {
Logger . error ( 'Email Read Imap node encountered a connection error' , { 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 ;
if ( options . forceReconnect !== undefined ) {
reconnectionInterval = setInterval ( async ( ) = > {
Logger . verbose ( 'Forcing reconnection of IMAP node.' ) ;
2022-12-02 12:54:28 -08:00
connection . end ( ) ;
2022-10-24 02:06:43 -07:00
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
async function closeFunction() {
if ( reconnectionInterval ) {
clearInterval ( reconnectionInterval ) ;
}
2022-12-02 12:54:28 -08:00
connection . end ( ) ;
2022-10-24 02:06:43 -07:00
}
// Resolve returned-promise so that waiting errors can be emitted
returnedPromise . resolve ( ) ;
return {
closeFunction ,
} ;
}
}