Gmail node (#856)

*  Gmail node

*  Improvements

*  Improvements

*  Improvements to Gmail-Node

*  Additional improvements to Gmail-Node

Co-authored-by: Erin <erin2722@gmail.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
Jan 2020-08-18 12:40:19 +02:00 committed by GitHub
parent 226dbce5c6
commit fb042adae2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 2090 additions and 0 deletions

View file

@ -0,0 +1,30 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/gmail.labels',
'https://www.googleapis.com/auth/gmail.addons.current.action.compose',
'https://www.googleapis.com/auth/gmail.addons.current.message.action',
'https://mail.google.com/',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.compose',
];
export class GmailOAuth2Api implements ICredentialType {
name = 'gmailOAuth2';
extends = [
'googleOAuth2Api',
];
displayName = 'Gmail OAuth2 API';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View file

@ -0,0 +1,382 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const draftOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'draft',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new email draft',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a draft',
},
{
name: 'Get',
value: 'get',
description: 'Get a draft',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all drafts',
}
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const draftFields = [
{
displayName: 'Draft ID',
name: 'messageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'delete',
'get',
]
},
},
placeholder: 'r-3254521568507167962',
description: 'The ID of the draft to operate on.',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'create',
]
},
},
placeholder: 'Hello World!',
description: 'The message subject.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'create',
]
},
},
placeholder: 'Hello World!',
description: 'The message body. This can be in HTML.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'create',
]
},
},
default: {},
options: [
{
displayName: 'To Email',
name: 'toList',
type: 'string',
default: [],
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add To Email',
},
placeholder: 'info@example.com',
description: 'The email addresses of the recipients.',
},
{
displayName: 'CC Email',
name: 'ccList',
type: 'string',
description: 'The email addresses of the copy recipients.',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add CC Email',
},
placeholder: 'info@example.com',
default: [],
},
{
displayName: 'BCC Email',
name: 'bccList',
type: 'string',
description: 'The email addresses of the blind copy recipients.',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add BCC Email',
},
placeholder: 'info@example.com',
default: [],
},
{
displayName: 'Attachments',
name: 'attachmentsUi',
placeholder: 'Add Attachments',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachmentsBinary',
displayName: 'Attachments Binary',
values: [
{
displayName: 'Property',
name: 'property',
type: 'string',
default: '',
description: 'Name of the binary property containing the data to be added to the email as an attachment',
},
],
},
],
default: '',
description: 'Array of supported attachments to add to the message.',
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'get',
]
},
},
default: {},
options: [
{
displayName: 'Attachments Prefix',
name: 'dataPropertyAttachmentsPrefixName',
type: 'string',
default: 'attachment_',
displayOptions: {
hide: {
format: [
'full',
'metadata',
'minimal',
'raw',
],
},
},
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
},
{
displayName: 'Format',
name: 'format',
type: 'options',
options: [
{
name: 'Full',
value: 'full',
description: 'Returns the full email message data with body content parsed in the payload field',
},
{
name: 'Metadata',
value: 'metadata',
description: 'Returns only email message ID, labels, and email headers.',
},
{
name: 'Minimal',
value: 'minimal',
description: 'Returns only email message ID and labels; does not return the email headers, body, or payload',
},
{
name: 'RAW',
value: 'raw',
description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.'
},
{
name: 'Resolved',
value: 'resolved',
description: 'Returns the full email with all data resolved and attachments saved as binary data.',
},
],
default: 'resolved',
description: 'The format to return the message in',
},
]
},
/* -------------------------------------------------------------------------- */
/* draft:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'draft',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'draft',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 10,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'draft',
],
},
},
options: [
{
displayName: 'Attachments Prefix',
name: 'dataPropertyAttachmentsPrefixName',
type: 'string',
default: 'attachment_',
displayOptions: {
hide: {
format: [
'full',
'ids',
'metadata',
'minimal',
'raw',
],
},
},
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
},
{
displayName: 'Format',
name: 'format',
type: 'options',
options: [
{
name: 'Full',
value: 'full',
description: 'Returns the full email message data with body content parsed in the payload field',
},
{
name: 'IDs',
value: 'ids',
description: 'Returns only the IDs of the emails',
},
{
name: 'Metadata',
value: 'metadata',
description: 'Returns only email message ID, labels, and email headers.',
},
{
name: 'Minimal',
value: 'minimal',
description: 'Returns only email message ID and labels; does not return the email headers, body, or payload',
},
{
name: 'RAW',
value: 'raw',
description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.'
},
{
name: 'Resolved',
value: 'resolved',
description: 'Returns the full email with all data resolved and attachments saved as binary data.',
},
],
default: 'resolved',
description: 'The format to return the message in',
},
{
displayName: 'Include Spam Trash',
name: 'includeSpamTrash',
type: 'boolean',
default: false,
description: 'Include messages from SPAM and TRASH in the results.',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,191 @@
import {
OptionsWithUri,
} from 'request';
import {
simpleParser,
Source as ParserSource,
} from 'mailparser';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import {
IEmail,
} from './Gmail.node';
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string,
endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
let options: OptionsWithUri = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://www.googleapis.com${endpoint}`,
json: true,
};
options = Object.assign({}, options, option);
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'gmailOAuth2', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
let errorMessages;
if (error.response.body.error.errors) {
// Try to return the error prettier
errorMessages = error.response.body.error.errors;
errorMessages = errorMessages.map((errorItem: IDataObject) => errorItem.message);
errorMessages = errorMessages.join('|');
} else if (error.response.body.error.message) {
errorMessages = error.response.body.error.message;
}
throw new Error(`Gmail error response [${error.statusCode}]: ${errorMessages}`);
}
throw error;
}
}
export async function parseRawEmail(this: IExecuteFunctions, messageEncoded: ParserSource, dataPropertyNameDownload: string): Promise<INodeExecutionData> {
const responseData = await simpleParser(messageEncoded);
const headers: IDataObject = {};
for (const header of responseData.headerLines) {
headers[header.key] = header.line;
}
// @ts-ignore
responseData.headers = headers;
// @ts-ignore
responseData.headerLines = undefined;
const binaryData: IBinaryKeyData = {};
if (responseData.attachments) {
for (let i = 0; i < responseData.attachments.length; i++) {
const attachment = responseData.attachments[i];
binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(attachment.content, attachment.filename, attachment.contentType);
}
// @ts-ignore
responseData.attachments = undefined;
}
return {
json: responseData as unknown as IDataObject,
binary: Object.keys(binaryData).length ? binaryData : undefined,
} as INodeExecutionData;
}
//------------------------------------------------------------------------------------------------------------------------------------------
// This function converts an email object into a MIME encoded email and then converts that string into base64 encoding
// for more info on MIME, https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/aa494197(v%3Dexchg.140)
//------------------------------------------------------------------------------------------------------------------------------------------
export function encodeEmail(email: IEmail) {
let mimeEmail = '';
if (email.attachments !== undefined && email.attachments !== []) {
const attachments = email.attachments.map((attachment) => {
return [
"--XXXXboundary text\n",
"Content-Type:", attachment.type, ";\n",
"Content-Transfer-Encoding: Base64\n",
"Content-Disposition: attachment;\n",
"\tfilename=\"", attachment.name, "\"\n\n",
attachment.content, "\n\n",
"--XXXXboundary text\n\n",
].join('');
});
mimeEmail = [
"To: ", email.to, "\n",
"Cc: ", email.cc, "\n",
"Bcc: ", email.bcc, "\n",
"In-Reply-To: ", email.inReplyTo, "\n",
"References: ", email.reference, "\n",
"Subject: ", email.subject, "\n",
"MIME-Version: 1.0\n",
"Content-Type: multipart/mixed;\n",
"\tboundary=\"XXXXboundary text\"\n\n",
"This is a multipart message in MIME format.\n\n",
"--XXXXboundary text\n",
"Content-Type: text/plain\n\n",
email.body, "\n\n",
attachments.join(''),
"--XXXXboundary text--",
].join('');
} else {
mimeEmail = [
"Content-Type: text/plain; charset=\"UTF-8\"\n",
"MIME-Version: 1.0\n",
"Content-Transfer-Encoding: 7bit\n",
"To: ", email.to, "\n",
"Cc: ", email.cc, "\n",
"Bcc: ", email.bcc, "\n",
"In-Reply-To: ", email.inReplyTo, "\n",
"References: ", email.reference, "\n",
"Subject: ", email.subject, "\n\n",
email.body,
].join('');
}
return Buffer.from(mimeEmail).toString("base64").replace(/\+/g, '-').replace(/\//g, '_');
}
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.maxResults = 100;
do {
responseData = await googleApiRequest.call(this, method, endpoint, body, query);
query.pageToken = responseData['nextPageToken'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['nextPageToken'] !== undefined &&
responseData['nextPageToken'] !== ''
);
return returnData;
}
export function extractEmail(s: string) {
const data = s.split('<')[1];
return data.substring(0, data.length - 1);
}

View file

@ -0,0 +1,755 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
ILoadOptionsFunctions,
IDataObject,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
encodeEmail,
extractEmail,
googleApiRequest,
googleApiRequestAllItems,
parseRawEmail,
} from './GenericFunctions';
import {
messageOperations,
messageFields,
} from './MessageDescription';
import {
messageLabelOperations,
messageLabelFields,
} from './MessageLabelDescription';
import {
labelOperations,
labelFields,
} from './LabelDescription';
import {
draftOperations,
draftFields,
} from './DraftDescription';
import {
isEmpty,
} from 'lodash';
export interface IEmail {
to?: string;
cc?: string;
bcc?: string;
inReplyTo?: string;
reference?: string;
subject: string;
body: string;
attachments?: IDataObject[];
}
interface IAttachments {
type: string;
name: string;
content: string;
}
export class Gmail implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gmail',
name: 'gmail',
icon: 'file:gmail.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Gmail API',
defaults: {
name: 'Gmail',
color: '#d93025',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'gmailOAuth2',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Draft',
value: 'draft',
},
{
name: 'Label',
value: 'label',
},
{
name: 'Message',
value: 'message',
},
{
name: 'Message Label',
value: 'messageLabel',
},
],
default: 'draft',
description: 'The resource to operate on.',
},
//-------------------------------
// Draft Operations
//-------------------------------
...draftOperations,
...draftFields,
//-------------------------------
// Label Operations
//-------------------------------
...labelOperations,
...labelFields,
//-------------------------------
// Message Operations
//-------------------------------
...messageOperations,
...messageFields,
//-------------------------------
// MessageLabel Operations
//-------------------------------
...messageLabelOperations,
...messageLabelFields,
],
};
methods = {
loadOptions: {
// Get all the labels to display them to user so that he can
// select them easily
async getLabels(
this: ILoadOptionsFunctions
): Promise<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;
},
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let method = '';
let body: IDataObject = {};
let qs: IDataObject = {};
let endpoint = '';
let responseData;
for (let i = 0; i < items.length; i++) {
if (resource === 'label') {
if (operation === 'create') {
//https://developers.google.com/gmail/api/v1/reference/users/labels/create
const labelName = this.getNodeParameter('name', i) as string;
const labelListVisibility = this.getNodeParameter('labelListVisibility', i) as string;
const messageListVisibility = this.getNodeParameter('messageListVisibility', i) as string;
method = 'POST';
endpoint = '/gmail/v1/users/me/labels';
body = {
labelListVisibility,
messageListVisibility,
name: labelName,
};
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
if (operation === 'delete') {
//https://developers.google.com/gmail/api/v1/reference/users/labels/delete
const labelId = this.getNodeParameter('labelId', i) as string[];
method = 'DELETE';
endpoint = `/gmail/v1/users/me/labels/${labelId}`;
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
responseData = { success: true };
}
if (operation === 'get') {
// https://developers.google.com/gmail/api/v1/reference/users/labels/get
const labelId = this.getNodeParameter('labelId', i);
method = 'GET';
endpoint = `/gmail/v1/users/me/labels/${labelId}`;
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await googleApiRequest.call(
this,
'GET',
`/gmail/v1/users/me/labels`,
{},
qs
);
responseData = responseData.labels;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
}
if (resource === 'messageLabel') {
if (operation === 'remove') {
//https://developers.google.com/gmail/api/v1/reference/users/messages/modify
const messageID = this.getNodeParameter('messageId', i);
const labelIds = this.getNodeParameter('labelIds', i) as string[];
method = 'POST';
endpoint = `/gmail/v1/users/me/messages/${messageID}/modify`;
body = {
removeLabelIds: labelIds,
};
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
if (operation === 'add') {
// https://developers.google.com/gmail/api/v1/reference/users/messages/modify
const messageID = this.getNodeParameter('messageId', i);
const labelIds = this.getNodeParameter('labelIds', i) as string[];
method = 'POST';
endpoint = `/gmail/v1/users/me/messages/${messageID}/modify`;
body = {
addLabelIds: labelIds,
};
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
}
if (resource === 'message') {
if (operation === 'send') {
// https://developers.google.com/gmail/api/v1/reference/users/messages/send
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
let toStr = '';
let ccStr = '';
let bccStr = '';
let attachmentsList: IDataObject[] = [];
const toList = this.getNodeParameter('toList', i) as IDataObject[];
toList.forEach((email) => {
toStr += `<${email}>, `;
});
if (additionalFields.ccList) {
const ccList = additionalFields.ccList as IDataObject[];
ccList.forEach((email) => {
ccStr += `<${email}>, `;
});
}
if (additionalFields.bccList) {
const bccList = additionalFields.bccList as IDataObject[];
bccList.forEach((email) => {
bccStr += `<${email}>, `;
});
}
if (additionalFields.attachmentsUi) {
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
let attachmentsBinary = [];
if (!isEmpty(attachmentsUi)) {
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
&& !isEmpty(attachmentsUi.attachmentsBinary)
&& items[i].binary) {
// @ts-ignore
attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => {
if (items[i].binary!.hasOwnProperty(value.property)) {
const aux: IAttachments = { name: '', content: '', type: '' };
aux.name = items[i].binary![value.property].fileName || 'unknown';
aux.content = items[i].binary![value.property].data;
aux.type = items[i].binary![value.property].mimeType;
return aux;
}
});
}
qs = {
userId: 'me',
uploadType: 'media',
};
attachmentsList = attachmentsBinary;
}
}
const email: IEmail = {
to: toStr,
cc: ccStr,
bcc: bccStr,
subject: this.getNodeParameter('subject', i) as string,
body: this.getNodeParameter('message', i) as string,
attachments: attachmentsList,
};
endpoint = '/gmail/v1/users/me/messages/send';
method = 'POST';
body = {
raw: encodeEmail(email),
};
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
if (operation === 'reply') {
const id = this.getNodeParameter('messageId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
let toStr = '';
let ccStr = '';
let bccStr = '';
let attachmentsList: IDataObject[] = [];
const toList = this.getNodeParameter('toList', i) as IDataObject[];
toList.forEach((email) => {
toStr += `<${email}>, `;
});
if (additionalFields.ccList) {
const ccList = additionalFields.ccList as IDataObject[];
ccList.forEach((email) => {
ccStr += `<${email}>, `;
});
}
if (additionalFields.bccList) {
const bccList = additionalFields.bccList as IDataObject[];
bccList.forEach((email) => {
bccStr += `<${email}>, `;
});
}
if (additionalFields.attachmentsUi) {
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
let attachmentsBinary = [];
if (!isEmpty(attachmentsUi)) {
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
&& !isEmpty(attachmentsUi.attachmentsBinary)
&& items[i].binary) {
// @ts-ignore
attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => {
if (items[i].binary!.hasOwnProperty(value.property)) {
const aux: IAttachments = { name: '', content: '', type: '' };
aux.name = items[i].binary![value.property].fileName || 'unknown';
aux.content = items[i].binary![value.property].data;
aux.type = items[i].binary![value.property].mimeType;
return aux;
}
});
}
qs = {
userId: 'me',
uploadType: 'media',
};
attachmentsList = attachmentsBinary;
}
}
// if no recipient is defined then grab the one who sent the email
if (toStr === '') {
endpoint = `/gmail/v1/users/me/messages/${id}`;
qs.format = 'metadata';
const { payload } = await googleApiRequest.call(this, method, endpoint, body, qs);
for (const header of payload.headers as IDataObject[]) {
if (header.name === 'From') {
toStr = `<${extractEmail(header.value as string)}>,`;
break;
}
}
}
const email: IEmail = {
to: toStr,
cc: ccStr,
bcc: bccStr,
subject: this.getNodeParameter('subject', i) as string,
body: this.getNodeParameter('message', i) as string,
attachments: attachmentsList,
};
endpoint = '/gmail/v1/users/me/messages/send';
method = 'POST';
email.inReplyTo = id;
email.reference = id;
body = {
raw: encodeEmail(email),
threadId: this.getNodeParameter('threadId', i) as string,
};
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
if (operation === 'get') {
//https://developers.google.com/gmail/api/v1/reference/users/messages/get
method = 'GET';
const id = this.getNodeParameter('messageId', i);
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const format = additionalFields.format || 'resolved';
if (format === 'resolved') {
qs.format = 'raw';
} else {
qs.format = format;
}
endpoint = `/gmail/v1/users/me/messages/${id}`;
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
let nodeExecutionData: INodeExecutionData;
if (format === 'resolved') {
const messageEncoded = Buffer.from(responseData.raw, 'base64').toString('utf8');
const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_';
nodeExecutionData = await parseRawEmail.call(this, messageEncoded, dataPropertyNameDownload);
} else {
nodeExecutionData = {
json: responseData,
};
}
responseData = nodeExecutionData;
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
Object.assign(qs, additionalFields);
if (qs.labelIds) {
// tslint:disable-next-line: triple-equals
if (qs.labelIds == '') {
delete qs.labelIds;
} else {
qs.labelIds = (qs.labelIds as string[]).join(',');
}
}
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
'messages',
'GET',
`/gmail/v1/users/me/messages`,
{},
qs
);
} else {
qs.maxResults = this.getNodeParameter('limit', i) as number;
responseData = await googleApiRequest.call(
this,
'GET',
`/gmail/v1/users/me/messages`,
{},
qs
);
responseData = responseData.messages;
}
if (responseData === undefined) {
responseData = [];
}
const format = additionalFields.format || 'resolved';
if (format !== 'ids') {
if (format === 'resolved') {
qs.format = 'raw';
} else {
qs.format = format;
}
for (let i = 0; i < responseData.length; i++) {
responseData[i] = await googleApiRequest.call(
this,
'GET',
`/gmail/v1/users/me/messages/${responseData[i].id}`,
body,
qs
);
if (format === 'resolved') {
const messageEncoded = Buffer.from(responseData[i].raw, 'base64').toString('utf8');
const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_';
responseData[i] = await parseRawEmail.call(this, messageEncoded, dataPropertyNameDownload);
}
}
}
if (format !== 'resolved') {
responseData = this.helpers.returnJsonArray(responseData);
}
}
if (operation === 'delete') {
// https://developers.google.com/gmail/api/v1/reference/users/messages/delete
method = 'DELETE';
const id = this.getNodeParameter('messageId', i);
endpoint = `/gmail/v1/users/me/messages/${id}`;
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
responseData = { success: true };
}
}
if (resource === 'draft') {
if (operation === 'create') {
// https://developers.google.com/gmail/api/v1/reference/users/drafts/create
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
let toStr = '';
let ccStr = '';
let bccStr = '';
let attachmentsList: IDataObject[] = [];
if (additionalFields.toList) {
const toList = additionalFields.toList as IDataObject[];
toList.forEach((email) => {
toStr += `<${email}>, `;
});
}
if (additionalFields.ccList) {
const ccList = additionalFields.ccList as IDataObject[];
ccList.forEach((email) => {
ccStr += `<${email}>, `;
});
}
if (additionalFields.bccList) {
const bccList = additionalFields.bccList as IDataObject[];
bccList.forEach((email) => {
bccStr += `<${email}>, `;
});
}
if (additionalFields.attachmentsUi) {
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
let attachmentsBinary = [];
if (!isEmpty(attachmentsUi)) {
if (attachmentsUi.hasOwnProperty('attachmentsBinary')
&& !isEmpty(attachmentsUi.attachmentsBinary)
&& items[i].binary) {
// @ts-ignore
attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => {
if (items[i].binary!.hasOwnProperty(value.property)) {
const aux: IAttachments = { name: '', content: '', type: '' };
aux.name = items[i].binary![value.property].fileName || 'unknown';
aux.content = items[i].binary![value.property].data;
aux.type = items[i].binary![value.property].mimeType;
return aux;
}
});
}
qs = {
userId: 'me',
uploadType: 'media',
};
attachmentsList = attachmentsBinary;
}
}
const email: IEmail = {
to: toStr,
cc: ccStr,
bcc: bccStr,
subject: this.getNodeParameter('subject', i) as string,
body: this.getNodeParameter('message', i) as string,
attachments: attachmentsList,
};
endpoint = '/gmail/v1/users/me/drafts';
method = 'POST';
body = {
message: {
raw: encodeEmail(email),
},
};
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
}
if (operation === 'get') {
// https://developers.google.com/gmail/api/v1/reference/users/drafts/get
method = 'GET';
const id = this.getNodeParameter('messageId', i);
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const format = additionalFields.format || 'resolved';
if (format === 'resolved') {
qs.format = 'raw';
} else {
qs.format = format;
}
endpoint = `/gmail/v1/users/me/drafts/${id}`;
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
const binaryData: IBinaryKeyData = {};
let nodeExecutionData: INodeExecutionData;
if (format === 'resolved') {
const messageEncoded = Buffer.from(responseData.message.raw, 'base64').toString('utf8');
const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_';
nodeExecutionData = await parseRawEmail.call(this, messageEncoded, dataPropertyNameDownload);
} else {
nodeExecutionData = {
json: responseData,
binary: Object.keys(binaryData).length ? binaryData : undefined,
};
}
responseData = nodeExecutionData;
}
if (operation === 'delete') {
// https://developers.google.com/gmail/api/v1/reference/users/drafts/delete
method = 'DELETE';
const id = this.getNodeParameter('messageId', i);
endpoint = `/gmail/v1/users/me/drafts/${id}`;
responseData = await googleApiRequest.call(this, method, endpoint, body, qs);
responseData = { success: true };
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
Object.assign(qs, additionalFields);
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
'drafts',
'GET',
`/gmail/v1/users/me/drafts`,
{},
qs
);
} else {
qs.maxResults = this.getNodeParameter('limit', i) as number;
responseData = await googleApiRequest.call(
this,
'GET',
`/gmail/v1/users/me/drafts`,
{},
qs
);
responseData = responseData.drafts;
}
if (responseData === undefined) {
responseData = [];
}
const format = additionalFields.format || 'resolved';
if (format !== 'ids') {
if (format === 'resolved') {
qs.format = 'raw';
} else {
qs.format = format;
}
for (let i = 0; i < responseData.length; i++) {
responseData[i] = await googleApiRequest.call(
this,
'GET',
`/gmail/v1/users/me/drafts/${responseData[i].id}`,
body,
qs
);
if (format === 'resolved') {
const messageEncoded = Buffer.from(responseData[i].message.raw, 'base64').toString('utf8');
const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_';
responseData[i] = await parseRawEmail.call(this, messageEncoded, dataPropertyNameDownload);
}
}
}
if (format !== 'resolved') {
responseData = this.helpers.returnJsonArray(responseData);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
if (['draft', 'message'].includes(resource) && ['get', 'getAll'].includes(operation)) {
//@ts-ignore
return this.prepareOutputData(returnData);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,187 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const labelOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'label',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new label',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a label',
},
{
name: 'Get',
value: 'get',
description: 'Get a label',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all labels',
}
],
default: 'create',
description: 'The operation to perform',
},
] as INodeProperties[];
export const labelFields = [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'label',
],
operation: [
'create',
]
},
},
placeholder: 'invoices',
description: 'Label Name',
},
{
displayName: 'Label ID',
name: 'labelId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'label',
],
operation: [
'get',
'delete',
],
},
},
description: 'The ID of the label',
},
{
displayName: 'Label List Visibility',
name: 'labelListVisibility',
type: 'options',
options: [
{
name: 'Hide',
value: 'labelHide',
},
{
name: 'Show',
value: 'labelShow',
},
{
name: 'Show If Unread',
value: 'labelShowIfUnread',
},
],
default: 'labelShow',
required: true,
displayOptions: {
show: {
resource: [
'label',
],
operation: [
'create',
],
},
},
description: 'The visibility of the label in the label list in the Gmail web interface.',
},
{
displayName: 'Message List Visibility',
name: 'messageListVisibility',
type: 'options',
options: [
{
name: 'Hide',
value: 'hide',
},
{
name: 'show',
value: 'show',
},
],
default: 'show',
required: true,
displayOptions: {
show: {
resource: [
'label',
],
operation: [
'create',
],
},
},
description: 'The visibility of messages with this label in the message list in the Gmail web interface.',
},
/* -------------------------------------------------------------------------- */
/* label:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'label',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'label',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,464 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const messageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'message',
],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send an email',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a message',
},
{
name: 'Get',
value: 'get',
description: 'Get a message',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all messages',
},
{
name: 'Reply',
value: 'reply',
description: 'Reply to an email',
},
],
default: 'send',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const messageFields = [
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'get',
'delete',
]
},
},
placeholder: '172ce2c4a72cc243',
description: 'The ID of the message you are operating on.',
},
{
displayName: 'Thread ID',
name: 'threadId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
]
},
},
placeholder: '172ce2c4a72cc243',
description: 'The ID of the thread you are replying to.',
},
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
]
},
},
placeholder: 'CAHNQoFsC6JMMbOBJgtjsqN0eEc+gDg2a=SQj-tWUebQeHMDgqQ@mail.gmail.com',
description: 'The ID of the message you are replying to.',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
'send',
]
},
},
placeholder: 'Hello World!',
description: 'The message subject.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
'send',
]
},
},
placeholder: 'Hello World!',
description: 'The message body. This can be in HTML.',
},
{
displayName: 'To Email',
name: 'toList',
type: 'string',
default: [],
required: true,
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add To Email',
},
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
'send',
]
},
},
placeholder: 'info@example.com',
description: 'The email addresses of the recipients.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'send',
'reply',
]
},
},
default: {},
options: [
{
displayName: 'CC Email',
name: 'ccList',
type: 'string',
description: 'The email addresses of the copy recipients.',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add CC Email',
},
placeholder: 'info@example.com',
default: [],
},
{
displayName: 'BCC Email',
name: 'bccList',
type: 'string',
description: 'The email addresses of the blind copy recipients.',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add BCC Email',
},
placeholder: 'info@example.com',
default: [],
},
{
displayName: 'Attachments',
name: 'attachmentsUi',
placeholder: 'Add Attachments',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachmentsBinary',
displayName: 'Attachments Binary',
values: [
{
displayName: 'Property',
name: 'property',
type: 'string',
default: '',
description: 'Name of the binary properties which contain data which should be added to email as attachment',
},
],
},
],
default: '',
description: 'Array of supported attachments to add to the message.',
},
]
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'get',
]
},
},
default: {},
options: [
{
displayName: 'Format',
name: 'format',
type: 'options',
options: [
{
name: 'Full',
value: 'full',
description: 'Returns the full email message data with body content parsed in the payload field',
},
{
name: 'Metadata',
value: 'metadata',
description: 'Returns only email message ID, labels, and email headers.',
},
{
name: 'Minimal',
value: 'minimal',
description: 'Returns only email message ID and labels; does not return the email headers, body, or payload',
},
{
name: 'RAW',
value: 'raw',
description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.'
},
{
name: 'Resolved',
value: 'resolved',
description: 'Returns the full email with all data resolved and attachments saved as binary data.',
},
],
default: 'resolved',
description: 'The format to return the message in',
},
{
displayName: 'Attachments Prefix',
name: 'dataPropertyAttachmentsPrefixName',
type: 'string',
default: 'attachment_',
displayOptions: {
hide: {
format: [
'full',
'metadata',
'minimal',
'raw',
],
},
},
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
},
]
},
/* -------------------------------------------------------------------------- */
/* message:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'message',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'message',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 10,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'message',
],
},
},
options: [
{
displayName: 'Attachments Prefix',
name: 'dataPropertyAttachmentsPrefixName',
type: 'string',
default: 'attachment_',
displayOptions: {
hide: {
format: [
'full',
'ids',
'metadata',
'minimal',
'raw',
],
},
},
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
},
{
displayName: 'Format',
name: 'format',
type: 'options',
options: [
{
name: 'Full',
value: 'full',
description: 'Returns the full email message data with body content parsed in the payload field',
},
{
name: 'IDs',
value: 'ids',
description: 'Returns only the IDs of the emails',
},
{
name: 'Metadata',
value: 'metadata',
description: 'Returns only email message ID, labels, and email headers.',
},
{
name: 'Minimal',
value: 'minimal',
description: 'Returns only email message ID and labels; does not return the email headers, body, or payload',
},
{
name: 'RAW',
value: 'raw',
description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.'
},
{
name: 'Resolved',
value: 'resolved',
description: 'Returns the full email with all data resolved and attachments saved as binary data.',
},
],
default: 'resolved',
description: 'The format to return the message in',
},
{
displayName: 'Include Spam Trash',
name: 'includeSpamTrash',
type: 'boolean',
default: false,
description: 'Include messages from SPAM and TRASH in the results.',
},
{
displayName: 'Label IDs',
name: 'labelIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getLabels',
},
default: '',
description: 'Only return messages with labels that match all of the specified label IDs.',
},
{
displayName: 'Query',
name: 'q',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: `Only return messages matching the specified query.</br>
Supports the same query format as the Gmail search box.</br>
For example, "from:someuser@example.com rfc822msgid:<somemsgid@example.com> is:unread".</br>
Parameter cannot be used when accessing the api using the gmail.metadata scope.`,
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,77 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const messageLabelOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'messageLabel',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a label to a message',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a label from a message',
},
],
default: 'add',
description: 'The operation to perform',
},
] as INodeProperties[];
export const messageLabelFields = [
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'messageLabel',
],
operation: [
'add',
'remove',
]
},
},
placeholder: '172ce2c4a72cc243',
description: 'The message ID of your email.',
},
{
displayName: 'Label IDs',
name: 'labelIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getLabels',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'messageLabel',
],
operation: [
'add',
'remove',
]
},
},
description: 'The ID of the label',
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -67,6 +67,7 @@
"dist/credentials/GithubOAuth2Api.credentials.js",
"dist/credentials/GitlabApi.credentials.js",
"dist/credentials/GitlabOAuth2Api.credentials.js",
"dist/credentials/GmailOAuth2Api.credentials.js",
"dist/credentials/GoogleApi.credentials.js",
"dist/credentials/GoogleCalendarOAuth2Api.credentials.js",
"dist/credentials/GoogleContactsOAuth2Api.credentials.js",
@ -235,6 +236,7 @@
"dist/nodes/Google/Calendar/GoogleCalendar.node.js",
"dist/nodes/Google/Contacts/GoogleContacts.node.js",
"dist/nodes/Google/Drive/GoogleDrive.node.js",
"dist/nodes/Google/Gmail/Gmail.node.js",
"dist/nodes/Google/Sheet/GoogleSheets.node.js",
"dist/nodes/Google/Task/GoogleTasks.node.js",
"dist/nodes/Google/YouTube/YouTube.node.js",
@ -360,6 +362,7 @@
"@types/imap-simple": "^4.2.0",
"@types/jest": "^25.2.1",
"@types/lodash.set": "^4.3.6",
"@types/mailparser": "^2.7.3",
"@types/moment-timezone": "^0.5.12",
"@types/mongodb": "^3.5.4",
"@types/mssql": "^6.0.2",
@ -394,6 +397,7 @@
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"lodash.unset": "^4.5.2",
"mailparser": "^2.8.1",
"moment": "2.24.0",
"moment-timezone": "^0.5.28",
"mongodb": "^3.5.5",