diff --git a/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts b/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts index b20c69dea8950..e946ff411612e 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.ts @@ -58,7 +58,8 @@ export class GoogleBigQuery implements INodeType { noDataExpression: true, options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Books/GoogleBooks.node.ts b/packages/nodes-base/nodes/Google/Books/GoogleBooks.node.ts index b2a281addfb15..587de577e8d26 100644 --- a/packages/nodes-base/nodes/Google/Books/GoogleBooks.node.ts +++ b/packages/nodes-base/nodes/Google/Books/GoogleBooks.node.ts @@ -71,7 +71,8 @@ export class GoogleBooks implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Docs/GoogleDocs.node.ts b/packages/nodes-base/nodes/Google/Docs/GoogleDocs.node.ts index 83c968c02485d..3e1ef84c5a3bd 100644 --- a/packages/nodes-base/nodes/Google/Docs/GoogleDocs.node.ts +++ b/packages/nodes-base/nodes/Google/Docs/GoogleDocs.node.ts @@ -83,7 +83,8 @@ export class GoogleDocs implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts index 3159d1fe11171..e24d5c6bc719d 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts @@ -74,7 +74,8 @@ export class GoogleDrive implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts index 76b6835fce605..5e3f8cd8cbfee 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDriveTrigger.node.ts @@ -56,7 +56,8 @@ export class GoogleDriveTrigger implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts index 1506661808050..c17d7e6d46a1d 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts @@ -6,43 +6,52 @@ import { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } fro import { IBinaryKeyData, + ICredentialDataDecryptedObject, IDataObject, INodeExecutionData, + IPollFunctions, NodeApiError, NodeOperationError, } from 'n8n-workflow'; -import { IEmail } from './Gmail.node'; - import moment from 'moment-timezone'; import jwt from 'jsonwebtoken'; -interface IGoogleAuthCredentials { - delegatedEmail?: string; - email: string; - inpersonate: boolean; - privateKey: string; +import { DateTime } from 'luxon'; + +import { isEmpty } from 'lodash'; + +export interface IEmail { + from?: string; + to?: string; + cc?: string; + bcc?: string; + inReplyTo?: string; + reference?: string; + subject: string; + body: string; + htmlBody?: string; + attachments?: IDataObject[]; +} + +export interface IAttachments { + type: string; + name: string; + content: string; } const mailComposer = require('nodemailer/lib/mail-composer'); export async function googleApiRequest( - this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, endpoint: string, - // tslint:disable-next-line:no-any - body: any = {}, + body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}, - // tslint:disable-next-line:no-any -): Promise { - const authenticationMethod = this.getNodeParameter( - 'authentication', - 0, - 'serviceAccount', - ) as string; +) { let options: OptionsWithUri = { headers: { Accept: 'application/json', @@ -65,32 +74,93 @@ export async function googleApiRequest( delete options.body; } - if (authenticationMethod === 'serviceAccount') { + let credentialType = 'gmailOAuth2'; + const authentication = this.getNodeParameter('authentication', 0) as string; + + if (authentication === 'serviceAccount') { const credentials = await this.getCredentials('googleApi'); + credentialType = 'googleApi'; - const { access_token } = await getAccessToken.call( - this, - credentials as unknown as IGoogleAuthCredentials, - ); + const { access_token } = await getAccessToken.call(this, credentials); - options.headers!.Authorization = `Bearer ${access_token}`; - //@ts-ignore - return await this.helpers.request(options); - } else { - //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'gmailOAuth2', options); + (options.headers as IDataObject)['Authorization'] = `Bearer ${access_token}`; } + + const response = await this.helpers.requestWithAuthentication.call( + this, + credentialType, + options, + ); + return response; } catch (error) { if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') { error.statusCode = '401'; } - throw new NodeApiError(this.getNode(), error); + if (error.httpCode === '400') { + if (error.cause && ((error.cause.message as string) || '').includes('Invalid id value')) { + const resource = this.getNodeParameter('resource', 0) as string; + const options = { + message: `Invalid ${resource} ID`, + description: `${ + resource.charAt(0).toUpperCase() + resource.slice(1) + } IDs should look something like this: 182b676d244938bd`, + }; + throw new NodeApiError(this.getNode(), error, options); + } + } + + if (error.httpCode === '404') { + let resource = this.getNodeParameter('resource', 0) as string; + if (resource === 'label') { + resource = 'label ID'; + } + const options = { + message: `${resource.charAt(0).toUpperCase() + resource.slice(1)} not found`, + description: '', + }; + throw new NodeApiError(this.getNode(), error, options); + } + + if (error.httpCode === '409') { + const resource = this.getNodeParameter('resource', 0) as string; + if (resource === 'label') { + const options = { + message: `Label name exists already`, + description: '', + }; + throw new NodeApiError(this.getNode(), error, options); + } + } + + if (error.code === 'EAUTH') { + const options = { + message: error?.body?.error_description || 'Authorization error', + description: (error as Error).message, + }; + throw new NodeApiError(this.getNode(), error, options); + } + + if ( + ((error.message as string) || '').includes('Bad request - please check your parameters') && + error.description + ) { + const options = { + message: error.description, + description: ``, + }; + throw new NodeApiError(this.getNode(), error, options); + } + + throw new NodeApiError(this.getNode(), error, { + message: error.message, + description: error.description, + }); } } export async function parseRawEmail( - this: IExecuteFunctions, + this: IExecuteFunctions | IPollFunctions, // tslint:disable-next-line:no-any messageData: any, dataPropertyNameDownload: string, @@ -111,13 +181,20 @@ export async function parseRawEmail( 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, - ); + const downloadAttachments = this.getNodeParameter( + 'options.downloadAttachments', + 0, + false, + ) as boolean; + if (downloadAttachments) { + 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; @@ -146,6 +223,7 @@ export async function parseRawEmail( //------------------------------------------------------------------------------------------------------------------------------------------ export async function encodeEmail(email: IEmail) { + // https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields let mailBody: Buffer; const mailOptions = { @@ -153,12 +231,13 @@ export async function encodeEmail(email: IEmail) { to: email.to, cc: email.cc, bcc: email.bcc, - replyTo: email.inReplyTo, + inReplyTo: email.inReplyTo, references: email.reference, subject: email.subject, text: email.body, keepBcc: true, } as IDataObject; + if (email.htmlBody) { mailOptions.html = email.htmlBody; } @@ -192,7 +271,7 @@ export async function encodeEmail(email: IEmail) { } export async function googleApiRequestAllItems( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, @@ -216,13 +295,16 @@ export async function googleApiRequestAllItems( } export function extractEmail(s: string) { - const data = s.split('<')[1]; - return data.substring(0, data.length - 1); + if (s.includes('<')) { + const data = s.split('<')[1]; + return data.substring(0, data.length - 1); + } + return s; } function getAccessToken( - this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, - credentials: IGoogleAuthCredentials, + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, + credentials: ICredentialDataDecryptedObject, ): Promise { //https://developers.google.com/identity/protocols/oauth2/service-account#httprest @@ -237,7 +319,7 @@ function getAccessToken( const now = moment().unix(); - credentials.email = credentials.email.trim(); + credentials.email = (credentials.email as string).trim(); const privateKey = (credentials.privateKey as string).replace(/\\n/g, '\n').trim(); const signature = jwt.sign( @@ -276,3 +358,344 @@ function getAccessToken( //@ts-ignore return this.helpers.request(options); } + +export function prepareQuery( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, + fields: IDataObject, +) { + const qs: IDataObject = { ...fields }; + if (qs.labelIds) { + if (qs.labelIds === '') { + delete qs.labelIds; + } else { + qs.labelIds = qs.labelIds as string[]; + } + } + + if (qs.sender) { + if (qs.q) { + qs.q += ` from:${qs.sender}`; + } else { + qs.q = `from:${qs.sender}`; + } + delete qs.sender; + } + + if (qs.readStatus && qs.readStatus !== 'both') { + if (qs.q) { + qs.q += ` is:${qs.readStatus}`; + } else { + qs.q = `is:${qs.readStatus}`; + } + delete qs.readStatus; + } + + if (qs.receivedAfter) { + let timestamp = DateTime.fromISO(qs.receivedAfter as string).toSeconds(); + const timestampLengthInMilliseconds1990 = 12; + + if (!timestamp && (qs.receivedAfter as string).length < timestampLengthInMilliseconds1990) { + timestamp = parseInt(qs.receivedAfter as string, 10); + } + + if (!timestamp) { + timestamp = Math.floor( + DateTime.fromMillis(parseInt(qs.receivedAfter as string, 10)).toSeconds(), + ); + } + + if (!timestamp) { + const description = `'${qs.receivedAfter}' isn't a valid date and time. If you're using an expression, be sure to set an ISO date string or a timestamp.`; + throw new NodeOperationError(this.getNode(), `Invalid date/time in 'Received After' field`, { + description, + }); + } + + if (qs.q) { + qs.q += ` after:${timestamp}`; + } else { + qs.q = `after:${timestamp}`; + } + delete qs.receivedAfter; + } + + if (qs.receivedBefore) { + let timestamp = DateTime.fromISO(qs.receivedBefore as string).toSeconds(); + const timestampLengthInMilliseconds1990 = 12; + + if (!timestamp && (qs.receivedBefore as string).length < timestampLengthInMilliseconds1990) { + timestamp = parseInt(qs.receivedBefore as string, 10); + } + + if (!timestamp) { + timestamp = Math.floor( + DateTime.fromMillis(parseInt(qs.receivedBefore as string, 10)).toSeconds(), + ); + } + + if (!timestamp) { + const description = `'${qs.receivedBefore}' isn't a valid date and time. If you're using an expression, be sure to set an ISO date string or a timestamp.`; + throw new NodeOperationError(this.getNode(), `Invalid date/time in 'Received Before' field`, { + description, + }); + } + + if (qs.q) { + qs.q += ` before:${timestamp}`; + } else { + qs.q = `before:${timestamp}`; + } + delete qs.receivedBefore; + } + + return qs; +} + +export function prepareEmailsInput( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + input: string, + fieldName: string, + itemIndex: number, +) { + let emails = ''; + + input.split(',').forEach((entry) => { + const email = entry.trim(); + + if (email.indexOf('@') === -1) { + const description = `The email address '${email}' in the '${fieldName}' field isn't valid`; + throw new NodeOperationError(this.getNode(), `Invalid email address`, { + description, + itemIndex, + }); + } + if (email.includes('<') && email.includes('>')) { + emails += `${email},`; + } else { + emails += `<${email}>, `; + } + }); + + return emails; +} + +export function prepareEmailBody( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + itemIndex: number, +) { + const emailType = this.getNodeParameter('emailType', itemIndex) as string; + + let body = ''; + let htmlBody = ''; + + if (emailType === 'html') { + htmlBody = (this.getNodeParameter('message', itemIndex, '') as string).trim(); + } else { + body = (this.getNodeParameter('message', itemIndex, '') as string).trim(); + } + + return { body, htmlBody }; +} + +export async function prepareEmailAttachments( + this: IExecuteFunctions, + options: IDataObject, + items: INodeExecutionData[], + itemIndex: number, +) { + const attachmentsList: IDataObject[] = []; + const attachments = (options as IDataObject).attachmentsBinary as IDataObject[]; + + if (attachments && !isEmpty(attachments)) { + for (const { property } of attachments) { + for (const name of (property as string).split(',')) { + if (!items[itemIndex].binary || items[itemIndex].binary![name] === undefined) { + const description = `This node has no input field called '${name}' `; + throw new NodeOperationError(this.getNode(), `Attachment not found`, { + description, + itemIndex, + }); + } + + const binaryData = items[itemIndex].binary![name]; + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, name); + + if (!items[itemIndex].binary![name] || !Buffer.isBuffer(binaryDataBuffer)) { + const description = `The input field '${name}' doesn't contain an attachment. Please make sure you specify a field containing binary data`; + throw new NodeOperationError(this.getNode(), `Attachment not found`, { + description, + itemIndex, + }); + } + + attachmentsList.push({ + name: binaryData.fileName || 'unknown', + content: binaryDataBuffer, + type: binaryData.mimeType, + }); + } + } + } + return attachmentsList; +} + +export function unescapeSnippets(items: INodeExecutionData[]) { + const result = items.map((item) => { + const snippet = (item.json as IDataObject).snippet as string; + if (snippet) { + (item.json as IDataObject).snippet = snippet.replace( + /&|<|>|'|"/g, + (match) => { + switch (match) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case ''': + return "'"; + case '"': + return '"'; + default: + return match; + } + }, + ); + } + return item; + }); + return result; +} + +export async function replayToEmail( + this: IExecuteFunctions, + items: INodeExecutionData[], + gmailId: string, + options: IDataObject, + itemIndex: number, +) { + let qs: IDataObject = {}; + + let cc = ''; + let bcc = ''; + + if (options.ccList) { + cc = prepareEmailsInput.call(this, options.ccList as string, 'CC', itemIndex); + } + + if (options.bccList) { + bcc = prepareEmailsInput.call(this, options.bccList as string, 'BCC', itemIndex); + } + let attachments: IDataObject[] = []; + if (options.attachmentsUi) { + attachments = await prepareEmailAttachments.call( + this, + options.attachmentsUi as IDataObject, + items, + itemIndex, + ); + if (attachments.length) { + qs = { + userId: 'me', + uploadType: 'media', + }; + } + } + + const endpoint = `/gmail/v1/users/me/messages/${gmailId}`; + + qs.format = 'metadata'; + + const { payload, threadId } = await googleApiRequest.call(this, 'GET', endpoint, {}, qs); + + const subject = + payload.headers.filter( + (data: { [key: string]: string }) => data.name.toLowerCase() === 'subject', + )[0]?.value || ''; + + const messageIdGlobal = + payload.headers.filter( + (data: { [key: string]: string }) => data.name.toLowerCase() === 'message-id', + )[0]?.value || ''; + + const { emailAddress } = await googleApiRequest.call(this, 'GET', '/gmail/v1/users/me/profile'); + + let to = ''; + const replyToSenderOnly = + options.replyToSenderOnly === undefined ? false : (options.replyToSenderOnly as boolean); + + for (const header of payload.headers as IDataObject[]) { + if (((header.name as string) || '').toLowerCase() === 'from') { + const from = header.value as string; + if (from.includes('<') && from.includes('>')) { + to += `${from}, `; + } else { + to += `<${from}>, `; + } + } + + if (((header.name as string) || '').toLowerCase() === 'to' && !replyToSenderOnly) { + const toEmails = header.value as string; + toEmails.split(',').forEach((email: string) => { + if (email.includes(emailAddress)) return; + if (email.includes('<') && email.includes('>')) { + to += `${email}, `; + } else { + to += `<${email}>, `; + } + }); + } + } + + let from = ''; + if (options.senderName) { + from = `${options.senderName as string} <${emailAddress}>`; + } + + const email: IEmail = { + from, + to, + cc, + bcc, + subject, + attachments, + inReplyTo: messageIdGlobal, + reference: messageIdGlobal, + ...prepareEmailBody.call(this, itemIndex), + }; + + const body = { + raw: await encodeEmail(email), + threadId, + }; + + return await googleApiRequest.call(this, 'POST', '/gmail/v1/users/me/messages/send', body, qs); +} + +export async function simplifyOutput( + this: IExecuteFunctions | IPollFunctions, + data: IDataObject[], +) { + const labelsData = await googleApiRequest.call(this, 'GET', `/gmail/v1/users/me/labels`); + const labels = ((labelsData.labels as IDataObject[]) || []).map(({ id, name }) => ({ + id, + name, + })); + return ((data as IDataObject[]) || []).map((item) => { + if (item.labelIds) { + item.labels = labels.filter((label) => + (item.labelIds as string[]).includes(label.id as string), + ); + delete item.labelIds; + } + if (item.payload && (item.payload as IDataObject).headers) { + const { headers } = item.payload as IDataObject; + ((headers as IDataObject[]) || []).forEach((header) => { + item[header.name as string] = header.value; + }); + delete (item.payload as IDataObject).headers; + } + return item; + }); +} diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json index 5847ea0eab9cc..055cd7161ff24 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json @@ -2,9 +2,7 @@ "node": "n8n-nodes-base.gmail", "nodeVersion": "1.0", "codexVersion": "1.0", - "categories": [ - "Communication" - ], + "categories": ["Communication"], "resources": { "credentialDocumentation": [ { @@ -54,4 +52,4 @@ } ] } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts index df6ee2db73f5e..e004f1bd66801 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts @@ -1,838 +1,28 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { INodeTypeBaseDescription, INodeVersionedType } from 'n8n-workflow'; -import { - IBinaryKeyData, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; +import { NodeVersionedType } from '../../../src/NodeVersionedType'; -import { - encodeEmail, - extractEmail, - googleApiRequest, - googleApiRequestAllItems, - parseRawEmail, -} from './GenericFunctions'; +import { GmailV1 } from './v1/GmailV1.node'; -import { messageFields, messageOperations } from './MessageDescription'; +import { GmailV2 } from './v2/GmailV2.node'; -import { messageLabelFields, messageLabelOperations } from './MessageLabelDescription'; +export class Gmail extends NodeVersionedType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Gmail', + name: 'gmail', + icon: 'file:gmail.svg', + group: ['transform'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Gmail API', + defaultVersion: 2, + }; -import { labelFields, labelOperations } from './LabelDescription'; + const nodeVersions: INodeVersionedType['nodeVersions'] = { + 1: new GmailV1(baseDescription), + 2: new GmailV2(baseDescription), + }; -import { draftFields, draftOperations } from './DraftDescription'; - -import { isEmpty } from 'lodash'; - -export interface IEmail { - from?: string; - to?: string; - cc?: string; - bcc?: string; - inReplyTo?: string; - reference?: string; - subject: string; - body: string; - htmlBody?: string; - attachments?: IDataObject[]; -} - -interface IAttachments { - type: string; - name: string; - content: string; -} - -export class Gmail implements INodeType { - description: INodeTypeDescription = { - displayName: 'Gmail', - name: 'gmail', - icon: 'file:gmail.svg', - group: ['transform'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume the Gmail API', - defaults: { - name: 'Gmail', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'googleApi', - required: true, - displayOptions: { - show: { - authentication: ['serviceAccount'], - }, - }, - }, - { - name: 'gmailOAuth2', - required: true, - displayOptions: { - show: { - authentication: ['oAuth2'], - }, - }, - }, - ], - properties: [ - { - displayName: 'Authentication', - name: 'authentication', - type: 'options', - options: [ - { - name: 'OAuth2 (Recommended)', - value: 'oAuth2', - }, - { - name: 'Service Account', - value: 'serviceAccount', - }, - ], - default: 'oAuth2', - }, - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Draft', - value: 'draft', - }, - { - name: 'Label', - value: 'label', - }, - { - name: 'Message', - value: 'message', - }, - { - name: 'Message Label', - value: 'messageLabel', - }, - ], - default: 'draft', - }, - //------------------------------- - // Draft Operations - //------------------------------- - ...draftOperations, - ...draftFields, - //------------------------------- - // Label Operations - //------------------------------- - ...labelOperations, - ...labelFields, - //------------------------------- - // Message Operations - //------------------------------- - ...messageOperations, - ...messageFields, - //------------------------------- - // MessageLabel Operations - //------------------------------- - ...messageLabelOperations, - ...messageLabelFields, - ], - }; - - methods = { - loadOptions: { - // Get all the labels to display them to user so that he can - // select them easily - async getLabels(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const labels = await googleApiRequestAllItems.call( - this, - 'labels', - 'GET', - '/gmail/v1/users/me/labels', - ); - for (const label of labels) { - const labelName = label.name; - const labelId = label.id; - returnData.push({ - name: labelName, - value: labelId, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const resource = this.getNodeParameter('resource', 0) as string; - const operation = this.getNodeParameter('operation', 0) as string; - - let method = ''; - let body: IDataObject = {}; - let qs: IDataObject = {}; - let endpoint = ''; - let responseData; - - for (let i = 0; i < items.length; i++) { - try { - if (resource === 'label') { - if (operation === 'create') { - //https://developers.google.com/gmail/api/v1/reference/users/labels/create - const labelName = this.getNodeParameter('name', i) as string; - const labelListVisibility = this.getNodeParameter('labelListVisibility', i) as string; - const messageListVisibility = this.getNodeParameter( - 'messageListVisibility', - i, - ) as string; - - method = 'POST'; - endpoint = '/gmail/v1/users/me/labels'; - - body = { - labelListVisibility, - messageListVisibility, - name: labelName, - }; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - if (operation === 'delete') { - //https://developers.google.com/gmail/api/v1/reference/users/labels/delete - const labelId = this.getNodeParameter('labelId', i) as string[]; - - method = 'DELETE'; - endpoint = `/gmail/v1/users/me/labels/${labelId}`; - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - responseData = { success: true }; - } - if (operation === 'get') { - // https://developers.google.com/gmail/api/v1/reference/users/labels/get - const labelId = this.getNodeParameter('labelId', i); - - method = 'GET'; - endpoint = `/gmail/v1/users/me/labels/${labelId}`; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - - responseData = await googleApiRequest.call( - this, - 'GET', - `/gmail/v1/users/me/labels`, - {}, - qs, - ); - - responseData = responseData.labels; - - if (!returnAll) { - const limit = this.getNodeParameter('limit', i) as number; - responseData = responseData.splice(0, limit); - } - } - } - if (resource === 'messageLabel') { - if (operation === 'remove') { - //https://developers.google.com/gmail/api/v1/reference/users/messages/modify - const messageID = this.getNodeParameter('messageId', i); - const labelIds = this.getNodeParameter('labelIds', i) as string[]; - - method = 'POST'; - endpoint = `/gmail/v1/users/me/messages/${messageID}/modify`; - body = { - removeLabelIds: labelIds, - }; - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - if (operation === 'add') { - // https://developers.google.com/gmail/api/v1/reference/users/messages/modify - const messageID = this.getNodeParameter('messageId', i); - const labelIds = this.getNodeParameter('labelIds', i) as string[]; - - method = 'POST'; - endpoint = `/gmail/v1/users/me/messages/${messageID}/modify`; - - body = { - addLabelIds: labelIds, - }; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - } - if (resource === 'message') { - if (operation === 'send') { - // https://developers.google.com/gmail/api/v1/reference/users/messages/send - - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - - let toStr = ''; - let ccStr = ''; - let bccStr = ''; - let attachmentsList: IDataObject[] = []; - - const toList = this.getNodeParameter('toList', i) as IDataObject[]; - - toList.forEach((email) => { - toStr += `<${email}>, `; - }); - - if (additionalFields.ccList) { - const ccList = additionalFields.ccList as IDataObject[]; - - ccList.forEach((email) => { - ccStr += `<${email}>, `; - }); - } - - if (additionalFields.bccList) { - const bccList = additionalFields.bccList as IDataObject[]; - - bccList.forEach((email) => { - bccStr += `<${email}>, `; - }); - } - - if (additionalFields.attachmentsUi) { - const attachmentsUi = additionalFields.attachmentsUi as IDataObject; - const attachmentsBinary = []; - if (!isEmpty(attachmentsUi)) { - if ( - attachmentsUi.hasOwnProperty('attachmentsBinary') && - !isEmpty(attachmentsUi.attachmentsBinary) && - items[i].binary - ) { - // @ts-ignore - for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { - for (const binaryProperty of (property as string).split(',')) { - if (items[i].binary![binaryProperty] !== undefined) { - const binaryData = items[i].binary![binaryProperty]; - const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( - i, - binaryProperty, - ); - attachmentsBinary.push({ - name: binaryData.fileName || 'unknown', - content: binaryDataBuffer, - type: binaryData.mimeType, - }); - } - } - } - } - - qs = { - userId: 'me', - uploadType: 'media', - }; - attachmentsList = attachmentsBinary; - } - } - - const email: IEmail = { - from: (additionalFields.senderName as string) || '', - to: toStr, - cc: ccStr, - bcc: bccStr, - subject: this.getNodeParameter('subject', i) as string, - body: this.getNodeParameter('message', i) as string, - attachments: attachmentsList, - }; - - if ((this.getNodeParameter('includeHtml', i, false) as boolean) === true) { - email.htmlBody = this.getNodeParameter('htmlMessage', i) as string; - } - - endpoint = '/gmail/v1/users/me/messages/send'; - method = 'POST'; - - body = { - raw: await encodeEmail(email), - }; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - if (operation === 'reply') { - const id = this.getNodeParameter('messageId', i) as string; - - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - - let toStr = ''; - let ccStr = ''; - let bccStr = ''; - let attachmentsList: IDataObject[] = []; - - const toList = this.getNodeParameter('toList', i) as IDataObject[]; - - toList.forEach((email) => { - toStr += `<${email}>, `; - }); - - if (additionalFields.ccList) { - const ccList = additionalFields.ccList as IDataObject[]; - - ccList.forEach((email) => { - ccStr += `<${email}>, `; - }); - } - - if (additionalFields.bccList) { - const bccList = additionalFields.bccList as IDataObject[]; - - bccList.forEach((email) => { - bccStr += `<${email}>, `; - }); - } - - if (additionalFields.attachmentsUi) { - const attachmentsUi = additionalFields.attachmentsUi as IDataObject; - const attachmentsBinary = []; - if (!isEmpty(attachmentsUi)) { - if ( - attachmentsUi.hasOwnProperty('attachmentsBinary') && - !isEmpty(attachmentsUi.attachmentsBinary) && - items[i].binary - ) { - // @ts-ignore - for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { - for (const binaryProperty of (property as string).split(',')) { - if (items[i].binary![binaryProperty] !== undefined) { - const binaryData = items[i].binary![binaryProperty]; - const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( - i, - binaryProperty, - ); - attachmentsBinary.push({ - name: binaryData.fileName || 'unknown', - content: binaryDataBuffer, - type: binaryData.mimeType, - }); - } - } - } - } - - qs = { - userId: 'me', - uploadType: 'media', - }; - attachmentsList = attachmentsBinary; - } - } - // if no recipient is defined then grab the one who sent the email - if (toStr === '') { - endpoint = `/gmail/v1/users/me/messages/${id}`; - - qs.format = 'metadata'; - - const { payload } = await googleApiRequest.call(this, method, endpoint, body, qs); - - for (const header of payload.headers as IDataObject[]) { - if (header.name === 'From') { - toStr = `<${extractEmail(header.value as string)}>,`; - break; - } - } - } - - const email: IEmail = { - from: (additionalFields.senderName as string) || '', - to: toStr, - cc: ccStr, - bcc: bccStr, - subject: this.getNodeParameter('subject', i) as string, - body: this.getNodeParameter('message', i) as string, - attachments: attachmentsList, - }; - - if ((this.getNodeParameter('includeHtml', i, false) as boolean) === true) { - email.htmlBody = this.getNodeParameter('htmlMessage', i) as string; - } - - endpoint = '/gmail/v1/users/me/messages/send'; - method = 'POST'; - - email.inReplyTo = id; - email.reference = id; - - body = { - raw: await encodeEmail(email), - threadId: this.getNodeParameter('threadId', i) as string, - }; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - if (operation === 'get') { - //https://developers.google.com/gmail/api/v1/reference/users/messages/get - method = 'GET'; - - const id = this.getNodeParameter('messageId', i); - - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const format = additionalFields.format || 'resolved'; - - if (format === 'resolved') { - qs.format = 'raw'; - } else { - qs.format = format; - } - - endpoint = `/gmail/v1/users/me/messages/${id}`; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - - let nodeExecutionData: INodeExecutionData; - if (format === 'resolved') { - const dataPropertyNameDownload = - (additionalFields.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; - - nodeExecutionData = await parseRawEmail.call( - this, - responseData, - dataPropertyNameDownload, - ); - } else { - nodeExecutionData = { - json: responseData, - }; - } - - responseData = nodeExecutionData; - } - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - Object.assign(qs, additionalFields); - - if (qs.labelIds) { - // tslint:disable-next-line: triple-equals - if (qs.labelIds == '') { - delete qs.labelIds; - } else { - qs.labelIds = qs.labelIds as string[]; - } - } - - if (returnAll) { - responseData = await googleApiRequestAllItems.call( - this, - 'messages', - 'GET', - `/gmail/v1/users/me/messages`, - {}, - qs, - ); - } else { - qs.maxResults = this.getNodeParameter('limit', i) as number; - responseData = await googleApiRequest.call( - this, - 'GET', - `/gmail/v1/users/me/messages`, - {}, - qs, - ); - responseData = responseData.messages; - } - - if (responseData === undefined) { - responseData = []; - } - - const format = additionalFields.format || 'resolved'; - - if (format !== 'ids') { - if (format === 'resolved') { - qs.format = 'raw'; - } else { - qs.format = format; - } - - for (let i = 0; i < responseData.length; i++) { - responseData[i] = await googleApiRequest.call( - this, - 'GET', - `/gmail/v1/users/me/messages/${responseData[i].id}`, - body, - qs, - ); - - if (format === 'resolved') { - const dataPropertyNameDownload = - (additionalFields.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; - - responseData[i] = await parseRawEmail.call( - this, - responseData[i], - dataPropertyNameDownload, - ); - } - } - } - - if (format !== 'resolved') { - responseData = this.helpers.returnJsonArray(responseData); - } - } - if (operation === 'delete') { - // https://developers.google.com/gmail/api/v1/reference/users/messages/delete - method = 'DELETE'; - const id = this.getNodeParameter('messageId', i); - - endpoint = `/gmail/v1/users/me/messages/${id}`; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - - responseData = { success: true }; - } - } - if (resource === 'draft') { - if (operation === 'create') { - // https://developers.google.com/gmail/api/v1/reference/users/drafts/create - - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - - let toStr = ''; - let ccStr = ''; - let bccStr = ''; - let attachmentsList: IDataObject[] = []; - - if (additionalFields.toList) { - const toList = additionalFields.toList as IDataObject[]; - - toList.forEach((email) => { - toStr += `<${email}>, `; - }); - } - - if (additionalFields.ccList) { - const ccList = additionalFields.ccList as IDataObject[]; - - ccList.forEach((email) => { - ccStr += `<${email}>, `; - }); - } - - if (additionalFields.bccList) { - const bccList = additionalFields.bccList as IDataObject[]; - - bccList.forEach((email) => { - bccStr += `<${email}>, `; - }); - } - - if (additionalFields.attachmentsUi) { - const attachmentsUi = additionalFields.attachmentsUi as IDataObject; - const attachmentsBinary = []; - if (!isEmpty(attachmentsUi)) { - if (!isEmpty(attachmentsUi)) { - if ( - attachmentsUi.hasOwnProperty('attachmentsBinary') && - !isEmpty(attachmentsUi.attachmentsBinary) && - items[i].binary - ) { - for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { - for (const binaryProperty of (property as string).split(',')) { - if (items[i].binary![binaryProperty] !== undefined) { - const binaryData = items[i].binary![binaryProperty]; - const binaryDataBuffer = await this.helpers.getBinaryDataBuffer( - i, - binaryProperty, - ); - attachmentsBinary.push({ - name: binaryData.fileName || 'unknown', - content: binaryDataBuffer, - type: binaryData.mimeType, - }); - } - } - } - } - } - - qs = { - userId: 'me', - uploadType: 'media', - }; - - attachmentsList = attachmentsBinary; - } - } - - const email: IEmail = { - to: toStr, - cc: ccStr, - bcc: bccStr, - subject: this.getNodeParameter('subject', i) as string, - body: this.getNodeParameter('message', i) as string, - attachments: attachmentsList, - }; - - if ((this.getNodeParameter('includeHtml', i, false) as boolean) === true) { - email.htmlBody = this.getNodeParameter('htmlMessage', i) as string; - } - - endpoint = '/gmail/v1/users/me/drafts'; - method = 'POST'; - - body = { - message: { - raw: await encodeEmail(email), - }, - }; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - } - if (operation === 'get') { - // https://developers.google.com/gmail/api/v1/reference/users/drafts/get - method = 'GET'; - const id = this.getNodeParameter('messageId', i); - - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - const format = additionalFields.format || 'resolved'; - - if (format === 'resolved') { - qs.format = 'raw'; - } else { - qs.format = format; - } - - endpoint = `/gmail/v1/users/me/drafts/${id}`; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - - const binaryData: IBinaryKeyData = {}; - - let nodeExecutionData: INodeExecutionData; - if (format === 'resolved') { - const dataPropertyNameDownload = - (additionalFields.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; - - nodeExecutionData = await parseRawEmail.call( - this, - responseData.message, - dataPropertyNameDownload, - ); - - // Add the draft-id - nodeExecutionData.json.messageId = nodeExecutionData.json.id; - nodeExecutionData.json.id = responseData.id; - } else { - nodeExecutionData = { - json: responseData, - binary: Object.keys(binaryData).length ? binaryData : undefined, - }; - } - - responseData = nodeExecutionData; - } - if (operation === 'delete') { - // https://developers.google.com/gmail/api/v1/reference/users/drafts/delete - method = 'DELETE'; - const id = this.getNodeParameter('messageId', i); - - endpoint = `/gmail/v1/users/me/drafts/${id}`; - - responseData = await googleApiRequest.call(this, method, endpoint, body, qs); - - responseData = { success: true }; - } - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - Object.assign(qs, additionalFields); - - if (returnAll) { - responseData = await googleApiRequestAllItems.call( - this, - 'drafts', - 'GET', - `/gmail/v1/users/me/drafts`, - {}, - qs, - ); - } else { - qs.maxResults = this.getNodeParameter('limit', i) as number; - responseData = await googleApiRequest.call( - this, - 'GET', - `/gmail/v1/users/me/drafts`, - {}, - qs, - ); - responseData = responseData.drafts; - } - - if (responseData === undefined) { - responseData = []; - } - - const format = additionalFields.format || 'resolved'; - - if (format !== 'ids') { - if (format === 'resolved') { - qs.format = 'raw'; - } else { - qs.format = format; - } - - for (let i = 0; i < responseData.length; i++) { - responseData[i] = await googleApiRequest.call( - this, - 'GET', - `/gmail/v1/users/me/drafts/${responseData[i].id}`, - body, - qs, - ); - - if (format === 'resolved') { - const dataPropertyNameDownload = - (additionalFields.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; - const id = responseData[i].id; - responseData[i] = await parseRawEmail.call( - this, - responseData[i].message, - dataPropertyNameDownload, - ); - - // Add the draft-id - responseData[i].json.messageId = responseData[i].json.id; - responseData[i].json.id = id; - } - } - } - - if (format !== 'resolved') { - responseData = this.helpers.returnJsonArray(responseData); - } - } - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ json: { error: error.message } }); - continue; - } - throw error; - } - } - - return this.prepareOutputData(returnData); + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.json b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.json new file mode 100644 index 0000000000000..9bdd8e99fec99 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.json @@ -0,0 +1,55 @@ +{ + "node": "n8n-nodes-base.gmailTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Communication"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/google" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.gmailTrigger/" + } + ], + "generic": [ + { + "label": "Why business process automation with n8n can change your daily life", + "icon": "🧬", + "url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/" + }, + { + "label": "Supercharging your conference registration process with n8n", + "icon": "🎫", + "url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/" + }, + { + "label": "6 e-commerce workflows to power up your Shopify s", + "icon": "store", + "url": "https://n8n.io/blog/no-code-ecommerce-workflow-automations/" + }, + { + "label": "How to get started with CRM automation (with 3 no-code workflow ideas", + "icon": "👥", + "url": "https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/" + }, + { + "label": "15 Google apps you can combine and automate to increase productivity", + "icon": "💡", + "url": "https://n8n.io/blog/automate-google-apps-for-productivity/" + }, + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, + { + "label": "Using Automation to Boost Productivity in the Workplace", + "icon": "💪", + "url": "https://n8n.io/blog/using-automation-to-boost-productivity-in-the-workplace/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts new file mode 100644 index 0000000000000..fe663c2490c87 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts @@ -0,0 +1,284 @@ +import { IPollFunctions } from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + LoggerProxy as Logger, +} from 'n8n-workflow'; + +import { googleApiRequest, parseRawEmail, prepareQuery, simplifyOutput } from './GenericFunctions'; + +import { DateTime } from 'luxon'; + +export class GmailTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Gmail Trigger', + name: 'gmailTrigger', + icon: 'file:gmail.svg', + group: ['trigger'], + version: 1, + 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: [], + outputs: ['main'], + 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', + }, + { + displayName: 'Label Names or IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + description: + 'Only return messages with labels that match all of the specified label IDs. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'Search', + name: 'q', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + placeholder: 'has:attachment', + hint: 'Use the same format as in the Gmail search box. More info.', + 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', + placeholder: 'Add Option', + 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, + description: "Whether the emaail's attachments will be downloaded", + }, + ], + }, + ], + }; + + async poll(this: IPollFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + let responseData; + + const now = Math.floor(DateTime.now().toSeconds()) + ''; + const startDate = (webhookData.lastTimeChecked as string) || now; + const endDate = now; + + 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; + } + + Object.assign(qs, prepareQuery.call(this, filters), options); + + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/messages`, + {}, + qs, + ); + responseData = responseData.messages; + + if (responseData === undefined) { + responseData = []; + } + + const simple = this.getNodeParameter('simple') as boolean; + + if (simple) { + qs.format = 'metadata'; + qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; + } else { + qs.format = 'raw'; + } + + for (let i = 0; i < responseData.length; i++) { + responseData[i] = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/messages/${responseData[i].id}`, + {}, + qs, + ); + + if (!simple) { + const dataPropertyNameDownload = + (options.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; + + responseData[i] = await parseRawEmail.call( + this, + responseData[i], + dataPropertyNameDownload, + ); + } + } + + if (simple) { + responseData = this.helpers.returnJsonArray(await simplifyOutput.call(this, responseData)); + } + } catch (error) { + if (this.getMode() === 'manual' || !webhookData.lastTimeChecked) { + throw error; + } + const workflow = this.getWorkflow(); + const node = this.getNode(); + Logger.error( + `There was a problem in '${node.name}' node in workflow '${workflow.id}': '${error.description}'`, + { + node: node.name, + workflowId: workflow.id, + error, + }, + ); + } + + webhookData.lastTimeChecked = endDate; + + if (Array.isArray(responseData) && responseData.length) { + return [responseData as INodeExecutionData[]]; + } + + return null; + } +} diff --git a/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v1/DraftDescription.ts similarity index 94% rename from packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts rename to packages/nodes-base/nodes/Google/Gmail/v1/DraftDescription.ts index 106a28cd92c98..669f6cead28a6 100644 --- a/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v1/DraftDescription.ts @@ -15,25 +15,21 @@ export const draftOperations: INodeProperties[] = [ { name: 'Create', value: 'create', - description: 'Create a new email draft', action: 'Create a draft', }, { name: 'Delete', value: 'delete', - description: 'Delete a draft', action: 'Delete a draft', }, { name: 'Get', value: 'get', - description: 'Get a draft', action: 'Get a draft', }, { name: 'Get Many', value: 'getAll', - description: 'Get all drafts', action: 'Get all drafts', }, ], @@ -55,7 +51,6 @@ export const draftFields: INodeProperties[] = [ }, }, placeholder: 'r-3254521568507167962', - description: 'The ID of the draft to operate on', }, { displayName: 'Subject', @@ -70,7 +65,6 @@ export const draftFields: INodeProperties[] = [ }, }, placeholder: 'Hello World!', - description: 'The message subject', }, { displayName: 'HTML', @@ -166,9 +160,9 @@ export const draftFields: INodeProperties[] = [ default: [], }, { - displayName: 'Attachments', + displayName: 'Attachment', name: 'attachmentsUi', - placeholder: 'Add Attachments', + placeholder: 'Add Attachment', type: 'fixedCollection', typeOptions: { multipleValues: true, @@ -176,10 +170,10 @@ export const draftFields: INodeProperties[] = [ options: [ { name: 'attachmentsBinary', - displayName: 'Attachments Binary', + displayName: 'Attachment Binary', values: [ { - displayName: 'Property', + displayName: 'Attachment Field Name (in Input)', name: 'property', type: 'string', default: '', @@ -208,7 +202,7 @@ export const draftFields: INodeProperties[] = [ default: {}, options: [ { - displayName: 'Attachments Prefix', + displayName: 'Attachment Prefix', name: 'dataPropertyAttachmentsPrefixName', type: 'string', default: 'attachment_', @@ -309,7 +303,7 @@ export const draftFields: INodeProperties[] = [ }, options: [ { - displayName: 'Attachments Prefix', + displayName: 'Attachment Prefix', name: 'dataPropertyAttachmentsPrefixName', type: 'string', default: 'attachment_', @@ -365,7 +359,7 @@ export const draftFields: INodeProperties[] = [ description: 'The format to return the message in', }, { - displayName: 'Include Spam Trash', + displayName: 'Include Spam and Trash', name: 'includeSpamTrash', type: 'boolean', default: false, diff --git a/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts b/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts new file mode 100644 index 0000000000000..555b70ada3a91 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts @@ -0,0 +1,825 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + encodeEmail, + extractEmail, + googleApiRequest, + googleApiRequestAllItems, + IEmail, + parseRawEmail, +} from '../GenericFunctions'; + +import { + messageFields, + messageOperations, +} from './MessageDescription'; + +import { + messageLabelFields, + messageLabelOperations, +} from './MessageLabelDescription'; + +import { + labelFields, + labelOperations, +} from './LabelDescription'; + +import { + draftFields, + draftOperations, +} from './DraftDescription'; + +import { + isEmpty, +} from 'lodash'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Gmail', + name: 'gmail', + icon: 'file:gmail.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Gmail API', + defaults: { + name: 'Gmail', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, + { + name: 'gmailOAuth2', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + ], + 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: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Label', + value: 'label', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Message Label', + value: 'messageLabel', + }, + ], + default: 'draft', + }, + //------------------------------- + // Draft Operations + //------------------------------- + ...draftOperations, + ...draftFields, + //------------------------------- + // Label Operations + //------------------------------- + ...labelOperations, + ...labelFields, + //------------------------------- + // Message Operations + //------------------------------- + ...messageOperations, + ...messageFields, + //------------------------------- + // MessageLabel Operations + //------------------------------- + ...messageLabelOperations, + ...messageLabelFields, + //------------------------------- + ], +}; + +export class GmailV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + // Get all the labels to display them to user so that he can + // select them easily + async getLabels( + this: ILoadOptionsFunctions, + ): Promise { + 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; + }); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let method = ''; + let body: IDataObject = {}; + let qs: IDataObject = {}; + let endpoint = ''; + let responseData; + + for (let i = 0; i < items.length; i++) { + try { + if (resource === 'label') { + if (operation === 'create') { + //https://developers.google.com/gmail/api/v1/reference/users/labels/create + const labelName = this.getNodeParameter('name', i) as string; + const labelListVisibility = this.getNodeParameter('labelListVisibility', i) as string; + const messageListVisibility = this.getNodeParameter('messageListVisibility', i) as string; + + method = 'POST'; + endpoint = '/gmail/v1/users/me/labels'; + + body = { + labelListVisibility, + messageListVisibility, + name: labelName, + }; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + if (operation === 'delete') { + //https://developers.google.com/gmail/api/v1/reference/users/labels/delete + const labelId = this.getNodeParameter('labelId', i) as string[]; + + method = 'DELETE'; + endpoint = `/gmail/v1/users/me/labels/${labelId}`; + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + responseData = { success: true }; + + } + if (operation === 'get') { + // https://developers.google.com/gmail/api/v1/reference/users/labels/get + const labelId = this.getNodeParameter('labelId', i); + + method = 'GET'; + endpoint = `/gmail/v1/users/me/labels/${labelId}`; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/labels`, + {}, + qs, + ); + + responseData = responseData.labels; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + } + if (resource === 'messageLabel') { + if (operation === 'remove') { + //https://developers.google.com/gmail/api/v1/reference/users/messages/modify + const messageID = this.getNodeParameter('messageId', i); + const labelIds = this.getNodeParameter('labelIds', i) as string[]; + + method = 'POST'; + endpoint = `/gmail/v1/users/me/messages/${messageID}/modify`; + body = { + removeLabelIds: labelIds, + }; + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + if (operation === 'add') { + // https://developers.google.com/gmail/api/v1/reference/users/messages/modify + const messageID = this.getNodeParameter('messageId', i); + const labelIds = this.getNodeParameter('labelIds', i) as string[]; + + method = 'POST'; + endpoint = `/gmail/v1/users/me/messages/${messageID}/modify`; + + body = { + addLabelIds: labelIds, + }; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + } + if (resource === 'message') { + if (operation === 'send') { + // https://developers.google.com/gmail/api/v1/reference/users/messages/send + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + let toStr = ''; + let ccStr = ''; + let bccStr = ''; + let attachmentsList: IDataObject[] = []; + + const toList = this.getNodeParameter('toList', i) as IDataObject[]; + + toList.forEach((email) => { + toStr += `<${email}>, `; + }); + + if (additionalFields.ccList) { + const ccList = additionalFields.ccList as IDataObject[]; + + ccList.forEach((email) => { + ccStr += `<${email}>, `; + }); + } + + if (additionalFields.bccList) { + const bccList = additionalFields.bccList as IDataObject[]; + + bccList.forEach((email) => { + bccStr += `<${email}>, `; + }); + } + + if (additionalFields.attachmentsUi) { + const attachmentsUi = additionalFields.attachmentsUi as IDataObject; + const attachmentsBinary = []; + if (!isEmpty(attachmentsUi)) { + if (attachmentsUi.hasOwnProperty('attachmentsBinary') + && !isEmpty(attachmentsUi.attachmentsBinary) + && items[i].binary) { + // @ts-ignore + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + for (const binaryProperty of (property as string).split(',')) { + if (items[i].binary![binaryProperty] !== undefined) { + const binaryData = items[i].binary![binaryProperty]; + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty); + attachmentsBinary.push({ + name: binaryData.fileName || 'unknown', + content: binaryDataBuffer, + type: binaryData.mimeType, + }); + } + } + } + } + + qs = { + userId: 'me', + uploadType: 'media', + }; + attachmentsList = attachmentsBinary; + } + } + + const email: IEmail = { + from: additionalFields.senderName as string || '', + to: toStr, + cc: ccStr, + bcc: bccStr, + subject: this.getNodeParameter('subject', i) as string, + body: this.getNodeParameter('message', i) as string, + attachments: attachmentsList, + }; + + if (this.getNodeParameter('includeHtml', i, false) as boolean === true) { + email.htmlBody = this.getNodeParameter('htmlMessage', i) as string; + } + + endpoint = '/gmail/v1/users/me/messages/send'; + method = 'POST'; + + body = { + raw: await encodeEmail(email), + }; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + if (operation === 'reply') { + + const id = this.getNodeParameter('messageId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + let toStr = ''; + let ccStr = ''; + let bccStr = ''; + let attachmentsList: IDataObject[] = []; + + const toList = this.getNodeParameter('toList', i) as IDataObject[]; + + toList.forEach((email) => { + toStr += `<${email}>, `; + }); + + if (additionalFields.ccList) { + const ccList = additionalFields.ccList as IDataObject[]; + + ccList.forEach((email) => { + ccStr += `<${email}>, `; + }); + } + + if (additionalFields.bccList) { + const bccList = additionalFields.bccList as IDataObject[]; + + bccList.forEach((email) => { + bccStr += `<${email}>, `; + }); + } + + if (additionalFields.attachmentsUi) { + const attachmentsUi = additionalFields.attachmentsUi as IDataObject; + const attachmentsBinary = []; + if (!isEmpty(attachmentsUi)) { + if (attachmentsUi.hasOwnProperty('attachmentsBinary') + && !isEmpty(attachmentsUi.attachmentsBinary) + && items[i].binary) { + // @ts-ignore + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + for (const binaryProperty of (property as string).split(',')) { + if (items[i].binary![binaryProperty] !== undefined) { + const binaryData = items[i].binary![binaryProperty]; + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty); + attachmentsBinary.push({ + name: binaryData.fileName || 'unknown', + content: binaryDataBuffer, + type: binaryData.mimeType, + }); + } + } + } + } + + qs = { + userId: 'me', + uploadType: 'media', + }; + attachmentsList = attachmentsBinary; + } + } + + endpoint = `/gmail/v1/users/me/messages/${id}`; + + qs.format = 'metadata'; + + const { payload } = await googleApiRequest.call(this, method, endpoint, body, qs); + + if (toStr === '') { + for (const header of payload.headers as IDataObject[]) { + if (header.name === 'From') { + toStr = `<${extractEmail(header.value as string)}>,`; + break; + } + } + } + + const subject = payload.headers.filter((data: { [key: string]: string }) => data.name === 'Subject')[0]?.value || ''; + const references = payload.headers.filter((data: { [key: string]: string }) => data.name === 'References')[0]?.value || ''; + + const email: IEmail = { + from: additionalFields.senderName as string || '', + to: toStr, + cc: ccStr, + bcc: bccStr, + subject, + body: this.getNodeParameter('message', i) as string, + attachments: attachmentsList, + }; + + if (this.getNodeParameter('includeHtml', i, false) as boolean === true) { + email.htmlBody = this.getNodeParameter('htmlMessage', i) as string; + } + + endpoint = '/gmail/v1/users/me/messages/send'; + method = 'POST'; + + email.inReplyTo = id; + email.reference = references; + + body = { + raw: await encodeEmail(email), + threadId: this.getNodeParameter('threadId', i) as string, + }; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + if (operation === 'get') { + //https://developers.google.com/gmail/api/v1/reference/users/messages/get + method = 'GET'; + + const id = this.getNodeParameter('messageId', i); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const format = additionalFields.format || 'resolved'; + + if (format === 'resolved') { + qs.format = 'raw'; + } else { + qs.format = format; + } + + endpoint = `/gmail/v1/users/me/messages/${id}`; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + + let nodeExecutionData: INodeExecutionData; + if (format === 'resolved') { + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + + nodeExecutionData = await parseRawEmail.call(this, responseData, dataPropertyNameDownload); + } else { + nodeExecutionData = { + json: responseData, + }; + } + + responseData = nodeExecutionData; + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + if (qs.labelIds) { + // tslint:disable-next-line: triple-equals + if (qs.labelIds == '') { + delete qs.labelIds; + } else { + qs.labelIds = qs.labelIds as string[]; + } + } + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'messages', + 'GET', + `/gmail/v1/users/me/messages`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/messages`, + {}, + qs, + ); + responseData = responseData.messages; + } + + if (responseData === undefined) { + responseData = []; + } + + const format = additionalFields.format || 'resolved'; + + if (format !== 'ids') { + + if (format === 'resolved') { + qs.format = 'raw'; + } else { + qs.format = format; + } + + for (let i = 0; i < responseData.length; i++) { + responseData[i] = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/messages/${responseData[i].id}`, + body, + qs, + ); + + if (format === 'resolved') { + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + + responseData[i] = await parseRawEmail.call(this, responseData[i], dataPropertyNameDownload); + } + } + } + + if (format !== 'resolved') { + responseData = this.helpers.returnJsonArray(responseData); + } + + } + if (operation === 'delete') { + // https://developers.google.com/gmail/api/v1/reference/users/messages/delete + method = 'DELETE'; + const id = this.getNodeParameter('messageId', i); + + endpoint = `/gmail/v1/users/me/messages/${id}`; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + + responseData = { success: true }; + } + } + if (resource === 'draft') { + if (operation === 'create') { + // https://developers.google.com/gmail/api/v1/reference/users/drafts/create + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + let toStr = ''; + let ccStr = ''; + let bccStr = ''; + let attachmentsList: IDataObject[] = []; + + if (additionalFields.toList) { + const toList = additionalFields.toList as IDataObject[]; + + toList.forEach((email) => { + toStr += `<${email}>, `; + }); + } + + if (additionalFields.ccList) { + const ccList = additionalFields.ccList as IDataObject[]; + + ccList.forEach((email) => { + ccStr += `<${email}>, `; + }); + } + + if (additionalFields.bccList) { + const bccList = additionalFields.bccList as IDataObject[]; + + bccList.forEach((email) => { + bccStr += `<${email}>, `; + }); + } + + if (additionalFields.attachmentsUi) { + const attachmentsUi = additionalFields.attachmentsUi as IDataObject; + const attachmentsBinary = []; + if (!isEmpty(attachmentsUi)) { + if (!isEmpty(attachmentsUi)) { + if (attachmentsUi.hasOwnProperty('attachmentsBinary') + && !isEmpty(attachmentsUi.attachmentsBinary) + && items[i].binary) { + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + for (const binaryProperty of (property as string).split(',')) { + if (items[i].binary![binaryProperty] !== undefined) { + const binaryData = items[i].binary![binaryProperty]; + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty); + attachmentsBinary.push({ + name: binaryData.fileName || 'unknown', + content: binaryDataBuffer, + type: binaryData.mimeType, + }); + } + } + } + } + } + + qs = { + userId: 'me', + uploadType: 'media', + }; + + attachmentsList = attachmentsBinary; + } + } + + const email: IEmail = { + to: toStr, + cc: ccStr, + bcc: bccStr, + subject: this.getNodeParameter('subject', i) as string, + body: this.getNodeParameter('message', i) as string, + attachments: attachmentsList, + }; + + if (this.getNodeParameter('includeHtml', i, false) as boolean === true) { + email.htmlBody = this.getNodeParameter('htmlMessage', i) as string; + } + + endpoint = '/gmail/v1/users/me/drafts'; + method = 'POST'; + + body = { + message: { + raw: await encodeEmail(email), + }, + }; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + } + if (operation === 'get') { + // https://developers.google.com/gmail/api/v1/reference/users/drafts/get + method = 'GET'; + const id = this.getNodeParameter('messageId', i); + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const format = additionalFields.format || 'resolved'; + + if (format === 'resolved') { + qs.format = 'raw'; + } else { + qs.format = format; + } + + endpoint = `/gmail/v1/users/me/drafts/${id}`; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + + const binaryData: IBinaryKeyData = {}; + + let nodeExecutionData: INodeExecutionData; + if (format === 'resolved') { + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + + nodeExecutionData = await parseRawEmail.call(this, responseData.message, dataPropertyNameDownload); + + // Add the draft-id + nodeExecutionData.json.messageId = nodeExecutionData.json.id; + nodeExecutionData.json.id = responseData.id; + } else { + nodeExecutionData = { + json: responseData, + binary: Object.keys(binaryData).length ? binaryData : undefined, + }; + } + + responseData = nodeExecutionData; + } + if (operation === 'delete') { + // https://developers.google.com/gmail/api/v1/reference/users/drafts/delete + method = 'DELETE'; + const id = this.getNodeParameter('messageId', i); + + endpoint = `/gmail/v1/users/me/drafts/${id}`; + + responseData = await googleApiRequest.call(this, method, endpoint, body, qs); + + responseData = { success: true }; + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + Object.assign(qs, additionalFields); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'drafts', + 'GET', + `/gmail/v1/users/me/drafts`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/drafts`, + {}, + qs, + ); + responseData = responseData.drafts; + } + + if (responseData === undefined) { + responseData = []; + } + + const format = additionalFields.format || 'resolved'; + + if (format !== 'ids') { + if (format === 'resolved') { + qs.format = 'raw'; + } else { + qs.format = format; + } + + for (let i = 0; i < responseData.length; i++) { + + responseData[i] = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/drafts/${responseData[i].id}`, + body, + qs, + ); + + if (format === 'resolved') { + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + const id = responseData[i].id; + responseData[i] = await parseRawEmail.call(this, responseData[i].message, dataPropertyNameDownload); + + // Add the draft-id + responseData[i].json.messageId = responseData[i].json.id; + responseData[i].json.id = id; + } + } + } + + if (format !== 'resolved') { + responseData = this.helpers.returnJsonArray(responseData); + } + } + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v1/LabelDescription.ts similarity index 94% rename from packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts rename to packages/nodes-base/nodes/Google/Gmail/v1/LabelDescription.ts index 987e10951dfd4..1808633bc2f80 100644 --- a/packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v1/LabelDescription.ts @@ -15,25 +15,21 @@ export const labelOperations: INodeProperties[] = [ { name: 'Create', value: 'create', - description: 'Create a new label', action: 'Create a label', }, { name: 'Delete', value: 'delete', - description: 'Delete a label', action: 'Delete a label', }, { name: 'Get', value: 'get', - description: 'Get a label', action: 'Get a label', }, { name: 'Get Many', value: 'getAll', - description: 'Get all labels', action: 'Get all labels', }, ], @@ -155,7 +151,7 @@ export const labelFields: INodeProperties[] = [ minValue: 1, maxValue: 500, }, - default: 100, + default: 50, description: 'Max number of results to return', }, ]; diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v1/MessageDescription.ts similarity index 90% rename from packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts rename to packages/nodes-base/nodes/Google/Gmail/v1/MessageDescription.ts index 6b5093e7a3766..2c213ea57bada 100644 --- a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v1/MessageDescription.ts @@ -15,31 +15,26 @@ export const messageOperations: INodeProperties[] = [ { name: 'Delete', value: 'delete', - description: 'Delete a message', action: 'Delete a message', }, { name: 'Get', value: 'get', - description: 'Get a message', action: 'Get a message', }, { name: 'Get Many', value: 'getAll', - description: 'Get all messages', action: 'Get all messages', }, { name: 'Reply', value: 'reply', - description: 'Reply to an email', action: 'Reply to a message', }, { name: 'Send', value: 'send', - description: 'Send an email', action: 'Send a message', }, ], @@ -61,7 +56,6 @@ export const messageFields: INodeProperties[] = [ }, }, placeholder: '172ce2c4a72cc243', - description: 'The ID of the message you are operating on', }, { displayName: 'Thread ID', @@ -76,7 +70,6 @@ export const messageFields: INodeProperties[] = [ }, }, placeholder: '172ce2c4a72cc243', - description: 'The ID of the thread you are replying to', }, { displayName: 'Message ID', @@ -91,7 +84,6 @@ export const messageFields: INodeProperties[] = [ }, }, placeholder: 'CAHNQoFsC6JMMbOBJgtjsqN0eEc+gDg2a=SQj-tWUebQeHMDgqQ@mail.gmail.com', - description: 'The ID of the message you are replying to', }, { displayName: 'Subject', @@ -106,7 +98,6 @@ export const messageFields: INodeProperties[] = [ }, }, placeholder: 'Hello World!', - description: 'The message subject', }, { displayName: 'HTML', @@ -183,9 +174,9 @@ export const messageFields: INodeProperties[] = [ default: {}, options: [ { - displayName: 'Attachments', + displayName: 'Attachment', name: 'attachmentsUi', - placeholder: 'Add Attachments', + placeholder: 'Add Attachment', type: 'fixedCollection', typeOptions: { multipleValues: true, @@ -193,15 +184,15 @@ export const messageFields: INodeProperties[] = [ options: [ { name: 'attachmentsBinary', - displayName: 'Attachments Binary', + displayName: 'Attachment Binary', values: [ { - displayName: 'Property', + displayName: 'Attachment Field Name (in Input)', name: 'property', type: 'string', default: '', description: - 'Name of the binary property containing the data to be added to the email as an attachment. Multiple properties can be set separated by comma.', + 'Add the field name from the input node. Multiple properties can be set separated by comma.', }, ], }, @@ -234,13 +225,13 @@ export const messageFields: INodeProperties[] = [ default: [], }, { - displayName: 'Sender Name', + displayName: 'Override Sender Name', name: 'senderName', type: 'string', placeholder: 'Name ', default: '', description: - 'The name displayed in your contacts inboxes. It has to be in the format: "Display-Name <name@gmail.com>". The email address has to match the email address of the logged in user for the API', + 'The name displayed in your contacts inboxes. It has to be in the format: "Display-Name <name@gmail.com>". The email address has to match the email address of the logged in user for the API.', }, ], }, @@ -296,7 +287,7 @@ export const messageFields: INodeProperties[] = [ description: 'The format to return the message in', }, { - displayName: 'Attachments Prefix', + displayName: 'Attachment Prefix', name: 'dataPropertyAttachmentsPrefixName', type: 'string', default: 'attachment_', @@ -359,7 +350,7 @@ export const messageFields: INodeProperties[] = [ }, options: [ { - displayName: 'Attachments Prefix', + displayName: 'Attachment Prefix', name: 'dataPropertyAttachmentsPrefixName', type: 'string', default: 'attachment_', @@ -369,7 +360,7 @@ export const messageFields: INodeProperties[] = [ }, }, 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"', + '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: 'Format', @@ -415,7 +406,7 @@ export const messageFields: INodeProperties[] = [ description: 'The format to return the message in', }, { - displayName: 'Include Spam Trash', + displayName: 'Include Spam and Trash', name: 'includeSpamTrash', type: 'boolean', default: false, diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v1/MessageLabelDescription.ts similarity index 89% rename from packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts rename to packages/nodes-base/nodes/Google/Gmail/v1/MessageLabelDescription.ts index b3a63f331b399..3e95ba6baecad 100644 --- a/packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v1/MessageLabelDescription.ts @@ -15,13 +15,11 @@ export const messageLabelOperations: INodeProperties[] = [ { name: 'Add', value: 'add', - description: 'Add a label to a message', action: 'Add a label to a message', }, { name: 'Remove', value: 'remove', - description: 'Remove a label from a message', action: 'Remove a label from a message', }, ], @@ -43,7 +41,6 @@ export const messageLabelFields: INodeProperties[] = [ }, }, placeholder: '172ce2c4a72cc243', - description: 'The message ID of your email', }, { displayName: 'Label Names or IDs', diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/DraftDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v2/DraftDescription.ts new file mode 100644 index 0000000000000..80664fd5c875c --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v2/DraftDescription.ts @@ -0,0 +1,277 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const draftOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['draft'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a draft', + }, + { + name: 'Delete', + value: 'delete', + action: 'Delete a draft', + }, + { + name: 'Get', + value: 'get', + action: 'Get a draft', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get all drafts', + }, + ], + default: 'create', + }, +]; + +export const draftFields: INodeProperties[] = [ + { + displayName: 'Draft ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['draft'], + operation: ['delete', 'get'], + }, + }, + placeholder: 'r-3254521568507167962', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['draft'], + operation: ['create'], + }, + }, + placeholder: 'Hello World!', + }, + { + displayName: 'Email Type', + name: 'emailType', + type: 'options', + default: 'text', + required: true, + noDataExpression: true, + options: [ + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'text', + }, + ], + displayOptions: { + show: { + resource: ['draft'], + operation: ['create'], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['draft'], + operation: ['create'], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: ['draft'], + operation: ['create'], + }, + }, + default: {}, + options: [ + { + displayName: 'To Email', + name: 'sendTo', + type: 'string', + default: '', + placeholder: 'info@example.com', + description: + 'The email addresses of the recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + }, + { + displayName: 'BCC', + name: 'bccList', + type: 'string', + description: + 'The email addresses of the blind copy recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + placeholder: 'info@example.com', + default: '', + }, + { + displayName: 'CC', + name: 'ccList', + type: 'string', + description: + 'The email addresses of the copy recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + placeholder: 'info@example.com', + default: '', + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachment', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachmentsBinary', + displayName: 'Attachment Binary', + values: [ + { + displayName: 'Attachment Field Name (in Input)', + name: 'property', + type: 'string', + default: '', + description: + 'Add the field name from the input node. Multiple properties can be set separated by comma.', + }, + ], + }, + ], + default: {}, + description: 'Array of supported attachments to add to the message', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: ['draft'], + operation: ['get'], + }, + }, + default: {}, + 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, + description: "Whether the draft's attachments will be downloaded", + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* draft:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['draft'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['draft'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['draft'], + }, + }, + options: [ + { + displayName: 'Attachment Prefix', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + 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: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + description: "Whether the draft's attachments will be downloaded", + }, + { + displayName: 'Include Spam and Trash', + name: 'includeSpamTrash', + type: 'boolean', + default: false, + description: 'Whether to include messages from SPAM and TRASH in the results', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts new file mode 100644 index 0000000000000..7a6dbb9398835 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -0,0 +1,812 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import { IExecuteFunctions } from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + encodeEmail, + googleApiRequest, + googleApiRequestAllItems, + IEmail, + parseRawEmail, + prepareEmailAttachments, + prepareEmailBody, + prepareEmailsInput, + prepareQuery, + replayToEmail, + simplifyOutput, + unescapeSnippets, +} from '../GenericFunctions'; + +import { messageFields, messageOperations } from './MessageDescription'; + +import { labelFields, labelOperations } from './LabelDescription'; + +import { draftFields, draftOperations } from './DraftDescription'; + +import { threadFields, threadOperations } from './ThreadDescription'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Gmail', + name: 'gmail', + icon: 'file:gmail.svg', + group: ['transform'], + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Gmail API', + defaults: { + name: 'Gmail', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleApi', + required: true, + displayOptions: { + show: { + authentication: ['serviceAccount'], + }, + }, + }, + { + name: 'gmailOAuth2', + required: true, + displayOptions: { + show: { + authentication: ['oAuth2'], + }, + }, + }, + ], + 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: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Message', + value: 'message', + }, + { + name: 'Label', + value: 'label', + }, + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Thread', + value: 'thread', + }, + ], + default: 'message', + }, + //------------------------------- + // Draft Operations + //------------------------------- + ...draftOperations, + ...draftFields, + //------------------------------- + // Label Operations + //------------------------------- + ...labelOperations, + ...labelFields, + //------------------------------- + // Message Operations + //------------------------------- + ...messageOperations, + ...messageFields, + //------------------------------- + // Thread Operations + //------------------------------- + ...threadOperations, + ...threadFields, + //------------------------------- + ], +}; + +export class GmailV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + // Get all the labels to display them to user so that he can + // select them easily + async getLabels(this: ILoadOptionsFunctions): Promise { + 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; + }); + }, + + async getThreadMessages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const id = this.getNodeParameter('threadId', 0) as string; + const { messages } = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/threads/${id}`, + {}, + { format: 'minimal' }, + ); + + for (const message of messages || []) { + returnData.push({ + name: message.snippet, + value: message.id, + }); + } + + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + + for (let i = 0; i < items.length; i++) { + try { + //------------------------------------------------------------------// + // labels // + //------------------------------------------------------------------// + if (resource === 'label') { + if (operation === 'create') { + //https://developers.google.com/gmail/api/v1/reference/users/labels/create + const labelName = this.getNodeParameter('name', i) as string; + const labelListVisibility = this.getNodeParameter( + 'options.labelListVisibility', + i, + 'labelShow', + ) as string; + const messageListVisibility = this.getNodeParameter( + 'options.messageListVisibility', + i, + 'show', + ) as string; + + const body = { + labelListVisibility, + messageListVisibility, + name: labelName, + }; + + responseData = await googleApiRequest.call( + this, + 'POST', + '/gmail/v1/users/me/labels', + body, + ); + } + if (operation === 'delete') { + //https://developers.google.com/gmail/api/v1/reference/users/labels/delete + const labelId = this.getNodeParameter('labelId', i) as string[]; + const endpoint = `/gmail/v1/users/me/labels/${labelId}`; + + responseData = await googleApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + } + if (operation === 'get') { + // https://developers.google.com/gmail/api/v1/reference/users/labels/get + const labelId = this.getNodeParameter('labelId', i); + const endpoint = `/gmail/v1/users/me/labels/${labelId}`; + + responseData = await googleApiRequest.call(this, 'GET', endpoint); + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await googleApiRequest.call(this, 'GET', `/gmail/v1/users/me/labels`); + + responseData = this.helpers.returnJsonArray(responseData.labels); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + } + //------------------------------------------------------------------// + // messages // + //------------------------------------------------------------------// + if (resource === 'message') { + if (operation === 'send') { + // https://developers.google.com/gmail/api/v1/reference/users/messages/send + const options = this.getNodeParameter('options', i) as IDataObject; + const sendTo = this.getNodeParameter('sendTo', i) as string; + let qs: IDataObject = {}; + + const to = prepareEmailsInput.call(this, sendTo, 'To', i); + let cc = ''; + let bcc = ''; + + if (options.ccList) { + cc = prepareEmailsInput.call(this, options.ccList as string, 'CC', i); + } + + if (options.bccList) { + bcc = prepareEmailsInput.call(this, options.bccList as string, 'BCC', i); + } + + let attachments: IDataObject[] = []; + + if (options.attachmentsUi) { + attachments = await prepareEmailAttachments.call( + this, + options.attachmentsUi as IDataObject, + items, + i, + ); + if (attachments.length) { + qs = { + userId: 'me', + uploadType: 'media', + }; + } + } + + let from = ''; + if (options.senderName) { + const { emailAddress } = await googleApiRequest.call( + this, + 'GET', + '/gmail/v1/users/me/profile', + ); + from = `${options.senderName as string} <${emailAddress}>`; + } + + const email: IEmail = { + from, + to, + cc, + bcc, + subject: this.getNodeParameter('subject', i) as string, + ...prepareEmailBody.call(this, i), + attachments, + }; + + const endpoint = '/gmail/v1/users/me/messages/send'; + + const body = { + raw: await encodeEmail(email), + }; + + responseData = await googleApiRequest.call(this, 'POST', endpoint, body, qs); + } + if (operation === 'reply') { + const messageIdGmail = this.getNodeParameter('messageId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + responseData = await replayToEmail.call(this, items, messageIdGmail, options, i); + } + if (operation === 'get') { + //https://developers.google.com/gmail/api/v1/reference/users/messages/get + const id = this.getNodeParameter('messageId', i); + const endpoint = `/gmail/v1/users/me/messages/${id}`; + const qs: IDataObject = {}; + + const options = this.getNodeParameter('options', i, {}) as IDataObject; + const simple = this.getNodeParameter('simple', i) as boolean; + + if (simple) { + qs.format = 'metadata'; + qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; + } else { + qs.format = 'raw'; + } + + responseData = await googleApiRequest.call(this, 'GET', endpoint, {}, qs); + + let nodeExecutionData: INodeExecutionData; + if (!simple) { + const dataPropertyNameDownload = + (options.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; + + nodeExecutionData = await parseRawEmail.call( + this, + responseData, + dataPropertyNameDownload, + ); + } else { + const [json, _] = await simplifyOutput.call(this, [responseData]); + nodeExecutionData = { json }; + } + + responseData = [nodeExecutionData]; + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i, {}) as IDataObject; + const filters = this.getNodeParameter('filters', i, {}) as IDataObject; + const qs: IDataObject = {}; + Object.assign(qs, prepareQuery.call(this, filters), options); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'messages', + 'GET', + `/gmail/v1/users/me/messages`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/messages`, + {}, + qs, + ); + responseData = responseData.messages; + } + + if (responseData === undefined) { + responseData = []; + } + + const simple = this.getNodeParameter('simple', i) as boolean; + + if (simple) { + qs.format = 'metadata'; + qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; + } else { + qs.format = 'raw'; + } + + for (let i = 0; i < responseData.length; i++) { + responseData[i] = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/messages/${responseData[i].id}`, + {}, + qs, + ); + + if (!simple) { + const dataPropertyNameDownload = + (options.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; + + responseData[i] = await parseRawEmail.call( + this, + responseData[i], + dataPropertyNameDownload, + ); + } + } + + if (simple) { + responseData = this.helpers.returnJsonArray( + await simplifyOutput.call(this, responseData), + ); + } + } + if (operation === 'delete') { + // https://developers.google.com/gmail/api/v1/reference/users/messages/delete + const id = this.getNodeParameter('messageId', i); + const endpoint = `/gmail/v1/users/me/messages/${id}`; + + responseData = await googleApiRequest.call(this, 'DELETE', endpoint); + + responseData = { success: true }; + } + if (operation === 'markAsRead') { + // https://developers.google.com/gmail/api/reference/rest/v1/users.messages/modify + const id = this.getNodeParameter('messageId', i); + const endpoint = `/gmail/v1/users/me/messages/${id}/modify`; + + const body = { + removeLabelIds: ['UNREAD'], + }; + + responseData = await googleApiRequest.call(this, 'POST', endpoint, body); + } + + if (operation === 'markAsUnread') { + // https://developers.google.com/gmail/api/reference/rest/v1/users.messages/modify + const id = this.getNodeParameter('messageId', i); + const endpoint = `/gmail/v1/users/me/messages/${id}/modify`; + + const body = { + addLabelIds: ['UNREAD'], + }; + + responseData = await googleApiRequest.call(this, 'POST', endpoint, body); + } + + if (operation === 'addLabels') { + const id = this.getNodeParameter('messageId', i); + const labelIds = this.getNodeParameter('labelIds', i) as string[]; + + const endpoint = `/gmail/v1/users/me/messages/${id}/modify`; + + const body = { + addLabelIds: labelIds, + }; + + responseData = await googleApiRequest.call(this, 'POST', endpoint, body); + } + if (operation === 'removeLabels') { + const id = this.getNodeParameter('messageId', i); + const labelIds = this.getNodeParameter('labelIds', i) as string[]; + + const endpoint = `/gmail/v1/users/me/messages/${id}/modify`; + + const body = { + removeLabelIds: labelIds, + }; + responseData = await googleApiRequest.call(this, 'POST', endpoint, body); + } + } + //------------------------------------------------------------------// + // drafts // + //------------------------------------------------------------------// + if (resource === 'draft') { + if (operation === 'create') { + // https://developers.google.com/gmail/api/v1/reference/users/drafts/create + const options = this.getNodeParameter('options', i) as IDataObject; + let qs: IDataObject = {}; + + let to = ''; + let cc = ''; + let bcc = ''; + + if (options.sendTo) { + to += prepareEmailsInput.call(this, options.sendTo as string, 'To', i); + } + + if (options.ccList) { + cc = prepareEmailsInput.call(this, options.ccList as string, 'CC', i); + } + + if (options.bccList) { + bcc = prepareEmailsInput.call(this, options.bccList as string, 'BCC', i); + } + + let attachments: IDataObject[] = []; + if (options.attachmentsUi) { + attachments = await prepareEmailAttachments.call( + this, + options.attachmentsUi as IDataObject, + items, + i, + ); + if (attachments.length) { + qs = { + userId: 'me', + uploadType: 'media', + }; + } + } + + const email: IEmail = { + to, + cc, + bcc, + subject: this.getNodeParameter('subject', i) as string, + ...prepareEmailBody.call(this, i), + attachments, + }; + + const body = { + message: { + raw: await encodeEmail(email), + }, + }; + + responseData = await googleApiRequest.call( + this, + 'POST', + '/gmail/v1/users/me/drafts', + body, + qs, + ); + } + if (operation === 'get') { + // https://developers.google.com/gmail/api/v1/reference/users/drafts/get + const id = this.getNodeParameter('messageId', i); + const endpoint = `/gmail/v1/users/me/drafts/${id}`; + const qs: IDataObject = {}; + + const options = this.getNodeParameter('options', i) as IDataObject; + qs.format = 'raw'; + + responseData = await googleApiRequest.call(this, 'GET', endpoint, {}, qs); + + let nodeExecutionData: INodeExecutionData; + + const dataPropertyNameDownload = + (options.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; + + nodeExecutionData = await parseRawEmail.call( + this, + responseData.message, + dataPropertyNameDownload, + ); + + // Add the draft-id + nodeExecutionData.json.messageId = nodeExecutionData.json.id; + nodeExecutionData.json.id = responseData.id; + + responseData = [nodeExecutionData]; + } + if (operation === 'delete') { + // https://developers.google.com/gmail/api/v1/reference/users/drafts/delete + const id = this.getNodeParameter('messageId', i); + const endpoint = `/gmail/v1/users/me/drafts/${id}`; + + responseData = await googleApiRequest.call(this, 'DELETE', endpoint); + + responseData = { success: true }; + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + const qs: IDataObject = {}; + Object.assign(qs, options); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'drafts', + 'GET', + `/gmail/v1/users/me/drafts`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/drafts`, + {}, + qs, + ); + responseData = responseData.drafts; + } + + if (responseData === undefined) { + responseData = []; + } + + qs.format = 'raw'; + + for (let i = 0; i < responseData.length; i++) { + responseData[i] = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/drafts/${responseData[i].id}`, + {}, + qs, + ); + + const dataPropertyNameDownload = + (options.dataPropertyAttachmentsPrefixName as string) || 'attachment_'; + const id = responseData[i].id; + responseData[i] = await parseRawEmail.call( + this, + responseData[i].message, + dataPropertyNameDownload, + ); + + // Add the draft-id + responseData[i].json.messageId = responseData[i].json.id; + responseData[i].json.id = id; + } + } + } + //------------------------------------------------------------------// + // threads // + //------------------------------------------------------------------// + if (resource === 'thread') { + if (operation === 'delete') { + //https://developers.google.com/gmail/api/reference/rest/v1/users.threads/delete + const id = this.getNodeParameter('threadId', i); + const endpoint = `/gmail/v1/users/me/threads/${id}`; + + responseData = await googleApiRequest.call(this, 'DELETE', endpoint); + + responseData = { success: true }; + } + if (operation === 'get') { + //https://developers.google.com/gmail/api/reference/rest/v1/users.threads/get + const id = this.getNodeParameter('threadId', i); + const endpoint = `/gmail/v1/users/me/threads/${id}`; + + const options = this.getNodeParameter('options', i) as IDataObject; + const onlyMessages = options.returnOnlyMessages || false; + const qs: IDataObject = {}; + + const simple = this.getNodeParameter('simple', i) as boolean; + + if (simple) { + qs.format = 'metadata'; + qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; + } else { + qs.format = 'full'; + } + + responseData = await googleApiRequest.call(this, 'GET', endpoint, {}, qs); + + if (onlyMessages) { + responseData = this.helpers.returnJsonArray( + await simplifyOutput.call(this, responseData.messages), + ); + } else { + responseData.messages = await simplifyOutput.call(this, responseData.messages); + responseData = [{ json: responseData }]; + } + } + if (operation === 'getAll') { + //https://developers.google.com/gmail/api/reference/rest/v1/users.threads/list + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + const qs: IDataObject = {}; + Object.assign(qs, prepareQuery.call(this, filters)); + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'threads', + 'GET', + `/gmail/v1/users/me/threads`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/gmail/v1/users/me/threads`, + {}, + qs, + ); + responseData = responseData.threads; + } + + if (responseData === undefined) { + responseData = []; + } + + responseData = this.helpers.returnJsonArray(responseData); + } + if (operation === 'reply') { + const messageIdGmail = this.getNodeParameter('messageId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + responseData = await replayToEmail.call(this, items, messageIdGmail, options, i); + } + if (operation === 'trash') { + //https://developers.google.com/gmail/api/reference/rest/v1/users.threads/trash + const id = this.getNodeParameter('threadId', i); + const endpoint = `/gmail/v1/users/me/threads/${id}/trash`; + + responseData = await googleApiRequest.call(this, 'POST', endpoint); + } + if (operation === 'untrash') { + //https://developers.google.com/gmail/api/reference/rest/v1/users.threads/untrash + const id = this.getNodeParameter('threadId', i); + + const endpoint = `/gmail/v1/users/me/threads/${id}/untrash`; + + responseData = await googleApiRequest.call(this, 'POST', endpoint); + } + if (operation === 'addLabels') { + const id = this.getNodeParameter('threadId', i); + const labelIds = this.getNodeParameter('labelIds', i) as string[]; + + const endpoint = `/gmail/v1/users/me/threads/${id}/modify`; + + const body = { + addLabelIds: labelIds, + }; + + responseData = await googleApiRequest.call(this, 'POST', endpoint, body); + } + if (operation === 'removeLabels') { + const id = this.getNodeParameter('threadId', i); + const labelIds = this.getNodeParameter('labelIds', i) as string[]; + + const endpoint = `/gmail/v1/users/me/threads/${id}/modify`; + + const body = { + removeLabelIds: labelIds, + }; + responseData = await googleApiRequest.call(this, 'POST', endpoint, body); + } + } + //------------------------------------------------------------------// + + const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { + itemData: { item: i }, + }); + returnData.push(...executionData); + } catch (error) { + error.message = `${error.message} (item ${i})`; + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message }, pairedItem: { item: i } }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { + description: error.description, + itemIndex: i, + }); + } + } + if ( + ['draft', 'message', 'thread'].includes(resource) && + ['get', 'getAll'].includes(operation) + ) { + return this.prepareOutputData(unescapeSnippets(returnData)); + } + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/LabelDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v2/LabelDescription.ts new file mode 100644 index 0000000000000..cd318fc02b1b3 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v2/LabelDescription.ts @@ -0,0 +1,159 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const labelOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['label'], + }, + }, + + options: [ + { + name: 'Create', + value: 'create', + action: 'Create a label', + }, + { + name: 'Delete', + value: 'delete', + action: 'Delete a label', + }, + { + name: 'Get', + value: 'get', + action: 'Get a label info', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get all labels', + }, + ], + default: 'getAll', + }, +]; + +export const labelFields: INodeProperties[] = [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['label'], + operation: ['create'], + }, + }, + placeholder: 'invoices', + description: 'Label Name', + }, + { + displayName: 'Label ID', + name: 'labelId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['label'], + operation: ['get', 'delete'], + }, + }, + description: 'The ID of the label', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: ['label'], + operation: ['create'], + }, + }, + default: {}, + options: [ + { + displayName: 'Label List Visibility', + name: 'labelListVisibility', + type: 'options', + options: [ + { + name: 'Hide', + value: 'labelHide', + }, + { + name: 'Show', + value: 'labelShow', + }, + { + name: 'Show If Unread', + value: 'labelShowIfUnread', + }, + ], + default: 'labelShow', + description: 'The visibility of the label in the label list in the Gmail web interface', + }, + { + displayName: 'Message List Visibility', + name: 'messageListVisibility', + type: 'options', + options: [ + { + name: 'Hide', + value: 'hide', + }, + { + name: 'Show', + value: 'show', + }, + ], + default: 'show', + description: + 'The visibility of messages with this label in the message list in the Gmail web interface', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* label:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['label'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['label'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'Max number of results to return', + }, +]; diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts new file mode 100644 index 0000000000000..c7e873a3f7bb3 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts @@ -0,0 +1,506 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const messageOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['message'], + }, + }, + options: [ + { + name: 'Add Label', + value: 'addLabels', + action: 'Add label to message', + }, + { + name: 'Delete', + value: 'delete', + action: 'Delete a message', + }, + { + name: 'Get', + value: 'get', + action: 'Get a message', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get all messages', + }, + { + name: 'Mark as Read', + value: 'markAsRead', + action: 'Mark a message as read', + }, + { + name: 'Mark as Unread', + value: 'markAsUnread', + action: 'Mark a message as unread', + }, + { + name: 'Remove Label', + value: 'removeLabels', + action: 'Remove label from message', + }, + { + name: 'Reply', + value: 'reply', + action: 'Reply to a message', + }, + { + name: 'Send', + value: 'send', + action: 'Send a message', + }, + ], + default: 'send', + }, +]; + +export const messageFields: INodeProperties[] = [ + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['get', 'delete', 'markAsRead', 'markAsUnread'], + }, + }, + placeholder: '172ce2c4a72cc243', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['reply'], + }, + }, + placeholder: '172ce2c4a72cc243', + }, + { + displayName: 'To', + name: 'sendTo', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['send'], + }, + }, + placeholder: 'info@example.com', + description: + 'The email addresses of the recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['send'], + }, + }, + placeholder: 'Hello World!', + }, + { + displayName: 'Email Type', + name: 'emailType', + type: 'options', + default: 'text', + required: true, + noDataExpression: true, + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'HTML', + value: 'html', + }, + ], + displayOptions: { + show: { + resource: ['message'], + operation: ['send', 'reply'], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['reply', 'send'], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: ['message'], + operation: ['send', 'reply'], + }, + }, + default: {}, + options: [ + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachment', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachmentsBinary', + displayName: 'Attachment Binary', + values: [ + { + displayName: 'Attachment Field Name', + name: 'property', + type: 'string', + default: 'data', + description: + 'Add the field name from the input node. Multiple properties can be set separated by comma.', + hint: 'The name of the field with the attachment in the node input', + }, + ], + }, + ], + default: {}, + description: 'Array of supported attachments to add to the message', + }, + { + displayName: 'BCC', + name: 'bccList', + type: 'string', + description: + 'The email addresses of the blind copy recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + placeholder: 'info@example.com', + default: '', + }, + { + displayName: 'CC', + name: 'ccList', + type: 'string', + description: + 'The email addresses of the copy recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + placeholder: 'info@example.com', + default: '', + }, + { + displayName: 'Sender Name', + name: 'senderName', + type: 'string', + placeholder: 'e.g. Nathan', + default: '', + description: "The name that will be shown in recipients' inboxes", + }, + { + displayName: 'Reply to Sender Only', + name: 'replyToSenderOnly', + type: 'boolean', + default: false, + description: 'Whether to reply to the sender only or to the entire list of recipients', + }, + ], + }, + { + displayName: 'Simplify', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + operation: ['get'], + resource: ['message'], + }, + }, + default: true, + description: 'Whether to return a simplified version of the response instead of the raw data', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: ['message'], + operation: ['get'], + }, + hide: { + simple: [true], + }, + }, + default: {}, + 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, + description: + "Whether the email's attachments will be downloaded and included in the output", + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* message:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['message'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['message'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Simplify', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['message'], + }, + }, + default: true, + description: 'Whether to return a simplified version of the response instead of the raw data', + }, + { + displayName: + 'Fetching a lot of messages may take a long time. Consider using filters to speed things up', + name: 'filtersNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['message'], + returnAll: [true], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['message'], + }, + }, + options: [ + { + displayName: 'Include Spam and Trash', + name: 'includeSpamTrash', + type: 'boolean', + default: false, + description: 'Whether to include messages from SPAM and TRASH in the results', + }, + { + displayName: 'Label Names or IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + description: + 'Only return messages with labels that match all of the specified label IDs. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'Search', + name: 'q', + type: 'string', + default: '', + placeholder: 'has:attachment', + hint: 'Use the same format as in the Gmail search box. More info.', + 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: 'Received After', + name: 'receivedAfter', + type: 'dateTime', + default: '', + description: + 'Get all emails received after the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + displayName: 'Received Before', + name: 'receivedBefore', + type: 'dateTime', + default: '', + description: + 'Get all emails received before the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + 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', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['message'], + }, + 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, + description: + "Whether the email's attachments will be downloaded and included in the output", + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* label:addLabel, removeLabel */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + placeholder: '172ce2c4a72cc243', + displayOptions: { + show: { + resource: ['message'], + operation: ['addLabels', 'removeLabels'], + }, + }, + }, + { + displayName: 'Label Names or IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + required: true, + displayOptions: { + show: { + resource: ['message'], + operation: ['addLabels', 'removeLabels'], + }, + }, + description: + 'Choose from the list, or specify IDs using an expression', + }, +]; diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/ThreadDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v2/ThreadDescription.ts new file mode 100644 index 0000000000000..f02290d7d4946 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/v2/ThreadDescription.ts @@ -0,0 +1,415 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const threadOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['thread'], + }, + }, + options: [ + { + name: 'Add Label', + value: 'addLabels', + action: 'Add label to thread', + }, + { + name: 'Delete', + value: 'delete', + action: 'Delete a thread', + }, + { + name: 'Get', + value: 'get', + action: 'Get a thread', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get all threads', + }, + { + name: 'Remove Label', + value: 'removeLabels', + action: 'Remove label from thread', + }, + { + name: 'Reply', + value: 'reply', + action: 'Reply to a message', + }, + { + name: 'Trash', + value: 'trash', + action: 'Trash a thread', + }, + { + name: 'Untrash', + value: 'untrash', + action: 'Untrash a thread', + }, + ], + default: 'getAll', + }, +]; + +export const threadFields: INodeProperties[] = [ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + required: true, + description: 'The ID of the thread you are operating on', + displayOptions: { + show: { + resource: ['thread'], + operation: ['get', 'delete', 'reply', 'trash', 'untrash'], + }, + }, + }, + + /* -------------------------------------------------------------------------- */ + /* thread:reply */ + /* -------------------------------------------------------------------------- */ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Message Snippet or ID', + name: 'messageId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getThreadMessages', + loadOptionsDependsOn: ['threadId'], + }, + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + displayOptions: { + show: { + resource: ['thread'], + operation: ['reply'], + }, + }, + }, + { + displayName: 'Email Type', + name: 'emailType', + type: 'options', + default: 'text', + required: true, + noDataExpression: true, + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'HTML', + value: 'html', + }, + ], + displayOptions: { + show: { + resource: ['thread'], + operation: ['reply'], + }, + }, + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: ['thread'], + operation: ['reply'], + }, + }, + hint: 'Get better Text and Expressions writing experience by using the expression editor', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + resource: ['thread'], + operation: ['reply'], + }, + }, + default: {}, + options: [ + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachment', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachmentsBinary', + displayName: 'Attachment Binary', + values: [ + { + displayName: 'Attachment Field Name', + name: 'property', + type: 'string', + default: '', + description: + 'Add the field name from the input node. Multiple properties can be set separated by comma.', + }, + ], + }, + ], + default: {}, + description: 'Array of supported attachments to add to the message', + }, + { + displayName: 'BCC', + name: 'bccList', + type: 'string', + description: + 'The email addresses of the blind copy recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + placeholder: 'info@example.com', + default: '', + }, + { + displayName: 'CC', + name: 'ccList', + type: 'string', + description: + 'The email addresses of the copy recipients. Multiple addresses can be separated by a comma. e.g. jay@getsby.com, jon@smith.com.', + placeholder: 'info@example.com', + default: '', + }, + { + displayName: 'Sender Name', + name: 'senderName', + type: 'string', + placeholder: 'e.g. Nathan', + default: '', + description: 'The name displayed in your contacts inboxes', + }, + { + displayName: 'Reply to Sender Only', + name: 'replyToSenderOnly', + type: 'boolean', + default: false, + description: 'Whether to reply to the sender only or to the entire list of recipients', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* thread:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Simplify', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + operation: ['get'], + resource: ['thread'], + }, + }, + default: true, + description: 'Whether to return a simplified version of the response instead of the raw data', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: ['thread'], + operation: ['get'], + }, + }, + default: {}, + options: [ + { + displayName: 'Return Only Messages', + name: 'returnOnlyMessages', + type: 'boolean', + default: true, + description: 'Whether to return only thread messages', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* thread:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['thread'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['thread'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: + 'Fetching a lot of messages may take a long time. Consider using filters to speed things up', + name: 'filtersNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + operation: ['getAll'], + resource: ['thread'], + returnAll: [true], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + operation: ['getAll'], + resource: ['thread'], + }, + }, + options: [ + { + displayName: 'Include Spam and Trash', + name: 'includeSpamTrash', + type: 'boolean', + default: false, + description: 'Whether to include threads from SPAM and TRASH in the results', + }, + { + displayName: 'Label ID Names or IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + description: + 'Only return threads with labels that match all of the specified label IDs. Choose from the list, or specify IDs using an expression.', + }, + { + displayName: 'Search', + name: 'q', + type: 'string', + default: '', + placeholder: 'has:attachment', + hint: 'Use the same format as in the Gmail search box. More info.', + 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: 'Received After', + name: 'receivedAfter', + type: 'dateTime', + default: '', + description: + 'Get all emails received after the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + { + displayName: 'Received Before', + name: 'receivedBefore', + type: 'dateTime', + default: '', + description: + 'Get all emails received before the specified date. In an expression you can set date using string in ISO format or a timestamp in miliseconds.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* label:addLabel, removeLabel */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + required: true, + placeholder: '172ce2c4a72cc243', + displayOptions: { + show: { + resource: ['thread'], + operation: ['addLabels', 'removeLabels'], + }, + }, + }, + { + displayName: 'Label Names or IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + required: true, + displayOptions: { + show: { + resource: ['thread'], + operation: ['addLabels', 'removeLabels'], + }, + }, + description: + 'Choose from the list, or specify IDs using an expression', + }, +]; diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index d4dedc2024541..b30b2ae27c918 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -92,7 +92,8 @@ export class GoogleSheets implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts index 76250764e3406..3382b9555835a 100644 --- a/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts +++ b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts @@ -73,7 +73,8 @@ export class GoogleSlides implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/nodes/Google/Translate/GoogleTranslate.node.ts b/packages/nodes-base/nodes/Google/Translate/GoogleTranslate.node.ts index 870bdd151cbe5..8ce291e321a34 100644 --- a/packages/nodes-base/nodes/Google/Translate/GoogleTranslate.node.ts +++ b/packages/nodes-base/nodes/Google/Translate/GoogleTranslate.node.ts @@ -78,7 +78,8 @@ export class GoogleTranslate implements INodeType { type: 'options', options: [ { - name: 'OAuth2 (Recommended)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'OAuth2 (recommended)', value: 'oAuth2', }, { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 570a3618d7fa2..7a153cb52efec 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -459,6 +459,7 @@ "dist/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.js", "dist/nodes/Google/Firebase/RealtimeDatabase/GoogleFirebaseRealtimeDatabase.node.js", "dist/nodes/Google/Gmail/Gmail.node.js", + "dist/nodes/Google/Gmail/GmailTrigger.node.js", "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", "dist/nodes/Google/Perspective/GooglePerspective.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js",