feat(gmail): overhaul Gmail node + create gmail trigger (#3734)

This commit is contained in:
Michael Kret
2022-09-08 15:44:34 +03:00
committed by GitHub
parent ca8c2d6577
commit 74304db4e2
24 changed files with 3858 additions and 927 deletions

View File

@@ -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',
},
{

View File

@@ -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',
},
{

View File

@@ -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',
},
{

View File

@@ -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',
},
{

View File

@@ -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',
},
{

View File

@@ -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<any> {
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,6 +181,12 @@ export async function parseRawEmail(
const binaryData: IBinaryKeyData = {};
if (responseData.attachments) {
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(
@@ -119,6 +195,7 @@ export async function parseRawEmail(
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) {
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<IDataObject> {
//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(
/&amp;|&lt;|&gt;|&#39;|&quot;/g,
(match) => {
switch (match) {
case '&amp;':
return '&';
case '&lt;':
return '<';
case '&gt;':
return '>';
case '&#39;':
return "'";
case '&quot;':
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;
});
}

View File

@@ -2,9 +2,7 @@
"node": "n8n-nodes-base.gmail",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Communication"
],
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{

View File

@@ -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';
import { labelFields, labelOperations } from './LabelDescription';
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 = {
export class Gmail extends NodeVersionedType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
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,
],
defaultVersion: 2,
};
methods = {
loadOptions: {
// Get all the labels to display them to user so that he can
// select them easily
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const labels = await googleApiRequestAllItems.call(
this,
'labels',
'GET',
'/gmail/v1/users/me/labels',
);
for (const label of labels) {
const labelName = label.name;
const labelId = label.id;
returnData.push({
name: labelName,
value: labelId,
});
}
return returnData;
},
},
const nodeVersions: INodeVersionedType['nodeVersions'] = {
1: new GmailV1(baseDescription),
2: new GmailV2(baseDescription),
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
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);
}
}

View File

@@ -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/"
}
]
}
}

View File

@@ -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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'Search',
name: 'q',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
placeholder: 'has:attachment',
hint: 'Use the same format as in the Gmail search box. <a href="https://support.google.com/mail/answer/7190?hl=en">More info</a>.',
description: 'Only return messages matching the specified query',
},
{
displayName: 'Read Status',
name: 'readStatus',
type: 'options',
default: 'unread',
hint: 'Filter emails by whether they have been read or not',
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread and read emails',
value: 'both',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread emails only',
value: 'unread',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Read emails only',
value: 'read',
},
],
},
{
displayName: 'Sender',
name: 'sender',
type: 'string',
default: '',
description: 'Sender name or email to filter by',
hint: 'Enter an email or part of a sender name',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
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<INodeExecutionData[][] | null> {
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;
}
}

View File

@@ -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,

View File

@@ -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<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const labels = await googleApiRequestAllItems.call(
this,
'labels',
'GET',
'/gmail/v1/users/me/labels',
);
for (const label of labels) {
returnData.push({
name: label.name,
value: label.id,
});
}
return returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
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);
}
}

View File

@@ -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',
},
];

View File

@@ -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 <test@gmail.com>',
default: '',
description:
'The name displayed in your contacts inboxes. It has to be in the format: "Display-Name &#60;name@gmail.com&#62;". 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 &#60;name@gmail.com&#62;". 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,

View File

@@ -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',

View File

@@ -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',
},
],
},
];

View File

@@ -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<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const labels = await googleApiRequestAllItems.call(
this,
'labels',
'GET',
'/gmail/v1/users/me/labels',
);
for (const label of labels) {
returnData.push({
name: label.name,
value: label.id,
});
}
return returnData.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
},
async getThreadMessages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
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);
}
}

View File

@@ -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',
},
];

View File

@@ -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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'Search',
name: 'q',
type: 'string',
default: '',
placeholder: 'has:attachment',
hint: 'Use the same format as in the Gmail search box. <a href="https://support.google.com/mail/answer/7190?hl=en">More info</a>.',
description: 'Only return messages matching the specified query',
},
{
displayName: 'Read Status',
name: 'readStatus',
type: 'options',
default: 'unread',
hint: 'Filter emails by whether they have been read or not',
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread and read emails',
value: 'both',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread emails only',
value: 'unread',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Read emails only',
value: 'read',
},
],
},
{
displayName: '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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
},
];

View File

@@ -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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'Search',
name: 'q',
type: 'string',
default: '',
placeholder: 'has:attachment',
hint: 'Use the same format as in the Gmail search box. <a href="https://support.google.com/mail/answer/7190?hl=en">More info</a>.',
description: 'Only return messages matching the specified query',
},
{
displayName: 'Read Status',
name: 'readStatus',
type: 'options',
default: 'unread',
hint: 'Filter emails by whether they have been read or not',
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread and read emails',
value: 'both',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Unread emails only',
value: 'unread',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Read emails only',
value: 'read',
},
],
},
{
displayName: '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 <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
},
];

View File

@@ -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',
},
{

View File

@@ -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',
},
{

View File

@@ -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',
},
{

View File

@@ -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",