2023-01-27 03:22:44 -08:00
import type {
2023-03-09 09:13:15 -08:00
IPollFunctions ,
2022-09-08 05:44:34 -07:00
IDataObject ,
2023-01-18 01:47:26 -08:00
ILoadOptionsFunctions ,
2022-09-08 05:44:34 -07:00
INodeExecutionData ,
2023-01-18 01:47:26 -08:00
INodePropertyOptions ,
2022-09-08 05:44:34 -07:00
INodeType ,
INodeTypeDescription ,
} from 'n8n-workflow' ;
2024-08-29 06:55:53 -07:00
import { NodeConnectionType } from 'n8n-workflow' ;
2022-09-08 05:44:34 -07:00
import { DateTime } from 'luxon' ;
2023-01-18 01:47:26 -08:00
import {
googleApiRequest ,
googleApiRequestAllItems ,
parseRawEmail ,
prepareQuery ,
simplifyOutput ,
} from './GenericFunctions' ;
2022-09-08 05:44:34 -07:00
export class GmailTrigger implements INodeType {
description : INodeTypeDescription = {
displayName : 'Gmail Trigger' ,
name : 'gmailTrigger' ,
icon : 'file:gmail.svg' ,
group : [ 'trigger' ] ,
2024-11-06 05:28:26 -08:00
version : [ 1 , 1.1 , 1.2 ] ,
2022-09-08 05:44:34 -07:00
description :
'Fetches emails from Gmail and starts the workflow on specified polling intervals.' ,
subtitle : '={{"Gmail Trigger"}}' ,
defaults : {
name : 'Gmail Trigger' ,
} ,
credentials : [
{
name : 'googleApi' ,
required : true ,
displayOptions : {
show : {
authentication : [ 'serviceAccount' ] ,
} ,
} ,
} ,
{
name : 'gmailOAuth2' ,
required : true ,
displayOptions : {
show : {
authentication : [ 'oAuth2' ] ,
} ,
} ,
} ,
] ,
polling : true ,
inputs : [ ] ,
2024-08-29 06:55:53 -07:00
outputs : [ NodeConnectionType . Main ] ,
2022-09-08 05:44:34 -07:00
properties : [
{
displayName : 'Authentication' ,
name : 'authentication' ,
type : 'options' ,
options : [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name : 'OAuth2 (recommended)' ,
value : 'oAuth2' ,
} ,
{
name : 'Service Account' ,
value : 'serviceAccount' ,
} ,
] ,
default : 'oAuth2' ,
} ,
{
displayName : 'Event' ,
name : 'event' ,
type : 'options' ,
default : 'messageReceived' ,
options : [
{
name : 'Message Received' ,
value : 'messageReceived' ,
} ,
] ,
} ,
{
displayName : 'Simplify' ,
name : 'simple' ,
type : 'boolean' ,
default : true ,
description :
'Whether to return a simplified version of the response instead of the raw data' ,
} ,
{
displayName : 'Filters' ,
name : 'filters' ,
type : 'collection' ,
placeholder : 'Add Filter' ,
default : { } ,
options : [
{
displayName : 'Include Spam and Trash' ,
name : 'includeSpamTrash' ,
type : 'boolean' ,
default : false ,
description : 'Whether to include messages from SPAM and TRASH in the results' ,
} ,
2024-11-06 05:28:26 -08:00
{
displayName : 'Include Drafts' ,
name : 'includeDrafts' ,
type : 'boolean' ,
default : false ,
description : 'Whether to include email drafts in the results' ,
} ,
2022-09-08 05:44:34 -07:00
{
displayName : 'Label Names or IDs' ,
name : 'labelIds' ,
type : 'multiOptions' ,
typeOptions : {
loadOptionsMethod : 'getLabels' ,
} ,
default : [ ] ,
description :
2024-09-12 07:53:36 -07:00
'Only return messages with labels that match all of the specified label IDs. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.' ,
2022-09-08 05:44:34 -07:00
} ,
{
displayName : 'Search' ,
name : 'q' ,
type : 'string' ,
default : '' ,
placeholder : 'has:attachment' ,
hint : 'Use the same format as in the Gmail search box. <a href="https://support.google.com/mail/answer/7190?hl=en">More info</a>.' ,
description : 'Only return messages matching the specified query' ,
} ,
{
displayName : 'Read Status' ,
name : 'readStatus' ,
type : 'options' ,
default : 'unread' ,
hint : 'Filter emails by whether they have been read or not' ,
options : [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name : 'Unread and read emails' ,
value : 'both' ,
} ,
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name : 'Unread emails only' ,
value : 'unread' ,
} ,
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name : 'Read emails only' ,
value : 'read' ,
} ,
] ,
} ,
{
displayName : 'Sender' ,
name : 'sender' ,
type : 'string' ,
default : '' ,
description : 'Sender name or email to filter by' ,
hint : 'Enter an email or part of a sender name' ,
} ,
] ,
} ,
{
displayName : 'Options' ,
name : 'options' ,
type : 'collection' ,
2024-07-29 05:27:23 -07:00
placeholder : 'Add option' ,
2022-09-08 05:44:34 -07:00
default : { } ,
displayOptions : {
hide : {
simple : [ true ] ,
} ,
} ,
options : [
{
displayName : 'Attachment Prefix' ,
name : 'dataPropertyAttachmentsPrefixName' ,
type : 'string' ,
default : 'attachment_' ,
description :
"Prefix for name of the binary property to which to write the attachment. An index starting with 0 will be added. So if name is 'attachment_' the first attachment is saved to 'attachment_0'." ,
} ,
{
displayName : 'Download Attachments' ,
name : 'downloadAttachments' ,
type : 'boolean' ,
default : false ,
2023-10-30 03:30:53 -07:00
description : "Whether the email's attachments will be downloaded" ,
2022-09-08 05:44:34 -07:00
} ,
] ,
} ,
] ,
} ;
2023-01-18 01:47:26 -08:00
methods = {
loadOptions : {
2023-04-19 07:00:49 -07:00
// Get all the labels to display them to user so that they can
2023-01-18 01:47:26 -08:00
// select them easily
async getLabels ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
const labels = await googleApiRequestAllItems . call (
this ,
'labels' ,
'GET' ,
'/gmail/v1/users/me/labels' ,
) ;
for ( const label of labels ) {
returnData . push ( {
name : label.name ,
value : label.id ,
} ) ;
}
return returnData . sort ( ( a , b ) = > {
if ( a . name < b . name ) {
return - 1 ;
}
if ( a . name > b . name ) {
return 1 ;
}
return 0 ;
} ) ;
} ,
} ,
} ;
2022-09-08 05:44:34 -07:00
async poll ( this : IPollFunctions ) : Promise < INodeExecutionData [ ] [ ] | null > {
2024-05-22 03:17:52 -07:00
const workflowStaticData = this . getWorkflowStaticData ( 'node' ) ;
const node = this . getNode ( ) ;
let nodeStaticData = workflowStaticData ;
if ( node . typeVersion > 1 ) {
const nodeName = node . name ;
if ( workflowStaticData [ nodeName ] === undefined ) {
workflowStaticData [ nodeName ] = { } as IDataObject ;
nodeStaticData = workflowStaticData [ nodeName ] as IDataObject ;
} else {
nodeStaticData = workflowStaticData [ nodeName ] as IDataObject ;
}
}
2022-09-08 05:44:34 -07:00
let responseData ;
2023-01-13 09:11:56 -08:00
const now = Math . floor ( DateTime . now ( ) . toSeconds ( ) ) . toString ( ) ;
2024-05-22 03:17:52 -07:00
const startDate = ( nodeStaticData . lastTimeChecked as string ) || + now ;
2022-12-05 06:12:26 -08:00
const endDate = + now ;
2022-09-08 05:44:34 -07:00
const options = this . getNodeParameter ( 'options' , { } ) as IDataObject ;
const filters = this . getNodeParameter ( 'filters' , { } ) as IDataObject ;
try {
const qs : IDataObject = { } ;
filters . receivedAfter = startDate ;
if ( this . getMode ( ) === 'manual' ) {
qs . maxResults = 1 ;
delete filters . receivedAfter ;
}
2023-04-03 02:48:11 -07:00
Object . assign ( qs , prepareQuery . call ( this , filters , 0 ) , options ) ;
2022-09-08 05:44:34 -07:00
responseData = await googleApiRequest . call (
this ,
'GET' ,
2022-12-29 03:20:43 -08:00
'/gmail/v1/users/me/messages' ,
2022-09-08 05:44:34 -07:00
{ } ,
qs ,
) ;
responseData = responseData . messages ;
2023-07-24 07:29:38 -07:00
if ( ! responseData ? . length ) {
2024-05-22 03:17:52 -07:00
nodeStaticData . lastTimeChecked = endDate ;
2023-07-24 07:29:38 -07:00
return null ;
2022-09-08 05:44:34 -07:00
}
const simple = this . getNodeParameter ( 'simple' ) as boolean ;
if ( simple ) {
qs . format = 'metadata' ;
qs . metadataHeaders = [ 'From' , 'To' , 'Cc' , 'Bcc' , 'Subject' ] ;
} else {
qs . format = 'raw' ;
}
2024-11-06 05:28:26 -08:00
let includeDrafts ;
if ( node . typeVersion > 1.1 ) {
includeDrafts = ( qs . includeDrafts as boolean ) ? ? false ;
} else {
includeDrafts = ( qs . includeDrafts as boolean ) ? ? true ;
}
delete qs . includeDrafts ;
const withoutDrafts = [ ] ;
2022-09-08 05:44:34 -07:00
for ( let i = 0 ; i < responseData . length ; i ++ ) {
responseData [ i ] = await googleApiRequest . call (
this ,
'GET' ,
` /gmail/v1/users/me/messages/ ${ responseData [ i ] . id } ` ,
{ } ,
qs ,
) ;
2024-11-06 05:28:26 -08:00
if ( ! includeDrafts ) {
if ( responseData [ i ] . labelIds . includes ( 'DRAFT' ) ) {
continue ;
}
}
if ( ! simple && responseData ? . length ) {
2022-09-08 05:44:34 -07:00
const dataPropertyNameDownload =
( options . dataPropertyAttachmentsPrefixName as string ) || 'attachment_' ;
responseData [ i ] = await parseRawEmail . call (
this ,
responseData [ i ] ,
dataPropertyNameDownload ,
) ;
}
2024-11-06 05:28:26 -08:00
withoutDrafts . push ( responseData [ i ] ) ;
2022-09-08 05:44:34 -07:00
}
2024-11-06 05:28:26 -08:00
if ( ! includeDrafts ) {
responseData = withoutDrafts ;
}
if ( simple && responseData ? . length ) {
2023-02-27 19:39:43 -08:00
responseData = this . helpers . returnJsonArray (
await simplifyOutput . call ( this , responseData as IDataObject [ ] ) ,
) ;
2022-09-08 05:44:34 -07:00
}
} catch ( error ) {
2024-05-22 03:17:52 -07:00
if ( this . getMode ( ) === 'manual' || ! nodeStaticData . lastTimeChecked ) {
2022-09-08 05:44:34 -07:00
throw error ;
}
const workflow = this . getWorkflow ( ) ;
2023-03-22 06:04:15 -07:00
this . logger . error (
2022-09-08 05:44:34 -07:00
` There was a problem in ' ${ node . name } ' node in workflow ' ${ workflow . id } ': ' ${ error . description } ' ` ,
{
node : node.name ,
workflowId : workflow.id ,
error ,
} ,
) ;
}
2023-07-24 07:29:38 -07:00
if ( ! responseData ? . length ) {
2024-05-22 03:17:52 -07:00
nodeStaticData . lastTimeChecked = endDate ;
2023-07-24 07:29:38 -07:00
return null ;
}
2024-05-22 03:17:52 -07:00
const emailsWithInvalidDate = new Set < string > ( ) ;
const getEmailDateAsSeconds = ( email : IDataObject ) : number = > {
let date ;
if ( email . internalDate ) {
date = + ( email . internalDate as string ) / 1000 ;
} else if ( email . date ) {
date = + DateTime . fromJSDate ( new Date ( email . date as string ) ) . toSeconds ( ) ;
} else {
date = + DateTime . fromJSDate (
new Date ( ( email ? . headers as IDataObject ) ? . date as string ) ,
) . toSeconds ( ) ;
}
if ( ! date || isNaN ( date ) ) {
emailsWithInvalidDate . add ( email . id as string ) ;
return + startDate ;
}
return date ;
2022-12-05 06:12:26 -08:00
} ;
const lastEmailDate = ( responseData as IDataObject [ ] ) . reduce ( ( lastDate , { json } ) = > {
const emailDate = getEmailDateAsSeconds ( json as IDataObject ) ;
return emailDate > lastDate ? emailDate : lastDate ;
} , 0 ) ;
const nextPollPossibleDuplicates = ( responseData as IDataObject [ ] ) . reduce (
( duplicates , { json } ) = > {
const emailDate = getEmailDateAsSeconds ( json as IDataObject ) ;
2024-09-10 00:55:04 -07:00
return emailDate <= lastEmailDate
2022-12-05 06:12:26 -08:00
? duplicates . concat ( ( json as IDataObject ) . id as string )
: duplicates ;
} ,
2024-05-22 03:17:52 -07:00
Array . from ( emailsWithInvalidDate ) ,
2022-12-05 06:12:26 -08:00
) ;
2024-05-22 03:17:52 -07:00
const possibleDuplicates = ( nodeStaticData . possibleDuplicates as string [ ] ) || [ ] ;
2022-12-05 06:12:26 -08:00
if ( possibleDuplicates . length ) {
responseData = ( responseData as IDataObject [ ] ) . filter ( ( { json } ) = > {
const { id } = json as IDataObject ;
return ! possibleDuplicates . includes ( id as string ) ;
} ) ;
}
2024-05-22 03:17:52 -07:00
nodeStaticData . possibleDuplicates = nextPollPossibleDuplicates ;
nodeStaticData . lastTimeChecked = lastEmailDate || endDate ;
2022-09-08 05:44:34 -07:00
if ( Array . isArray ( responseData ) && responseData . length ) {
return [ responseData as INodeExecutionData [ ] ] ;
}
return null ;
}
}