From fb042adae2b83a934232fae1b8daef9c52b1169c Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 18 Aug 2020 12:40:19 +0200 Subject: [PATCH] :sparkles: Gmail node (#856) * :sparkles: Gmail node * :zap: Improvements * :zap: Improvements * :zap: Improvements to Gmail-Node * :zap: Additional improvements to Gmail-Node Co-authored-by: Erin Co-authored-by: ricardo --- .../credentials/GmailOAuth2Api.credentials.ts | 30 + .../nodes/Google/Gmail/DraftDescription.ts | 382 +++++++++ .../nodes/Google/Gmail/GenericFunctions.ts | 191 +++++ .../nodes/Google/Gmail/Gmail.node.ts | 755 ++++++++++++++++++ .../nodes/Google/Gmail/LabelDescription.ts | 187 +++++ .../nodes/Google/Gmail/MessageDescription.ts | 464 +++++++++++ .../Google/Gmail/MessageLabelDescription.ts | 77 ++ .../nodes-base/nodes/Google/Gmail/gmail.png | Bin 0 -> 3724 bytes packages/nodes-base/package.json | 4 + 9 files changed, 2090 insertions(+) create mode 100644 packages/nodes-base/credentials/GmailOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts create mode 100644 packages/nodes-base/nodes/Google/Gmail/gmail.png diff --git a/packages/nodes-base/credentials/GmailOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GmailOAuth2Api.credentials.ts new file mode 100644 index 0000000000..63fe6916f1 --- /dev/null +++ b/packages/nodes-base/credentials/GmailOAuth2Api.credentials.ts @@ -0,0 +1,30 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/gmail.labels', + 'https://www.googleapis.com/auth/gmail.addons.current.action.compose', + 'https://www.googleapis.com/auth/gmail.addons.current.message.action', + 'https://mail.google.com/', + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/gmail.compose', +]; + + +export class GmailOAuth2Api implements ICredentialType { + name = 'gmailOAuth2'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Gmail OAuth2 API'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts b/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts new file mode 100644 index 0000000000..0f80f90923 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts @@ -0,0 +1,382 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const draftOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'draft', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new email draft', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a draft', + }, + { + name: 'Get', + value: 'get', + description: 'Get a draft', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all drafts', + } + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const draftFields = [ + { + displayName: 'Draft ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'delete', + 'get', + ] + }, + }, + placeholder: 'r-3254521568507167962', + description: 'The ID of the draft to operate on.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'create', + ] + }, + }, + placeholder: 'Hello World!', + description: 'The message subject.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'create', + ] + }, + }, + placeholder: 'Hello World!', + description: 'The message body. This can be in HTML.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'create', + ] + }, + }, + default: {}, + options: [ + { + displayName: 'To Email', + name: 'toList', + type: 'string', + default: [], + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add To Email', + }, + placeholder: 'info@example.com', + description: 'The email addresses of the recipients.', + }, + { + displayName: 'CC Email', + name: 'ccList', + type: 'string', + description: 'The email addresses of the copy recipients.', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add CC Email', + }, + placeholder: 'info@example.com', + default: [], + }, + { + displayName: 'BCC Email', + name: 'bccList', + type: 'string', + description: 'The email addresses of the blind copy recipients.', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add BCC Email', + }, + placeholder: 'info@example.com', + default: [], + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: '', + description: 'Name of the binary property containing the data to be added to the email as an attachment', + }, + ], + }, + ], + default: '', + description: 'Array of supported attachments to add to the message.', + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'draft', + ], + operation: [ + 'get', + ] + }, + }, + default: {}, + options: [ + { + displayName: 'Attachments Prefix', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { + hide: { + format: [ + 'full', + 'metadata', + 'minimal', + 'raw', + ], + }, + }, + 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: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'Full', + value: 'full', + description: 'Returns the full email message data with body content parsed in the payload field', + }, + { + name: 'Metadata', + value: 'metadata', + description: 'Returns only email message ID, labels, and email headers.', + }, + { + name: 'Minimal', + value: 'minimal', + description: 'Returns only email message ID and labels; does not return the email headers, body, or payload', + }, + { + name: 'RAW', + value: 'raw', + description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.' + }, + { + name: 'Resolved', + value: 'resolved', + description: 'Returns the full email with all data resolved and attachments saved as binary data.', + }, + ], + default: 'resolved', + description: 'The format to return the message in', + }, + ] + }, + + /* -------------------------------------------------------------------------- */ + /* draft:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'draft', + ], + }, + }, + default: false, + description: 'If all results should be returned 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: 10, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'draft', + ], + }, + }, + options: [ + { + displayName: 'Attachments Prefix', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { + hide: { + format: [ + 'full', + 'ids', + 'metadata', + 'minimal', + 'raw', + ], + }, + }, + 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: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'Full', + value: 'full', + description: 'Returns the full email message data with body content parsed in the payload field', + }, + { + name: 'IDs', + value: 'ids', + description: 'Returns only the IDs of the emails', + }, + { + name: 'Metadata', + value: 'metadata', + description: 'Returns only email message ID, labels, and email headers.', + }, + { + name: 'Minimal', + value: 'minimal', + description: 'Returns only email message ID and labels; does not return the email headers, body, or payload', + }, + { + name: 'RAW', + value: 'raw', + description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.' + }, + { + name: 'Resolved', + value: 'resolved', + description: 'Returns the full email with all data resolved and attachments saved as binary data.', + }, + ], + default: 'resolved', + description: 'The format to return the message in', + }, + { + displayName: 'Include Spam Trash', + name: 'includeSpamTrash', + type: 'boolean', + default: false, + description: 'Include messages from SPAM and TRASH in the results.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts new file mode 100644 index 0000000000..eee0111d2a --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts @@ -0,0 +1,191 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + simpleParser, + Source as ParserSource, +} from 'mailparser'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, +} from 'n8n-workflow'; + +import { + IEmail, +} from './Gmail.node'; + +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, + endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUri = { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${endpoint}`, + json: true, + }; + + options = Object.assign({}, options, option); + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'gmailOAuth2', options); + + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + let errorMessages; + + if (error.response.body.error.errors) { + // Try to return the error prettier + errorMessages = error.response.body.error.errors; + + errorMessages = errorMessages.map((errorItem: IDataObject) => errorItem.message); + + errorMessages = errorMessages.join('|'); + + } else if (error.response.body.error.message) { + errorMessages = error.response.body.error.message; + } + + throw new Error(`Gmail error response [${error.statusCode}]: ${errorMessages}`); + } + throw error; + } +} + + +export async function parseRawEmail(this: IExecuteFunctions, messageEncoded: ParserSource, dataPropertyNameDownload: string): Promise { + + const responseData = await simpleParser(messageEncoded); + + const headers: IDataObject = {}; + for (const header of responseData.headerLines) { + headers[header.key] = header.line; + } + + // @ts-ignore + responseData.headers = headers; + // @ts-ignore + responseData.headerLines = undefined; + + const binaryData: IBinaryKeyData = {}; + if (responseData.attachments) { + + for (let i = 0; i < responseData.attachments.length; i++) { + const attachment = responseData.attachments[i]; + binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(attachment.content, attachment.filename, attachment.contentType); + } + // @ts-ignore + responseData.attachments = undefined; + } + + return { + json: responseData as unknown as IDataObject, + binary: Object.keys(binaryData).length ? binaryData : undefined, + } as INodeExecutionData; +} + + +//------------------------------------------------------------------------------------------------------------------------------------------ +// This function converts an email object into a MIME encoded email and then converts that string into base64 encoding +// for more info on MIME, https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/aa494197(v%3Dexchg.140) +//------------------------------------------------------------------------------------------------------------------------------------------ + +export function encodeEmail(email: IEmail) { + let mimeEmail = ''; + + if (email.attachments !== undefined && email.attachments !== []) { + const attachments = email.attachments.map((attachment) => { + return [ + "--XXXXboundary text\n", + "Content-Type:", attachment.type, ";\n", + "Content-Transfer-Encoding: Base64\n", + "Content-Disposition: attachment;\n", + "\tfilename=\"", attachment.name, "\"\n\n", + + attachment.content, "\n\n", + + "--XXXXboundary text\n\n", + ].join(''); + }); + + mimeEmail = [ + "To: ", email.to, "\n", + "Cc: ", email.cc, "\n", + "Bcc: ", email.bcc, "\n", + "In-Reply-To: ", email.inReplyTo, "\n", + "References: ", email.reference, "\n", + "Subject: ", email.subject, "\n", + "MIME-Version: 1.0\n", + "Content-Type: multipart/mixed;\n", + "\tboundary=\"XXXXboundary text\"\n\n", + + "This is a multipart message in MIME format.\n\n", + + "--XXXXboundary text\n", + "Content-Type: text/plain\n\n", + + email.body, "\n\n", + + attachments.join(''), + + "--XXXXboundary text--", + ].join(''); + } else { + mimeEmail = [ + "Content-Type: text/plain; charset=\"UTF-8\"\n", + "MIME-Version: 1.0\n", + "Content-Transfer-Encoding: 7bit\n", + "To: ", email.to, "\n", + "Cc: ", email.cc, "\n", + "Bcc: ", email.bcc, "\n", + "In-Reply-To: ", email.inReplyTo, "\n", + "References: ", email.reference, "\n", + "Subject: ", email.subject, "\n\n", + email.body, + ].join(''); + } + + return Buffer.from(mimeEmail).toString("base64").replace(/\+/g, '-').replace(/\//g, '_'); +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} + +export function extractEmail(s: string) { + const data = s.split('<')[1]; + return data.substring(0, data.length - 1); +} diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts new file mode 100644 index 0000000000..81645908bc --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts @@ -0,0 +1,755 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + ILoadOptionsFunctions, + IDataObject, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + encodeEmail, + extractEmail, + googleApiRequest, + googleApiRequestAllItems, + parseRawEmail, +} from './GenericFunctions'; + +import { + messageOperations, + messageFields, +} from './MessageDescription'; + +import { + messageLabelOperations, + messageLabelFields, +} from './MessageLabelDescription'; + +import { + labelOperations, + labelFields, +} from './LabelDescription'; + +import { + draftOperations, + draftFields, +} from './DraftDescription'; + +import { + isEmpty, +} from 'lodash'; + +export interface IEmail { + to?: string; + cc?: string; + bcc?: string; + inReplyTo?: string; + reference?: string; + subject: string; + body: 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.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Gmail API', + defaults: { + name: 'Gmail', + color: '#d93025', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'gmailOAuth2', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Draft', + value: 'draft', + }, + { + name: 'Label', + value: 'label', + }, + { + name: 'Message', + value: 'message', + }, + { + name: 'Message Label', + value: 'messageLabel', + }, + ], + default: 'draft', + description: 'The resource to operate on.', + }, + //------------------------------- + // 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: IDataObject[] = []; + 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++) { + 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; + let attachmentsBinary = []; + if (!isEmpty(attachmentsUi)) { + if (attachmentsUi.hasOwnProperty('attachmentsBinary') + && !isEmpty(attachmentsUi.attachmentsBinary) + && items[i].binary) { + // @ts-ignore + attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => { + if (items[i].binary!.hasOwnProperty(value.property)) { + const aux: IAttachments = { name: '', content: '', type: '' }; + aux.name = items[i].binary![value.property].fileName || 'unknown'; + aux.content = items[i].binary![value.property].data; + aux.type = items[i].binary![value.property].mimeType; + return aux; + } + }); + } + + 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, + }; + + endpoint = '/gmail/v1/users/me/messages/send'; + method = 'POST'; + + body = { + raw: 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; + let attachmentsBinary = []; + if (!isEmpty(attachmentsUi)) { + if (attachmentsUi.hasOwnProperty('attachmentsBinary') + && !isEmpty(attachmentsUi.attachmentsBinary) + && items[i].binary) { + // @ts-ignore + attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => { + if (items[i].binary!.hasOwnProperty(value.property)) { + const aux: IAttachments = { name: '', content: '', type: '' }; + aux.name = items[i].binary![value.property].fileName || 'unknown'; + aux.content = items[i].binary![value.property].data; + aux.type = items[i].binary![value.property].mimeType; + return aux; + } + }); + } + + 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 = { + to: toStr, + cc: ccStr, + bcc: bccStr, + subject: this.getNodeParameter('subject', i) as string, + body: this.getNodeParameter('message', i) as string, + attachments: attachmentsList, + }; + + endpoint = '/gmail/v1/users/me/messages/send'; + method = 'POST'; + + email.inReplyTo = id; + email.reference = id; + + body = { + raw: 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 messageEncoded = Buffer.from(responseData.raw, 'base64').toString('utf8'); + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + + nodeExecutionData = await parseRawEmail.call(this, messageEncoded, 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[]).join(','); + } + } + + 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 messageEncoded = Buffer.from(responseData[i].raw, 'base64').toString('utf8'); + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + responseData[i] = await parseRawEmail.call(this, messageEncoded, 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; + let attachmentsBinary = []; + if (!isEmpty(attachmentsUi)) { + if (attachmentsUi.hasOwnProperty('attachmentsBinary') + && !isEmpty(attachmentsUi.attachmentsBinary) + && items[i].binary) { + // @ts-ignore + attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => { + if (items[i].binary!.hasOwnProperty(value.property)) { + const aux: IAttachments = { name: '', content: '', type: '' }; + aux.name = items[i].binary![value.property].fileName || 'unknown'; + aux.content = items[i].binary![value.property].data; + aux.type = items[i].binary![value.property].mimeType; + return aux; + } + }); + } + + 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, + }; + + endpoint = '/gmail/v1/users/me/drafts'; + method = 'POST'; + + body = { + message: { + raw: 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 messageEncoded = Buffer.from(responseData.message.raw, 'base64').toString('utf8'); + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + + nodeExecutionData = await parseRawEmail.call(this, messageEncoded, dataPropertyNameDownload); + } 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 messageEncoded = Buffer.from(responseData[i].message.raw, 'base64').toString('utf8'); + const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; + responseData[i] = await parseRawEmail.call(this, messageEncoded, dataPropertyNameDownload); + } + } + } + + if (format !== 'resolved') { + responseData = this.helpers.returnJsonArray(responseData); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + if (['draft', 'message'].includes(resource) && ['get', 'getAll'].includes(operation)) { + //@ts-ignore + return this.prepareOutputData(returnData); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts b/packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts new file mode 100644 index 0000000000..7fddc17e9b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/LabelDescription.ts @@ -0,0 +1,187 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const labelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'label', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new label', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a label', + }, + { + name: 'Get', + value: 'get', + description: 'Get a label', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all labels', + } + ], + default: 'create', + description: 'The operation to perform', + }, +] as INodeProperties[]; + +export const labelFields = [ + { + 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: 'Label List Visibility', + name: 'labelListVisibility', + type: 'options', + options: [ + { + name: 'Hide', + value: 'labelHide', + }, + { + name: 'Show', + value: 'labelShow', + }, + { + name: 'Show If Unread', + value: 'labelShowIfUnread', + }, + ], + default: 'labelShow', + required: true, + displayOptions: { + show: { + resource: [ + 'label', + ], + operation: [ + 'create', + ], + }, + }, + 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', + required: true, + displayOptions: { + show: { + resource: [ + 'label', + ], + operation: [ + 'create', + ], + }, + }, + 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: 'If all results should be returned 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: 100, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts new file mode 100644 index 0000000000..cd23aa0fb3 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts @@ -0,0 +1,464 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const messageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send an email', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a message', + }, + { + name: 'Get', + value: 'get', + description: 'Get a message', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all messages', + }, + { + name: 'Reply', + value: 'reply', + description: 'Reply to an email', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const messageFields = [ + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'get', + 'delete', + ] + }, + }, + placeholder: '172ce2c4a72cc243', + description: 'The ID of the message you are operating on.', + }, + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + ] + }, + }, + placeholder: '172ce2c4a72cc243', + description: 'The ID of the thread you are replying to.', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + ] + }, + }, + placeholder: 'CAHNQoFsC6JMMbOBJgtjsqN0eEc+gDg2a=SQj-tWUebQeHMDgqQ@mail.gmail.com', + description: 'The ID of the message you are replying to.', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + 'send', + ] + }, + }, + placeholder: 'Hello World!', + description: 'The message subject.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + 'send', + ] + }, + }, + placeholder: 'Hello World!', + description: 'The message body. This can be in HTML.', + }, + { + displayName: 'To Email', + name: 'toList', + type: 'string', + default: [], + required: true, + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add To Email', + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'reply', + 'send', + ] + }, + }, + placeholder: 'info@example.com', + description: 'The email addresses of the recipients.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'send', + 'reply', + ] + }, + }, + default: {}, + options: [ + { + displayName: 'CC Email', + name: 'ccList', + type: 'string', + description: 'The email addresses of the copy recipients.', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add CC Email', + }, + placeholder: 'info@example.com', + default: [], + }, + { + displayName: 'BCC Email', + name: 'bccList', + type: 'string', + description: 'The email addresses of the blind copy recipients.', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add BCC Email', + }, + placeholder: 'info@example.com', + default: [], + }, + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: '', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + ], + }, + ], + default: '', + description: 'Array of supported attachments to add to the message.', + }, + ] + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'get', + ] + }, + }, + default: {}, + options: [ + { + displayName: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'Full', + value: 'full', + description: 'Returns the full email message data with body content parsed in the payload field', + }, + { + name: 'Metadata', + value: 'metadata', + description: 'Returns only email message ID, labels, and email headers.', + }, + { + name: 'Minimal', + value: 'minimal', + description: 'Returns only email message ID and labels; does not return the email headers, body, or payload', + }, + { + name: 'RAW', + value: 'raw', + description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.' + }, + { + name: 'Resolved', + value: 'resolved', + description: 'Returns the full email with all data resolved and attachments saved as binary data.', + }, + ], + default: 'resolved', + description: 'The format to return the message in', + }, + { + displayName: 'Attachments Prefix', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { + hide: { + format: [ + 'full', + 'metadata', + 'minimal', + 'raw', + ], + }, + }, + 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"', + }, + ] + }, + + /* -------------------------------------------------------------------------- */ + /* message:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'message', + ], + }, + }, + default: false, + description: 'If all results should be returned 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: 10, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'message', + ], + }, + }, + options: [ + { + displayName: 'Attachments Prefix', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { + hide: { + format: [ + 'full', + 'ids', + 'metadata', + 'minimal', + 'raw', + ], + }, + }, + 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: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'Full', + value: 'full', + description: 'Returns the full email message data with body content parsed in the payload field', + }, + { + name: 'IDs', + value: 'ids', + description: 'Returns only the IDs of the emails', + }, + { + name: 'Metadata', + value: 'metadata', + description: 'Returns only email message ID, labels, and email headers.', + }, + { + name: 'Minimal', + value: 'minimal', + description: 'Returns only email message ID and labels; does not return the email headers, body, or payload', + }, + { + name: 'RAW', + value: 'raw', + description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.' + }, + { + name: 'Resolved', + value: 'resolved', + description: 'Returns the full email with all data resolved and attachments saved as binary data.', + }, + ], + default: 'resolved', + description: 'The format to return the message in', + }, + { + displayName: 'Include Spam Trash', + name: 'includeSpamTrash', + type: 'boolean', + default: false, + description: 'Include messages from SPAM and TRASH in the results.', + }, + { + displayName: 'Label IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: '', + description: 'Only return messages with labels that match all of the specified label IDs.', + }, + { + displayName: 'Query', + name: 'q', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: `Only return messages matching the specified query.
+ Supports the same query format as the Gmail search box.
+ For example, "from:someuser@example.com rfc822msgid: is:unread".
+ Parameter cannot be used when accessing the api using the gmail.metadata scope.`, + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts b/packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts new file mode 100644 index 0000000000..1215044ebe --- /dev/null +++ b/packages/nodes-base/nodes/Google/Gmail/MessageLabelDescription.ts @@ -0,0 +1,77 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const messageLabelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'messageLabel', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a label to a message', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a label from a message', + }, + ], + default: 'add', + description: 'The operation to perform', + }, +] as INodeProperties[]; + +export const messageLabelFields = [ + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'messageLabel', + ], + operation: [ + 'add', + 'remove', + ] + }, + }, + placeholder: '172ce2c4a72cc243', + description: 'The message ID of your email.', + }, + { + displayName: 'Label IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'messageLabel', + ], + operation: [ + 'add', + 'remove', + ] + }, + }, + description: 'The ID of the label', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Gmail/gmail.png b/packages/nodes-base/nodes/Google/Gmail/gmail.png new file mode 100644 index 0000000000000000000000000000000000000000..cb9198dd2eff0c52afdac63f71737922b56c7a1a GIT binary patch literal 3724 zcmV;74s-E|P)-$`Kk4A!<}qU`B&MltocQ=%%|_r0u<{ zy6WBeqpF%-e${(%obNwX@7??EyZ8Ry@7}llgfk4CIn!A(EG3N~Nn*K@92#`1<*W%d zhAJiGsTk_>)t^ayB%Z<#-83^HA(VO%#>_yNq%Z~;3Aoy6Rs!q;_NryxPpnVaI>6_v zjF*nR?trhBU*$>u-ZdMQeBdg;(?gL)l#hUdfCgV1{_5t6o@;p;z2<ykR!~F6qK~N6SHxr}WP^+a&IgiO$?>E7S>Vx(!lHe> zp?8lRaLViL*WhV<5cF0cu^H*4AnOp_;x>V80}HoQbw%!8uh-q*?Z41MeyqYmU|ch@ zD(V9s%qrOUWl!|)wgVKTEPq}es*eL{%>=$<(5Et!(znR-&o}l&x#oS9FPe8D4wk8E zKBr?yK!ryGj><AsiD8Kxm? zr3lLaXQcXo!CEk+VEyi{%ImTNjxNrfCL*r`SrG}v81Bd@EZQKTU6t2el;_XSF&cUn zv!d$j^?sX#aE)nAeHhD}SC$q3<{h2f#AbsunJsG1fn4v1S z1C2m}h`d;upZkP%;lXyAu{3Y)GT_;!Hp0Uhqt1U&RQifU^9#SF5rSZ~zq;`AO!h8$Rg;oqHXJ7tKy}TF$=;OaVaUcbSD7e$!r?b{%ju zFE?9|oj?i@vc&u}eeI@qdrjjw;P9f^sV>X84LAot;Kq!CqE}+^#|(m{OXnmB;v;N4 zf-L``8xJu2)y)+e zzkJ}FJmnI)IIHXpgpQVMuYL64^Nqj3C8|%RXHUArdwsU3QB6-p=^){Mk96>wrNRL(L1%O|N_U$$c1ethWtp2Xzfa>$g|`_BYdv zzaa^5smRK*1E1Vs$1H}2BY$0%m=^J@)l<`@00O18{rxXv0~2ywPpd7hz~`UkRNcM8 zqu*D=e}dHL|GMU(RpkwvH%@Cyl*M*U(hG||2R4UGdP}j_+q7@dh5E)f2}*?ui@#_51f% zt-AkI!`pwG8jI&tZT-$&#YaQ+fyVP<@q~A$)E6mU5uF%W5Z=C42)|Rp-+QVwfY#OY zseItqll^bKxeaI&Ogn?%m;2tYTJi6rO?lZc+dk01m&cB8J$T~eB$UCR?Wa|hw$RlR>^v+DL85S|Z&8AWD5!~sS_6QYEWL%K9V*d9`)BtA2fVpmE3c3vC%J(IZo*Z~OX+ ztA>XxXKUxQ>wwJHHk}0ahdYvq5RCyUVlYbD+BZCUq)Vqa?dsxij9}TakbJ)9U45-^ zIw8|y$ZA8tQ~lUOHnX{?qQbFp<3BDtZ{j$Fb8Y+R#1n7ZhMMS-%4veh5v8OrfT;HN zDXc>$)q8p7k4cmtvzz2p;}#))|?n}y6V5V{EA(*0|&;O@%08mRwyO<=BP`mBWaMD zFj+cwKu^)H?<9TTK9^3J(ljTi%*xugXTic#@irbn41J>i8{qr!2oOC9f<({z;tm-g z^XE^|i!a_@w)3C<frOSp(0*pzHJt=&*wBp2vPX?L=n(m;#{y$Omo9Y)A5wSd@Mh>4fV%U&fBGP-iN`NKdEkD2{`aSPMi^!{Q11QWoc0+Dj zo7OQP?V6?`A>^bRH&Oca?|VVcIb%{ho`er$@pLt9D9n%iejL4f*d#-g!l>MkQ?Bo{ zP&I9gQ&nS()`2*{;pn2CO>j6I{l;aFyfiB-eLF~-`+=qtYo91e^5&llaxQ=fuSM&d z91yv8w<~M`x5U0?qP5LSt(iiGMpaSElGb1=niMj_@+F$=vp z@;7KRk^$Tx-7B zZ-C>0jO?8Ye7;a?zTzd&FDT8M>jSO>FqSoS$Sdprr#ZdNQyc8_)fnVg04PJ1#7EAAqVd4d{JC?87!Uel zYvUnCD$Oj|unyrp09BSB&CAV=qn>d=>5@6a1lB}~CynK}$Bxg|0Rn1_V+rsbfQbC5 zI4^f}Tr`aXzFK~jM~(G5$^ZaCZ;G|uA9FG_r0|UrL2m>=8Q?(vR-Qk9a9lL*MX1;7 zNUm@fMDGeJJdshbadRyGc4w23hSAhA%BXJnAv-}q$~=KT()8Z*R%TM#eeJdBa4xAJ zrHki2Ey8V)s>7BtCvxfF2ZOy{uRGP_e-R`P0O4b2gZsyWeZHFZ+I6^|5YWtk`wo!k zjgjF-*ipWC{xv;UyLV9R^$tjN2R1hu51$e=vpX^#I=P?{pD&a#`n;cm{8ywZ#faHh znwR^_u3Sxz2aYbzo#sdge9Gyi?$!jqduHSyv_o`Bc?BuU%e@`s37QTkb&Dl>Tl(5f zhq@xGrzp?=X|gd%_anLzB5Mz2t>0C-v@4&_bnEGrFP<~SSk|9WhDNID1%1rp_xyJ} zzwv``lrMD$t`m#LoAIrmENxa@A;^;=@?7&3S8w3Z zO*0eRjeQr1$Vwo)8EGpab8F^no4)9d+#bKb<%{P`G2(a%wE0n1P~fj9YXcg5r@J3J zezp86PvR-hB@T;4s`@jaUo*u^Rpp+H!VQHz`CO-00+@o7FP?WL=x;&C` zVIL01ezCN~Q|qco9Y4Mr;Ml<(i4sgmF_sQfth3c3+ft?}Or_rQp~r-;QY zMJ^-!DQusP+C|_8_JO=7n0K9SJ~->8jn(nc^Ni(i1u0&bo8flpSfi2wk|NN@fWtzn q6jO!BNegozts&uv*lcdZ1OEr7`f4PV2m!SK0000