2019-06-23 03:35:23 -07:00
import { ITriggerFunctions } from 'n8n-core' ;
import {
IBinaryData ,
2020-08-25 08:57:45 -07:00
IBinaryKeyData ,
2019-06-23 03:35:23 -07:00
IDataObject ,
INodeExecutionData ,
INodeType ,
INodeTypeDescription ,
ITriggerResponse ,
2021-04-16 09:33:36 -07:00
NodeOperationError ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
2020-10-01 05:01:39 -07:00
import {
connect as imapConnect ,
getParts ,
ImapSimple ,
ImapSimpleOptions ,
Message ,
} from 'imap-simple' ;
2020-08-25 08:57:17 -07:00
import {
simpleParser ,
Source as ParserSource ,
} from 'mailparser' ;
import * as lodash from 'lodash' ;
2019-06-23 03:35:23 -07:00
2021-05-15 17:33:15 -07:00
import {
LoggerProxy as Logger
} from 'n8n-workflow' ;
2019-06-23 03:35:23 -07:00
export class EmailReadImap implements INodeType {
description : INodeTypeDescription = {
displayName : 'EmailReadImap' ,
name : 'emailReadImap' ,
icon : 'fa:inbox' ,
group : [ 'trigger' ] ,
version : 1 ,
description : 'Triggers the workflow when a new email gets received' ,
defaults : {
name : 'IMAP Email' ,
color : '#44AA22' ,
} ,
inputs : [ ] ,
2020-09-17 23:28:32 -07:00
outputs : [ 'main' ] ,
2019-06-23 03:35:23 -07:00
credentials : [
{
name : 'imap' ,
required : true ,
2020-10-22 06:46:03 -07:00
} ,
2019-06-23 03:35:23 -07:00
] ,
properties : [
{
displayName : 'Mailbox Name' ,
name : 'mailbox' ,
type : 'string' ,
default : 'INBOX' ,
} ,
{
displayName : 'Action' ,
name : 'postProcessAction' ,
type : 'options' ,
options : [
{
name : 'Mark as read' ,
2020-08-25 08:57:45 -07:00
value : 'read' ,
2019-06-23 03:35:23 -07:00
} ,
{
name : 'Nothing' ,
2020-08-25 08:57:45 -07:00
value : 'nothing' ,
2019-06-23 03:35:23 -07:00
} ,
] ,
default : 'read' ,
description : 'What to do after the email has been received. If "nothing" gets<br />selected it will be processed multiple times.' ,
} ,
{
displayName : 'Download Attachments' ,
name : 'downloadAttachments' ,
type : 'boolean' ,
default : false ,
2020-08-25 08:57:17 -07:00
displayOptions : {
show : {
format : [
2020-08-25 08:57:45 -07:00
'simple' ,
2020-08-25 08:57:17 -07:00
] ,
} ,
} ,
2019-06-23 03:35:23 -07:00
description : 'If attachments of emails should be downloaded.<br />Only set if needed as it increases processing.' ,
} ,
2020-08-25 08:57:17 -07:00
{
displayName : 'Format' ,
name : 'format' ,
type : 'options' ,
options : [
{
name : 'RAW' ,
value : 'raw' ,
2020-10-22 06:46:03 -07:00
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.' ,
2020-08-25 08:57:17 -07:00
} ,
{
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' ,
} ,
2019-06-23 03:35:23 -07:00
{
displayName : 'Property Prefix Name' ,
name : 'dataPropertyAttachmentsPrefixName' ,
type : 'string' ,
default : 'attachment_' ,
displayOptions : {
show : {
2020-08-25 08:57:17 -07:00
format : [
2020-08-25 08:57:45 -07:00
'resolved' ,
2020-08-25 08:57:17 -07:00
] ,
} ,
} ,
description : 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />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 : [
2020-08-25 08:57:45 -07:00
'simple' ,
2020-08-25 08:57:17 -07:00
] ,
2019-06-23 03:35:23 -07:00
downloadAttachments : [
2020-08-25 08:57:45 -07:00
true ,
2019-06-23 03:35:23 -07:00
] ,
} ,
} ,
description : 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"' ,
} ,
2019-10-25 12:38:54 -07:00
{
displayName : 'Options' ,
name : 'options' ,
type : 'collection' ,
placeholder : 'Add Option' ,
default : { } ,
options : [
2020-09-17 23:28:32 -07:00
{
displayName : 'Custom email rules' ,
name : 'customEmailConfig' ,
type : 'string' ,
default : '["UNSEEN"]' ,
2020-10-22 06:46:03 -07:00
description : 'Custom email fetching rules. See <a href="https://github.com/mscdex/node-imap">node-imap</a>\'s search function for more details' ,
2020-09-17 23:28:32 -07:00
} ,
2019-10-25 12:38:54 -07:00
{
displayName : 'Ignore SSL Issues' ,
name : 'allowUnauthorizedCerts' ,
type : 'boolean' ,
default : false ,
description : 'Do connect even if SSL certificate validation is not possible.' ,
} ,
2021-05-15 17:33:15 -07:00
{
displayName : 'Force reconnect' ,
name : 'forceReconnect' ,
type : 'number' ,
default : 60 ,
description : 'Sets an interval (in minutes) to force a reconnection.' ,
} ,
2019-10-25 12:38:54 -07:00
] ,
} ,
2019-06-23 03:35:23 -07:00
] ,
} ;
async trigger ( this : ITriggerFunctions ) : Promise < ITriggerResponse > {
const credentials = this . getCredentials ( 'imap' ) ;
if ( credentials === undefined ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , 'No credentials got returned!' ) ;
2019-06-23 03:35:23 -07:00
}
const mailbox = this . getNodeParameter ( 'mailbox' ) as string ;
const postProcessAction = this . getNodeParameter ( 'postProcessAction' ) as string ;
2019-10-25 12:38:54 -07:00
const options = this . getNodeParameter ( 'options' , { } ) as IDataObject ;
2019-06-23 03:35:23 -07:00
2021-05-15 17:33:15 -07:00
const staticData = this . getWorkflowStaticData ( 'node' ) ;
Logger . debug ( 'Loaded static data for node "EmailReadImap"' , { staticData } ) ;
2019-06-23 03:35:23 -07:00
// Returns the email text
const getText = async ( parts : any [ ] , message : Message , subtype : string ) = > { // tslint:disable-line:no-any
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 '' ;
}
2020-09-17 23:27:44 -07:00
try {
return await connection . getPartData ( message , textParts [ 0 ] ) ;
} catch {
return '' ;
}
2019-06-23 03:35:23 -07:00
} ;
// Returns the email attachments
const getAttachment = async ( connection : ImapSimple , parts : any [ ] , message : Message ) : Promise < IBinaryData [ ] > = > { // tslint:disable-line:no-any
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 ) {
attachmentPromise = connection . getPartData ( message , attachmentPart )
. then ( ( partData ) = > {
// Return it in the format n8n expects
return this . helpers . prepareBinaryData ( partData , attachmentPart . disposition . params . filename ) ;
} ) ;
attachmentPromises . push ( attachmentPromise ) ;
}
return Promise . all ( attachmentPromises ) ;
} ;
// Returns all the new unseen messages
2021-05-15 17:33:15 -07:00
const getNewEmails = async ( connection : ImapSimple , searchCriteria : Array < string | string [ ] > ) : Promise < INodeExecutionData [ ] > = > {
2020-08-25 08:57:17 -07:00
const format = this . getNodeParameter ( 'format' , 0 ) as string ;
2020-09-17 23:27:44 -07:00
2020-08-25 08:57:17 -07:00
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 ,
} ;
}
2019-06-23 03:35:23 -07:00
const results = await connection . search ( searchCriteria , fetchOptions ) ;
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' ,
] ;
2020-08-25 08:57:17 -07:00
if ( format === 'resolved' ) {
const dataPropertyAttachmentsPrefixName = this . getNodeParameter ( 'dataPropertyAttachmentsPrefixName' ) as string ;
2019-06-23 03:35:23 -07:00
2020-08-25 08:57:17 -07:00
for ( const message of results ) {
2021-05-15 17:33:15 -07:00
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 ;
}
2020-08-25 08:57:45 -07:00
const part = lodash . find ( message . parts , { which : '' } ) ;
2019-06-23 03:35:23 -07:00
2020-08-25 08:57:17 -07:00
if ( part === undefined ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , 'Email part could not be parsed.' ) ;
2019-06-23 03:35:23 -07:00
}
2020-08-25 08:57:17 -07:00
const parsedEmail = await parseRawEmail . call ( this , part . body , dataPropertyAttachmentsPrefixName ) ;
newEmails . push ( parsedEmail ) ;
2019-06-23 03:35:23 -07:00
}
2020-08-25 08:57:17 -07:00
} else if ( format === 'simple' ) {
const downloadAttachments = this . getNodeParameter ( 'downloadAttachments' ) as boolean ;
2019-06-23 03:35:23 -07:00
2020-08-25 08:57:17 -07:00
let dataPropertyAttachmentsPrefixName = '' ;
2019-06-23 03:35:23 -07:00
if ( downloadAttachments === true ) {
2020-08-25 08:57:17 -07:00
dataPropertyAttachmentsPrefixName = this . getNodeParameter ( 'dataPropertyAttachmentsPrefixName' ) as string ;
}
for ( const message of results ) {
2021-05-15 17:33:15 -07:00
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 ;
}
2020-08-25 08:57:17 -07:00
const parts = getParts ( message . attributes . struct ! ) ;
newEmail = {
json : {
textHtml : await getText ( parts , message , 'html' ) ,
textPlain : await getText ( parts , message , 'plain' ) ,
metadata : { } as IDataObject ,
2020-10-22 06:46:03 -07:00
} ,
2020-08-25 08:57:17 -07:00
} ;
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 ] ;
}
2019-06-23 03:35:23 -07:00
}
}
2020-08-25 08:57:17 -07:00
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 ) ;
2019-06-23 03:35:23 -07:00
}
2020-08-25 08:57:17 -07:00
} else if ( format === 'raw' ) {
for ( const message of results ) {
2021-05-15 17:33:15 -07:00
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 ;
}
2020-08-25 08:57:45 -07:00
const part = lodash . find ( message . parts , { which : 'TEXT' } ) ;
2020-08-25 08:57:17 -07:00
if ( part === undefined ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , 'Email part could not be parsed.' ) ;
2020-08-25 08:57:17 -07:00
}
// Return base64 string
newEmail = {
json : {
2020-10-22 06:46:03 -07:00
raw : part.body ,
} ,
2020-08-25 08:57:17 -07:00
} ;
2019-06-23 03:35:23 -07:00
2020-08-25 08:57:17 -07:00
newEmails . push ( newEmail ) ;
}
2019-06-23 03:35:23 -07:00
}
return newEmails ;
} ;
2021-04-17 08:01:27 -07:00
const establishConnection = ( ) : Promise < ImapSimple > = > {
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 ) {
2021-05-15 17:33:15 -07:00
let searchCriteria = [
'UNSEEN' ,
] as Array < string | string [ ] > ;
if ( options . customEmailConfig !== undefined ) {
try {
searchCriteria = JSON . parse ( options . customEmailConfig as string ) ;
} catch ( error ) {
throw new NodeOperationError ( this . getNode ( ) , ` Custom email config is not valid JSON. ` ) ;
}
}
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 } ) ;
}
2021-04-17 08:01:27 -07:00
const returnData = await getNewEmails ( connection , searchCriteria ) ;
2019-06-23 03:35:23 -07:00
2021-04-17 08:01:27 -07:00
if ( returnData . length ) {
this . emit ( [ returnData ] ) ;
}
}
} ,
} ;
if ( options . allowUnauthorizedCerts === true ) {
config . imap . tlsOptions = {
rejectUnauthorized : false ,
} ;
}
// 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 err = > {
if ( err . code . toUpperCase ( ) === 'ECONNRESET' ) {
2021-05-15 17:33:15 -07:00
Logger . verbose ( 'IMAP connection was reset - reconnecting.' ) ;
2021-04-17 08:01:27 -07:00
connection = await establishConnection ( ) ;
2021-05-15 17:33:15 -07:00
await connection . openBox ( mailbox ) ;
2021-04-17 08:01:27 -07:00
}
throw err ;
} ) ;
return conn ;
} ) ;
2019-06-23 03:35:23 -07:00
} ;
2021-04-17 08:01:27 -07:00
let connection : ImapSimple = await establishConnection ( ) ;
2019-10-25 12:38:54 -07:00
2020-09-17 23:28:32 -07:00
await connection . openBox ( mailbox ) ;
2019-06-23 03:35:23 -07:00
2021-05-15 17:33:15 -07:00
let reconnectionInterval : NodeJS.Timeout | undefined ;
if ( options . forceReconnect !== undefined ) {
reconnectionInterval = setInterval ( async ( ) = > {
Logger . verbose ( 'Forcing reconnection of IMAP node.' ) ;
await connection . end ( ) ;
connection = await establishConnection ( ) ;
await connection . openBox ( mailbox ) ;
} , options . forceReconnect as number * 1000 * 60 ) ;
}
2019-06-23 03:35:23 -07:00
// When workflow and so node gets set to inactive close the connectoin
async function closeFunction() {
2021-05-15 17:33:15 -07:00
if ( reconnectionInterval ) {
clearInterval ( reconnectionInterval ) ;
}
2019-06-23 03:35:23 -07:00
await connection . end ( ) ;
}
return {
closeFunction ,
} ;
}
}
2020-08-25 08:57:17 -07: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 ;
}