2024-12-19 09:46:14 -08:00
import { DateTime } from 'luxon' ;
2023-01-27 03:22:44 -08:00
import type {
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 ,
2025-02-11 03:45:55 -08:00
IPollFunctions ,
2022-09-08 05:44:34 -07:00
} from 'n8n-workflow' ;
2024-08-29 06:55:53 -07:00
import { NodeConnectionType } from 'n8n-workflow' ;
2022-09-08 05:44:34 -07:00
2023-01-18 01:47:26 -08:00
import {
googleApiRequest ,
googleApiRequestAllItems ,
parseRawEmail ,
prepareQuery ,
simplifyOutput ,
} from './GenericFunctions' ;
2025-02-11 03:45:55 -08:00
import type {
GmailTriggerFilters ,
GmailTriggerOptions ,
GmailWorkflowStaticData ,
GmailWorkflowStaticDataDictionary ,
Label ,
Message ,
MessageListResponse ,
} from './types' ;
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 [ ] = [ ] ;
2025-02-11 03:45:55 -08:00
const labels = ( await googleApiRequestAllItems . call (
2023-01-18 01:47:26 -08:00
this ,
'labels' ,
'GET' ,
'/gmail/v1/users/me/labels' ,
2025-02-11 03:45:55 -08:00
) ) as Label [ ] ;
2023-01-18 01:47:26 -08:00
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 > {
2025-02-11 03:45:55 -08:00
const workflowStaticData = this . getWorkflowStaticData ( 'node' ) as
| GmailWorkflowStaticData
| GmailWorkflowStaticDataDictionary ;
2024-05-22 03:17:52 -07:00
const node = this . getNode ( ) ;
2025-02-11 03:45:55 -08:00
let nodeStaticData = ( workflowStaticData ? ? { } ) as GmailWorkflowStaticData ;
2024-05-22 03:17:52 -07:00
if ( node . typeVersion > 1 ) {
const nodeName = node . name ;
2025-02-11 03:45:55 -08:00
const dictionary = workflowStaticData as GmailWorkflowStaticDataDictionary ;
if ( ! ( nodeName in workflowStaticData ) ) {
dictionary [ nodeName ] = { } ;
2024-05-22 03:17:52 -07:00
}
2025-02-11 03:45:55 -08:00
nodeStaticData = dictionary [ nodeName ] ;
}
2022-09-08 05:44:34 -07:00
2023-01-13 09:11:56 -08:00
const now = Math . floor ( DateTime . now ( ) . toSeconds ( ) ) . toString ( ) ;
2025-02-11 03:45:55 -08:00
const startDate = nodeStaticData . lastTimeChecked ? ? + now ;
2022-12-05 06:12:26 -08:00
const endDate = + now ;
2022-09-08 05:44:34 -07:00
2025-02-11 03:45:55 -08:00
const options = this . getNodeParameter ( 'options' , { } ) as GmailTriggerOptions ;
const filters = this . getNodeParameter ( 'filters' , { } ) as GmailTriggerFilters ;
let responseData : INodeExecutionData [ ] = [ ] ;
2022-09-08 05:44:34 -07:00
try {
const qs : IDataObject = { } ;
2025-02-11 03:45:55 -08:00
const allFilters : GmailTriggerFilters = { . . . filters , receivedAfter : startDate } ;
2022-09-08 05:44:34 -07:00
if ( this . getMode ( ) === 'manual' ) {
qs . maxResults = 1 ;
2025-02-11 03:45:55 -08:00
delete allFilters . receivedAfter ;
2022-09-08 05:44:34 -07:00
}
2025-02-11 03:45:55 -08:00
Object . assign ( qs , prepareQuery . call ( this , allFilters , 0 ) , options ) ;
2022-09-08 05:44:34 -07:00
2025-02-11 03:45:55 -08:00
const messagesResponse : MessageListResponse = await googleApiRequest . call (
2022-09-08 05:44:34 -07:00
this ,
'GET' ,
2022-12-29 03:20:43 -08:00
'/gmail/v1/users/me/messages' ,
2022-09-08 05:44:34 -07:00
{ } ,
qs ,
) ;
2025-02-11 03:45:55 -08:00
const messages = messagesResponse . messages ? ? [ ] ;
if ( ! messages . 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' ;
}
2025-02-11 03:45:55 -08:00
let includeDrafts = false ;
2024-11-06 05:28:26 -08:00
if ( node . typeVersion > 1.1 ) {
2025-02-11 03:45:55 -08:00
includeDrafts = filters . includeDrafts ? ? false ;
2024-11-06 05:28:26 -08:00
} else {
2025-02-11 03:45:55 -08:00
includeDrafts = filters . includeDrafts ? ? true ;
2024-11-06 05:28:26 -08:00
}
2025-02-11 03:45:55 -08:00
2024-11-06 05:28:26 -08:00
delete qs . includeDrafts ;
2025-02-11 03:45:55 -08:00
for ( const message of messages ) {
const fullMessage = ( await googleApiRequest . call (
2022-09-08 05:44:34 -07:00
this ,
'GET' ,
2025-02-11 03:45:55 -08:00
` /gmail/v1/users/me/messages/ ${ message . id } ` ,
2022-09-08 05:44:34 -07:00
{ } ,
qs ,
2025-02-11 03:45:55 -08:00
) ) as Message ;
2024-11-06 05:28:26 -08:00
if ( ! includeDrafts ) {
2025-02-11 03:45:55 -08:00
if ( fullMessage . labelIds ? . includes ( 'DRAFT' ) ) {
2024-11-06 05:28:26 -08:00
continue ;
}
}
2025-02-11 03:45:55 -08:00
if ( ! simple ) {
2022-09-08 05:44:34 -07:00
const dataPropertyNameDownload =
2025-02-11 03:45:55 -08:00
options . dataPropertyAttachmentsPrefixName || 'attachment_' ;
2022-09-08 05:44:34 -07:00
2025-02-11 03:45:55 -08:00
const parsed = await parseRawEmail . call ( this , fullMessage , dataPropertyNameDownload ) ;
responseData . push ( parsed ) ;
} else {
responseData . push ( { json : fullMessage } ) ;
2022-09-08 05:44:34 -07:00
}
}
2025-02-11 03:45:55 -08:00
if ( simple ) {
2023-02-27 19:39:43 -08:00
responseData = this . helpers . returnJsonArray (
2025-02-11 03:45:55 -08:00
await simplifyOutput . call (
this ,
responseData . map ( ( item ) = > item . json ) ,
) ,
2023-02-27 19:39:43 -08:00
) ;
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 ,
} ,
) ;
}
2025-02-11 03:45:55 -08: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 > ( ) ;
2025-02-11 03:45:55 -08:00
const getEmailDateAsSeconds = ( email : Message ) : number = > {
2024-05-22 03:17:52 -07:00
let date ;
2025-02-11 03:45:55 -08:00
2024-05-22 03:17:52 -07:00
if ( email . internalDate ) {
2025-02-11 03:45:55 -08:00
date = + email . internalDate / 1000 ;
2024-05-22 03:17:52 -07:00
} else if ( email . date ) {
2025-02-11 03:45:55 -08:00
date = + DateTime . fromJSDate ( new Date ( email . date ) ) . toSeconds ( ) ;
} else if ( email . headers ? . date ) {
date = + DateTime . fromJSDate ( new Date ( email . headers . date ) ) . toSeconds ( ) ;
2024-05-22 03:17:52 -07:00
}
if ( ! date || isNaN ( date ) ) {
2025-02-11 03:45:55 -08:00
emailsWithInvalidDate . add ( email . id ) ;
2024-05-22 03:17:52 -07:00
return + startDate ;
}
return date ;
2022-12-05 06:12:26 -08:00
} ;
2025-02-11 03:45:55 -08:00
const lastEmailDate = responseData . reduce ( ( lastDate , { json } ) = > {
const emailDate = getEmailDateAsSeconds ( json as Message ) ;
2022-12-05 06:12:26 -08:00
return emailDate > lastDate ? emailDate : lastDate ;
} , 0 ) ;
2025-02-11 03:45:55 -08:00
const nextPollPossibleDuplicates = responseData
. filter ( ( item ) = > item . json )
. reduce ( ( duplicates , { json } ) = > {
const emailDate = getEmailDateAsSeconds ( json as Message ) ;
return emailDate <= lastEmailDate ? duplicates . concat ( ( json as Message ) . id ) : duplicates ;
} , Array . from ( emailsWithInvalidDate ) ) ;
const possibleDuplicates = new Set ( nodeStaticData . possibleDuplicates ? ? [ ] ) ;
if ( possibleDuplicates . size > 0 ) {
responseData = responseData . filter ( ( { json } ) = > {
if ( ! json || typeof json . id !== 'string' ) return false ;
return ! possibleDuplicates . has ( json . id ) ;
2022-12-05 06:12:26 -08:00
} ) ;
}
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 ) {
2025-02-11 03:45:55 -08:00
return [ responseData ] ;
2022-09-08 05:44:34 -07:00
}
return null ;
}
}