first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { DateTime } from 'luxon';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import type {
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
IPollFunctions,
|
||||
IRequestOptions,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
import MailComposer from 'nodemailer/lib/mail-composer';
|
||||
|
||||
import type { IEmail } from '../../../utils/sendAndWait/interfaces';
|
||||
import { createUtmCampaignLink, escapeHtml } from '../../../utils/utilities';
|
||||
import { getGoogleAccessToken } from '../GenericFunctions';
|
||||
|
||||
export interface IAttachments {
|
||||
type: string;
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export async function googleApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
option: IDataObject = {},
|
||||
) {
|
||||
let options: IRequestOptions = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://www.googleapis.com${endpoint}`,
|
||||
qsStringifyOptions: {
|
||||
arrayFormat: 'repeat',
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
try {
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
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 getGoogleAccessToken.call(this, credentials, 'gmail');
|
||||
|
||||
(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';
|
||||
}
|
||||
|
||||
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 errorOptions = {
|
||||
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 as JsonObject, errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (error.httpCode === '404') {
|
||||
let resource = this.getNodeParameter('resource', 0) as string;
|
||||
if (resource === 'label') {
|
||||
resource = 'label ID';
|
||||
}
|
||||
const errorOptions = {
|
||||
message: `${resource.charAt(0).toUpperCase() + resource.slice(1)} not found`,
|
||||
description: '',
|
||||
};
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, errorOptions);
|
||||
}
|
||||
|
||||
if (error.httpCode === '409') {
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
if (resource === 'label') {
|
||||
const errorOptions = {
|
||||
message: 'Label name exists already',
|
||||
description: '',
|
||||
};
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 'EAUTH') {
|
||||
const errorOptions = {
|
||||
message: error?.body?.error_description || 'Authorization error',
|
||||
description: (error as Error).message,
|
||||
};
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, errorOptions);
|
||||
}
|
||||
|
||||
if (
|
||||
((error.message as string) || '').includes('Bad request - please check your parameters') &&
|
||||
error.description
|
||||
) {
|
||||
const errorOptions = {
|
||||
message: error.description,
|
||||
description: '',
|
||||
};
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, errorOptions);
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, {
|
||||
message: error.message,
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseRawEmail(
|
||||
this: IExecuteFunctions | IPollFunctions,
|
||||
|
||||
messageData: any,
|
||||
dataPropertyNameDownload: string,
|
||||
): Promise<INodeExecutionData> {
|
||||
const messageEncoded = Buffer.from(messageData.raw as string, 'base64').toString('utf8');
|
||||
const responseData = await simpleParser(messageEncoded);
|
||||
|
||||
const headers: IDataObject = {};
|
||||
for (const header of responseData.headerLines) {
|
||||
headers[header.key] = header.line;
|
||||
}
|
||||
|
||||
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(
|
||||
attachment.content,
|
||||
attachment.filename,
|
||||
attachment.contentType,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mailBaseData: IDataObject = {};
|
||||
|
||||
const resolvedModeAddProperties = ['id', 'threadId', 'labelIds', 'sizeEstimate'];
|
||||
|
||||
for (const key of resolvedModeAddProperties) {
|
||||
mailBaseData[key] = messageData[key];
|
||||
}
|
||||
|
||||
const json = Object.assign({}, mailBaseData, responseData, {
|
||||
headers,
|
||||
headerLines: undefined,
|
||||
attachments: undefined,
|
||||
// Having data in IDataObjects that is not representable in JSON leads to
|
||||
// inconsistencies between test executions and production executions.
|
||||
// During a manual execution this would be stringified and during a
|
||||
// production execution the next node would receive a date instance.
|
||||
date: responseData.date ? responseData.date.toISOString() : responseData.date,
|
||||
}) as IDataObject;
|
||||
|
||||
return {
|
||||
json,
|
||||
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 async function encodeEmail(email: IEmail) {
|
||||
// https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields
|
||||
const mailOptions = {
|
||||
from: email.from,
|
||||
to: email.to,
|
||||
cc: email.cc,
|
||||
bcc: email.bcc,
|
||||
replyTo: email.replyTo,
|
||||
inReplyTo: email.inReplyTo,
|
||||
references: email.reference,
|
||||
subject: email.subject,
|
||||
text: email.body,
|
||||
keepBcc: true,
|
||||
} as IDataObject;
|
||||
|
||||
if (email.htmlBody) {
|
||||
mailOptions.html = email.htmlBody;
|
||||
}
|
||||
|
||||
if (
|
||||
email.attachments !== undefined &&
|
||||
Array.isArray(email.attachments) &&
|
||||
email.attachments.length > 0
|
||||
) {
|
||||
const attachments = email.attachments.map((attachment) => ({
|
||||
filename: attachment.name,
|
||||
content: attachment.content,
|
||||
contentType: attachment.type,
|
||||
encoding: 'base64',
|
||||
}));
|
||||
|
||||
mailOptions.attachments = attachments;
|
||||
}
|
||||
|
||||
const mail = new MailComposer(mailOptions).compile();
|
||||
|
||||
// by default the bcc headers are deleted when the mail is built.
|
||||
// So add keepBcc flag to override such behaviour. Only works when
|
||||
// the flag is set after the compilation.
|
||||
mail.keepBcc = true;
|
||||
|
||||
const mailBody = await mail.build();
|
||||
|
||||
return mailBody.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
export async function googleApiRequestAllItems(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
propertyName: string,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
query.maxResults = 100;
|
||||
|
||||
do {
|
||||
responseData = await googleApiRequest.call(this, method, endpoint, body as IDataObject, query);
|
||||
query.pageToken = responseData.nextPageToken;
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export function extractEmail(s: string) {
|
||||
if (s.includes('<')) {
|
||||
const data = s.split('<')[1];
|
||||
return data.substring(0, data.length - 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export const prepareTimestamp = (
|
||||
node: INode,
|
||||
itemIndex: number,
|
||||
query: string,
|
||||
dateValue: string | number | DateTime,
|
||||
label: 'after' | 'before',
|
||||
) => {
|
||||
if (dateValue instanceof DateTime) {
|
||||
dateValue = dateValue.toISO();
|
||||
}
|
||||
|
||||
let timestamp = DateTime.fromISO(dateValue as string).toSeconds();
|
||||
const timestampLengthInMilliseconds1990 = 12;
|
||||
|
||||
if (typeof timestamp === 'number') {
|
||||
timestamp = Math.round(timestamp);
|
||||
}
|
||||
|
||||
if (
|
||||
!timestamp &&
|
||||
typeof dateValue === 'number' &&
|
||||
dateValue.toString().length < timestampLengthInMilliseconds1990
|
||||
) {
|
||||
timestamp = dateValue;
|
||||
}
|
||||
|
||||
if (!timestamp && (dateValue as string).length < timestampLengthInMilliseconds1990) {
|
||||
timestamp = parseInt(dateValue as string, 10);
|
||||
}
|
||||
|
||||
if (!timestamp) {
|
||||
timestamp = Math.floor(DateTime.fromMillis(parseInt(dateValue as string, 10)).toSeconds());
|
||||
}
|
||||
|
||||
if (!timestamp) {
|
||||
const description = `'${dateValue}' 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(
|
||||
node,
|
||||
`Invalid date/time in 'Received ${label[0].toUpperCase() + label.slice(1)}' field`,
|
||||
{
|
||||
description,
|
||||
itemIndex,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
query += ` ${label}:${timestamp}`;
|
||||
} else {
|
||||
query = `${label}:${timestamp}`;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
export function prepareQuery(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
fields: IDataObject,
|
||||
itemIndex: number,
|
||||
) {
|
||||
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) {
|
||||
qs.q = prepareTimestamp(
|
||||
this.getNode(),
|
||||
itemIndex,
|
||||
qs.q as string,
|
||||
qs.receivedAfter as string,
|
||||
'after',
|
||||
);
|
||||
delete qs.receivedAfter;
|
||||
}
|
||||
|
||||
if (qs.receivedBefore) {
|
||||
qs.q = prepareTimestamp(
|
||||
this.getNode(),
|
||||
itemIndex,
|
||||
qs.q as string,
|
||||
qs.receivedBefore as string,
|
||||
'before',
|
||||
);
|
||||
delete qs.receivedBefore;
|
||||
}
|
||||
|
||||
return qs;
|
||||
}
|
||||
|
||||
export function prepareEmailsInput(
|
||||
this: IExecuteFunctions | 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 | ILoadOptionsFunctions,
|
||||
itemIndex: number,
|
||||
appendAttribution = false,
|
||||
instanceId?: string,
|
||||
) {
|
||||
const emailType = this.getNodeParameter('emailType', itemIndex) as string;
|
||||
let message = (this.getNodeParameter('message', itemIndex, '') as string).trim();
|
||||
|
||||
if (appendAttribution) {
|
||||
const attributionText = 'This email was sent automatically with ';
|
||||
const link = createUtmCampaignLink('n8n-nodes-base.gmail', instanceId);
|
||||
if (emailType === 'html') {
|
||||
message = `
|
||||
${message}
|
||||
<br>
|
||||
<br>
|
||||
---
|
||||
<br>
|
||||
<em>${attributionText}<a href="${link}" target="_blank">n8n</a></em>
|
||||
`;
|
||||
} else {
|
||||
message = `${message}\n\n---\n${attributionText}n8n\n${'https://n8n.io'}`;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
body: '',
|
||||
htmlBody: '',
|
||||
};
|
||||
|
||||
if (emailType === 'html') {
|
||||
body.htmlBody = message;
|
||||
} else {
|
||||
body.body = message;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function prepareEmailAttachments(
|
||||
this: IExecuteFunctions,
|
||||
options: IDataObject,
|
||||
itemIndex: number,
|
||||
) {
|
||||
const attachmentsList: IDataObject[] = [];
|
||||
const attachments = options.attachmentsBinary as IDataObject[];
|
||||
|
||||
if (attachments && !isEmpty(attachments)) {
|
||||
for (const { property } of attachments) {
|
||||
for (const name of (property as string).split(',')) {
|
||||
const binaryData = this.helpers.assertBinaryData(itemIndex, name);
|
||||
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, name);
|
||||
|
||||
if (!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.snippet as string;
|
||||
if (snippet) {
|
||||
item.json.snippet = escapeHtml(snippet);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
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 || []).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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the labels to display them to user so that they can select them easily
|
||||
*/
|
||||
export async function 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;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.gmail",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.gmail/"
|
||||
}
|
||||
],
|
||||
"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/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["email", "human", "form", "wait", "hitl", "approval"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { GmailV1 } from './v1/GmailV1.node';
|
||||
import { GmailV2 } from './v2/GmailV2.node';
|
||||
|
||||
export class Gmail extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Gmail',
|
||||
name: 'gmail',
|
||||
icon: 'file:gmail.svg',
|
||||
group: ['transform'],
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume the Gmail API',
|
||||
defaultVersion: 2.2,
|
||||
builderHint: {
|
||||
relatedNodes: [
|
||||
{
|
||||
nodeType: 'n8n-nodes-base.gmailTrigger',
|
||||
relationHint:
|
||||
'Use Gmail Trigger for scheduled email fetching, which is simpler for user than Schedule Trigger with Gmail getAll',
|
||||
},
|
||||
],
|
||||
},
|
||||
schemaPath: 'Google/Gmail',
|
||||
};
|
||||
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new GmailV1(baseDescription),
|
||||
2: new GmailV2(baseDescription),
|
||||
2.1: new GmailV2(baseDescription),
|
||||
2.2: new GmailV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.gmailTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IPollFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
googleApiRequest,
|
||||
googleApiRequestAllItems,
|
||||
parseRawEmail,
|
||||
prepareQuery,
|
||||
simplifyOutput,
|
||||
} from './GenericFunctions';
|
||||
import type {
|
||||
GmailTriggerFilters,
|
||||
GmailTriggerOptions,
|
||||
GmailWorkflowStaticData,
|
||||
GmailWorkflowStaticDataDictionary,
|
||||
Label,
|
||||
Message,
|
||||
MessageListResponse,
|
||||
} from './types';
|
||||
|
||||
export class GmailTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Gmail Trigger',
|
||||
name: 'gmailTrigger',
|
||||
icon: 'file:gmail.svg',
|
||||
group: ['trigger'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
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: [NodeConnectionTypes.Main],
|
||||
hints: [
|
||||
{
|
||||
type: 'info',
|
||||
message:
|
||||
'Multiple items will be returned if multiple messages are received within the polling interval. Make sure your workflow can handle multiple items.',
|
||||
whenToDisplay: 'beforeExecution',
|
||||
location: 'outputPane',
|
||||
},
|
||||
],
|
||||
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',
|
||||
builderHint: {
|
||||
message:
|
||||
'Set to false when the email body is needed for AI analysis, summarization, or content processing. When true, only returns snippet (preview text). When false, returns full email with {id, threadId, labelIds, headers, html, text, textAsHtml, subject, date, to, from, messageId, replyTo}.',
|
||||
},
|
||||
},
|
||||
{
|
||||
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: 'Include Drafts',
|
||||
name: 'includeDrafts',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to include email drafts 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/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Search',
|
||||
name: 'q',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'has:attachment',
|
||||
builderHint: {
|
||||
message:
|
||||
'Always set a search query to filter emails. Uses Gmail search syntax, e.g. "from:example@gmail.com", "subject:invoice", "has:attachment", "label:important", "newer_than:1d". Combine with spaces for AND: "from:shop@example.com subject:delivery". Without this filter, ALL incoming emails will trigger the workflow.',
|
||||
},
|
||||
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 email's attachments will be downloaded",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the labels to display them to user so that they 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',
|
||||
)) as Label[];
|
||||
|
||||
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 poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const workflowStaticData = this.getWorkflowStaticData('node') as
|
||||
| GmailWorkflowStaticData
|
||||
| GmailWorkflowStaticDataDictionary;
|
||||
const node = this.getNode();
|
||||
|
||||
let nodeStaticData = (workflowStaticData ?? {}) as GmailWorkflowStaticData;
|
||||
if (node.typeVersion > 1) {
|
||||
const nodeName = node.name;
|
||||
const dictionary = workflowStaticData as GmailWorkflowStaticDataDictionary;
|
||||
if (!(nodeName in workflowStaticData)) {
|
||||
dictionary[nodeName] = {};
|
||||
}
|
||||
|
||||
nodeStaticData = dictionary[nodeName];
|
||||
}
|
||||
|
||||
const now = Math.floor(DateTime.now().toSeconds()).toString();
|
||||
|
||||
if (this.getMode() !== 'manual') {
|
||||
nodeStaticData.lastTimeChecked ??= +now;
|
||||
}
|
||||
const startDate = nodeStaticData.lastTimeChecked ?? +now;
|
||||
|
||||
const options = this.getNodeParameter('options', {}) as GmailTriggerOptions;
|
||||
const filters = this.getNodeParameter('filters', {}) as GmailTriggerFilters;
|
||||
|
||||
let responseData: INodeExecutionData[] = [];
|
||||
const allFetchedMessages: Message[] = [];
|
||||
|
||||
try {
|
||||
const qs: IDataObject = {};
|
||||
const allFilters: GmailTriggerFilters = { ...filters, receivedAfter: startDate };
|
||||
|
||||
if (this.getMode() === 'manual') {
|
||||
qs.maxResults = 1;
|
||||
delete allFilters.receivedAfter;
|
||||
}
|
||||
|
||||
Object.assign(qs, prepareQuery.call(this, allFilters, 0), options);
|
||||
|
||||
const messagesResponse: MessageListResponse = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/gmail/v1/users/me/messages',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
const messages = messagesResponse.messages ?? [];
|
||||
|
||||
if (!messages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const simple = this.getNodeParameter('simple') as boolean;
|
||||
|
||||
if (simple) {
|
||||
qs.format = 'metadata';
|
||||
qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject'];
|
||||
} else {
|
||||
qs.format = 'raw';
|
||||
}
|
||||
|
||||
let includeDrafts = false;
|
||||
if (node.typeVersion > 1.1) {
|
||||
includeDrafts = filters.includeDrafts ?? false;
|
||||
} else {
|
||||
includeDrafts = filters.includeDrafts ?? true;
|
||||
}
|
||||
|
||||
delete qs.includeDrafts;
|
||||
|
||||
for (const message of messages) {
|
||||
const fullMessage = (await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/gmail/v1/users/me/messages/${message.id}`,
|
||||
{},
|
||||
qs,
|
||||
)) as Message;
|
||||
|
||||
allFetchedMessages.push(fullMessage);
|
||||
|
||||
if (!includeDrafts) {
|
||||
if (fullMessage.labelIds?.includes('DRAFT')) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (
|
||||
node.typeVersion > 1.2 &&
|
||||
fullMessage.labelIds?.includes('SENT') &&
|
||||
!fullMessage.labelIds?.includes('INBOX')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!simple) {
|
||||
const dataPropertyNameDownload =
|
||||
options.dataPropertyAttachmentsPrefixName || 'attachment_';
|
||||
|
||||
const parsed = await parseRawEmail.call(this, fullMessage, dataPropertyNameDownload);
|
||||
responseData.push(parsed);
|
||||
} else {
|
||||
responseData.push({ json: fullMessage });
|
||||
}
|
||||
}
|
||||
|
||||
if (simple) {
|
||||
responseData = this.helpers.returnJsonArray(
|
||||
await simplifyOutput.call(
|
||||
this,
|
||||
responseData.map((item) => item.json),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.getMode() === 'manual' || !nodeStaticData.lastTimeChecked) {
|
||||
throw error;
|
||||
}
|
||||
const workflow = this.getWorkflow();
|
||||
this.logger.error(
|
||||
`There was a problem in '${node.name}' node in workflow '${workflow.id}': '${error.description}'`,
|
||||
{
|
||||
node: node.name,
|
||||
workflowId: workflow.id,
|
||||
error,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!allFetchedMessages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emailsWithInvalidDate = new Set<string>();
|
||||
|
||||
const getEmailDateAsSeconds = (email: Message): number => {
|
||||
let date;
|
||||
|
||||
if (email.internalDate) {
|
||||
date = +email.internalDate / 1000;
|
||||
} else if (email.date) {
|
||||
date = +DateTime.fromJSDate(new Date(email.date)).toSeconds();
|
||||
} else if (email.headers?.date) {
|
||||
date = +DateTime.fromJSDate(new Date(email.headers.date)).toSeconds();
|
||||
}
|
||||
|
||||
if (!date || isNaN(date)) {
|
||||
emailsWithInvalidDate.add(email.id);
|
||||
return +startDate;
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
const lastEmailDate = allFetchedMessages.reduce((lastDate, message) => {
|
||||
const emailDate = getEmailDateAsSeconds(message);
|
||||
return emailDate > lastDate ? emailDate : lastDate;
|
||||
}, 0);
|
||||
|
||||
const nextPollPossibleDuplicates = allFetchedMessages.reduce((duplicates, message) => {
|
||||
const emailDate = getEmailDateAsSeconds(message);
|
||||
return emailDate <= lastEmailDate ? duplicates.concat(message.id) : duplicates;
|
||||
}, Array.from(emailsWithInvalidDate));
|
||||
|
||||
const possibleDuplicates = new Set(nodeStaticData.possibleDuplicates ?? []);
|
||||
if (possibleDuplicates.size > 0) {
|
||||
responseData = responseData.filter(({ json }) => {
|
||||
if (!json || typeof json.id !== 'string') return false;
|
||||
return !possibleDuplicates.has(json.id);
|
||||
});
|
||||
}
|
||||
|
||||
nodeStaticData.possibleDuplicates = nextPollPossibleDuplicates;
|
||||
nodeStaticData.lastTimeChecked = lastEmailDate ?? +startDate;
|
||||
|
||||
if (Array.isArray(responseData) && responseData.length) {
|
||||
return [responseData];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"message-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"textAsHtml": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content-transfer-encoding": {
|
||||
"type": "string"
|
||||
},
|
||||
"content-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"message-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"received": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"textAsHtml": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"messageListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"messageListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"messagesTotal": {
|
||||
"type": "integer"
|
||||
},
|
||||
"messagesUnread": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadsTotal": {
|
||||
"type": "integer"
|
||||
},
|
||||
"threadsUnread": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"messageListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 4
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arc-authentication-results": {
|
||||
"type": "string"
|
||||
},
|
||||
"arc-message-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"arc-seal": {
|
||||
"type": "string"
|
||||
},
|
||||
"authentication-results": {
|
||||
"type": "string"
|
||||
},
|
||||
"content-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"delivered-to": {
|
||||
"type": "string"
|
||||
},
|
||||
"dkim-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"message-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"received": {
|
||||
"type": "string"
|
||||
},
|
||||
"received-spf": {
|
||||
"type": "string"
|
||||
},
|
||||
"return-path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-google-smtp-source": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-received": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"textAsHtml": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 6
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arc-authentication-results": {
|
||||
"type": "string"
|
||||
},
|
||||
"arc-message-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"arc-seal": {
|
||||
"type": "string"
|
||||
},
|
||||
"authentication-results": {
|
||||
"type": "string"
|
||||
},
|
||||
"content-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"delivered-to": {
|
||||
"type": "string"
|
||||
},
|
||||
"dkim-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"message-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"received": {
|
||||
"type": "string"
|
||||
},
|
||||
"received-spf": {
|
||||
"type": "string"
|
||||
},
|
||||
"return-path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-gm-message-state": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-google-dkim-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-google-smtp-source": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-received": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 7
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"internalDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"approved": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ARC-Authentication-Results": {
|
||||
"type": "string"
|
||||
},
|
||||
"ARC-Message-Signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"ARC-Seal": {
|
||||
"type": "string"
|
||||
},
|
||||
"Authentication-Results": {
|
||||
"type": "string"
|
||||
},
|
||||
"Content-Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Date": {
|
||||
"type": "string"
|
||||
},
|
||||
"Delivered-To": {
|
||||
"type": "string"
|
||||
},
|
||||
"DKIM-Signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"From": {
|
||||
"type": "string"
|
||||
},
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"internalDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Message-ID": {
|
||||
"type": "string"
|
||||
},
|
||||
"MIME-Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"partId": {
|
||||
"type": "string"
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"partId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Received": {
|
||||
"type": "string"
|
||||
},
|
||||
"Received-SPF": {
|
||||
"type": "string"
|
||||
},
|
||||
"Return-Path": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string"
|
||||
},
|
||||
"Subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"To": {
|
||||
"type": "string"
|
||||
},
|
||||
"X-Google-Smtp-Source": {
|
||||
"type": "string"
|
||||
},
|
||||
"X-Received": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 4
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"messageListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"messageListVisibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arc-authentication-results": {
|
||||
"type": "string"
|
||||
},
|
||||
"arc-message-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"arc-seal": {
|
||||
"type": "string"
|
||||
},
|
||||
"authentication-results": {
|
||||
"type": "string"
|
||||
},
|
||||
"content-type": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"delivered-to": {
|
||||
"type": "string"
|
||||
},
|
||||
"dkim-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"message-id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime-version": {
|
||||
"type": "string"
|
||||
},
|
||||
"received": {
|
||||
"type": "string"
|
||||
},
|
||||
"received-spf": {
|
||||
"type": "string"
|
||||
},
|
||||
"return-path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-gm-message-state": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-google-dkim-signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-google-smtp-source": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-received": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"textAsHtml": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"From": {
|
||||
"type": "string"
|
||||
},
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"internalDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string"
|
||||
},
|
||||
"Subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"To": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"approved": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ARC-Authentication-Results": {
|
||||
"type": "string"
|
||||
},
|
||||
"ARC-Message-Signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"ARC-Seal": {
|
||||
"type": "string"
|
||||
},
|
||||
"Authentication-Results": {
|
||||
"type": "string"
|
||||
},
|
||||
"Content-Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Date": {
|
||||
"type": "string"
|
||||
},
|
||||
"Delivered-To": {
|
||||
"type": "string"
|
||||
},
|
||||
"DKIM-Signature": {
|
||||
"type": "string"
|
||||
},
|
||||
"From": {
|
||||
"type": "string"
|
||||
},
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"internalDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"partId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Received": {
|
||||
"type": "string"
|
||||
},
|
||||
"Received-SPF": {
|
||||
"type": "string"
|
||||
},
|
||||
"Return-Path": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeEstimate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string"
|
||||
},
|
||||
"Subject": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"To": {
|
||||
"type": "string"
|
||||
},
|
||||
"X-Received": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="193" preserveAspectRatio="xMidYMid"><path fill="#4285F4" d="M58.182 192.05V93.14L27.507 65.077 0 49.504v125.091c0 9.658 7.825 17.455 17.455 17.455z"/><path fill="#34A853" d="M197.818 192.05h40.727c9.659 0 17.455-7.826 17.455-17.455V49.505l-31.156 17.837-27.026 25.798z"/><path fill="#EA4335" d="m58.182 93.14-4.174-38.647 4.174-36.989L128 69.868l69.818-52.364 4.67 34.992-4.67 40.644L128 145.504z"/><path fill="#FBBC04" d="M197.818 17.504V93.14L256 49.504V26.231c0-21.585-24.64-33.89-41.89-20.945z"/><path fill="#C5221F" d="m0 49.504 26.759 20.07L58.182 93.14V17.504L41.89 5.286C24.61-7.66 0 4.646 0 26.23z"/></svg>
|
||||
|
After Width: | Height: | Size: 675 B |
@@ -0,0 +1,601 @@
|
||||
import * as mailparser from 'mailparser';
|
||||
import nock from 'nock';
|
||||
|
||||
import { testPollingTriggerNode } from '@test/nodes/TriggerHelpers';
|
||||
|
||||
import { GmailTrigger } from '../GmailTrigger.node';
|
||||
import type { Message, ListMessage, MessageListResponse } from '../types';
|
||||
|
||||
jest.mock('mailparser');
|
||||
|
||||
describe('GmailTrigger', () => {
|
||||
const baseUrl = 'https://www.googleapis.com';
|
||||
|
||||
function createMessage(message: Partial<Message> = {}): Message {
|
||||
const content = Buffer.from('test');
|
||||
const contentBase64 = content.toString('base64');
|
||||
const size = content.byteLength;
|
||||
|
||||
return {
|
||||
historyId: 'testHistoryId',
|
||||
id: 'testId',
|
||||
internalDate: '1727777957863',
|
||||
raw: contentBase64,
|
||||
labelIds: ['testLabelId'],
|
||||
sizeEstimate: size,
|
||||
snippet: content.toString('utf-8'),
|
||||
threadId: 'testThreadId',
|
||||
payload: {
|
||||
body: { attachmentId: 'testAttachmentId', data: contentBase64, size },
|
||||
filename: 'foo.txt',
|
||||
headers: [{ name: 'testHeader', value: 'testHeaderValue' }],
|
||||
mimeType: 'text/plain',
|
||||
partId: 'testPartId',
|
||||
parts: [],
|
||||
},
|
||||
...message,
|
||||
};
|
||||
}
|
||||
|
||||
function createListMessage(message: Partial<ListMessage> = {}): ListMessage {
|
||||
return { id: 'testId', threadId: 'testThreadId', ...message };
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(mailparser, 'simpleParser').mockResolvedValue({
|
||||
headers: new Map([['headerKey', 'headerValue']]),
|
||||
attachments: [],
|
||||
headerLines: [{ key: 'headerKey', line: 'headerValue' }],
|
||||
html: '<p>test</p>',
|
||||
date: new Date('2024-08-31'),
|
||||
from: {
|
||||
text: 'from@example.com',
|
||||
value: [{ name: 'From', address: 'from@example.com' }],
|
||||
html: 'from@example.com',
|
||||
},
|
||||
to: {
|
||||
text: 'to@example.com',
|
||||
value: [{ name: 'To', address: 'to@example.com' }],
|
||||
html: 'to@example.com',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return incoming emails', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' }), createListMessage({ id: '2' })],
|
||||
resultSizeEstimate: 123,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1' }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2' }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger);
|
||||
|
||||
expect(response).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
date: '2024-08-31T00:00:00.000Z',
|
||||
from: {
|
||||
html: 'from@example.com',
|
||||
text: 'from@example.com',
|
||||
value: [{ address: 'from@example.com', name: 'From' }],
|
||||
},
|
||||
headers: { headerKey: 'headerValue' },
|
||||
html: '<p>test</p>',
|
||||
id: '1',
|
||||
labelIds: ['testLabelId'],
|
||||
sizeEstimate: 4,
|
||||
threadId: 'testThreadId',
|
||||
to: {
|
||||
html: 'to@example.com',
|
||||
text: 'to@example.com',
|
||||
value: [{ address: 'to@example.com', name: 'To' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
date: '2024-08-31T00:00:00.000Z',
|
||||
from: {
|
||||
html: 'from@example.com',
|
||||
text: 'from@example.com',
|
||||
value: [{ address: 'from@example.com', name: 'From' }],
|
||||
},
|
||||
headers: { headerKey: 'headerValue' },
|
||||
html: '<p>test</p>',
|
||||
id: '2',
|
||||
labelIds: ['testLabelId'],
|
||||
sizeEstimate: 4,
|
||||
threadId: 'testThreadId',
|
||||
to: {
|
||||
html: 'to@example.com',
|
||||
text: 'to@example.com',
|
||||
value: [{ address: 'to@example.com', name: 'To' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should simplify output when enabled', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' }), createListMessage({ id: '2' })],
|
||||
resultSizeEstimate: 123,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1' }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2' }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { simple: true } },
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
historyId: 'testHistoryId',
|
||||
id: '1',
|
||||
internalDate: '1727777957863',
|
||||
labels: [{ id: 'testLabelId', name: 'Test Label Name' }],
|
||||
payload: {
|
||||
body: { attachmentId: 'testAttachmentId', data: 'dGVzdA==', size: 4 },
|
||||
filename: 'foo.txt',
|
||||
mimeType: 'text/plain',
|
||||
partId: 'testPartId',
|
||||
parts: [],
|
||||
},
|
||||
raw: 'dGVzdA==',
|
||||
sizeEstimate: 4,
|
||||
snippet: 'test',
|
||||
testHeader: 'testHeaderValue',
|
||||
threadId: 'testThreadId',
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
historyId: 'testHistoryId',
|
||||
id: '2',
|
||||
internalDate: '1727777957863',
|
||||
labels: [{ id: 'testLabelId', name: 'Test Label Name' }],
|
||||
payload: {
|
||||
body: { attachmentId: 'testAttachmentId', data: 'dGVzdA==', size: 4 },
|
||||
filename: 'foo.txt',
|
||||
mimeType: 'text/plain',
|
||||
partId: 'testPartId',
|
||||
parts: [],
|
||||
},
|
||||
raw: 'dGVzdA==',
|
||||
sizeEstimate: 4,
|
||||
snippet: 'test',
|
||||
testHeader: 'testHeaderValue',
|
||||
threadId: 'testThreadId',
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter out emails that were already processed', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [],
|
||||
resultSizeEstimate: 0,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { simple: true } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': { lastTimeChecked: new Date('2024-10-31').getTime() / 1000 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toEqual(null);
|
||||
});
|
||||
|
||||
it('should handle duplicates and different date fields', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [
|
||||
createListMessage({ id: '1' }),
|
||||
createListMessage({ id: '2' }),
|
||||
createListMessage({ id: '3' }),
|
||||
createListMessage({ id: '4' }),
|
||||
createListMessage({ id: '5' }),
|
||||
],
|
||||
resultSizeEstimate: 123,
|
||||
};
|
||||
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', internalDate: '1727777957863', date: undefined }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2', internalDate: undefined, date: '1727777957863' }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/3?.*'))
|
||||
.reply(
|
||||
200,
|
||||
createMessage({
|
||||
id: '3',
|
||||
internalDate: undefined,
|
||||
date: undefined,
|
||||
headers: { date: 'Thu, 5 Dec 2024 08:30:00 -0800' },
|
||||
}),
|
||||
);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/4?.*'))
|
||||
.reply(
|
||||
200,
|
||||
createMessage({
|
||||
id: '4',
|
||||
internalDate: undefined,
|
||||
date: undefined,
|
||||
headers: undefined,
|
||||
}),
|
||||
);
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages/5?.*')).reply(200, {});
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { simple: true } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': {
|
||||
lastTimeChecked: new Date('2024-10-31').getTime() / 1000,
|
||||
possibleDuplicates: ['1'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should skip DRAFTS when option is set', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' }), createListMessage({ id: '2' })],
|
||||
resultSizeEstimate: 2,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, {
|
||||
labels: [
|
||||
{ id: 'INBOX', name: 'INBOX' },
|
||||
{ id: 'DRAFT', name: 'DRAFT' },
|
||||
],
|
||||
});
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', labelIds: ['DRAFT'] }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2', labelIds: ['INBOX'] }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { filters: { includeDrafts: false } } },
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
[
|
||||
{
|
||||
binary: undefined,
|
||||
json: {
|
||||
attachements: undefined,
|
||||
date: '2024-08-31T00:00:00.000Z',
|
||||
from: {
|
||||
html: 'from@example.com',
|
||||
text: 'from@example.com',
|
||||
value: [{ address: 'from@example.com', name: 'From' }],
|
||||
},
|
||||
headerlines: undefined,
|
||||
headers: { headerKey: 'headerValue' },
|
||||
html: '<p>test</p>',
|
||||
id: '2',
|
||||
labelIds: ['INBOX'],
|
||||
sizeEstimate: 4,
|
||||
threadId: 'testThreadId',
|
||||
to: {
|
||||
html: 'to@example.com',
|
||||
text: 'to@example.com',
|
||||
value: [{ address: 'to@example.com', name: 'To' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip emails with SENT label', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' }), createListMessage({ id: '2' })],
|
||||
resultSizeEstimate: 2,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, {
|
||||
labels: [
|
||||
{ id: 'INBOX', name: 'INBOX' },
|
||||
{ id: 'SENT', name: 'SENT' },
|
||||
],
|
||||
});
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', labelIds: ['INBOX'] }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2', labelIds: ['SENT'] }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger);
|
||||
|
||||
expect(response).toEqual([
|
||||
[
|
||||
{
|
||||
binary: undefined,
|
||||
json: {
|
||||
attachements: undefined,
|
||||
date: '2024-08-31T00:00:00.000Z',
|
||||
from: {
|
||||
html: 'from@example.com',
|
||||
text: 'from@example.com',
|
||||
value: [{ address: 'from@example.com', name: 'From' }],
|
||||
},
|
||||
headerlines: undefined,
|
||||
headers: { headerKey: 'headerValue' },
|
||||
html: '<p>test</p>',
|
||||
id: '1',
|
||||
labelIds: ['INBOX'],
|
||||
sizeEstimate: 4,
|
||||
threadId: 'testThreadId',
|
||||
to: {
|
||||
html: 'to@example.com',
|
||||
text: 'to@example.com',
|
||||
value: [{ address: 'to@example.com', name: 'To' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not skip emails that were sent to own account', async () => {
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' }), createListMessage({ id: '2' })],
|
||||
resultSizeEstimate: 2,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, {
|
||||
labels: [
|
||||
{ id: 'INBOX', name: 'INBOX' },
|
||||
{ id: 'SENT', name: 'SENT' },
|
||||
],
|
||||
});
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', labelIds: ['INBOX', 'SENT'] }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2', labelIds: ['SENT'] }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger);
|
||||
expect(response).toEqual([
|
||||
[
|
||||
{
|
||||
binary: undefined,
|
||||
json: {
|
||||
attachements: undefined,
|
||||
date: '2024-08-31T00:00:00.000Z',
|
||||
from: {
|
||||
html: 'from@example.com',
|
||||
text: 'from@example.com',
|
||||
value: [{ address: 'from@example.com', name: 'From' }],
|
||||
},
|
||||
headerlines: undefined,
|
||||
headers: { headerKey: 'headerValue' },
|
||||
html: '<p>test</p>',
|
||||
id: '1',
|
||||
labelIds: ['INBOX', 'SENT'],
|
||||
sizeEstimate: 4,
|
||||
threadId: 'testThreadId',
|
||||
to: {
|
||||
html: 'to@example.com',
|
||||
text: 'to@example.com',
|
||||
value: [{ address: 'to@example.com', name: 'To' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple emails with the same timestamp', async () => {
|
||||
const timestamp = '1727777957000';
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [
|
||||
createListMessage({ id: '1' }),
|
||||
createListMessage({ id: '2' }),
|
||||
createListMessage({ id: '3' }),
|
||||
],
|
||||
resultSizeEstimate: 3,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', internalDate: timestamp }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2', internalDate: timestamp }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/3?.*'))
|
||||
.reply(200, createMessage({ id: '3', internalDate: timestamp }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { simple: true } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': {
|
||||
lastTimeChecked: Number(timestamp) / 1000,
|
||||
possibleDuplicates: ['1', '2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toEqual([[{ json: expect.objectContaining({ id: '3' }) }]]);
|
||||
});
|
||||
|
||||
it('should not skip emails when no messages are found', async () => {
|
||||
const initialTimestamp = 1727777957;
|
||||
const emailTimestamp = String((initialTimestamp + 1) * 1000);
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' })],
|
||||
resultSizeEstimate: 1,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', internalDate: emailTimestamp }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { simple: true } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': { lastTimeChecked: initialTimestamp },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toEqual([[{ json: expect.objectContaining({ id: '1' }) }]]);
|
||||
});
|
||||
|
||||
it('should update timestamp even when all emails are filtered (prevents infinite re-fetch)', async () => {
|
||||
const initialTimestamp = 1727777957;
|
||||
const draftEmailTimestamp = String((initialTimestamp + 1) * 1000);
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: 'draft-1' })],
|
||||
resultSizeEstimate: 1,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, {
|
||||
labels: [
|
||||
{ id: 'INBOX', name: 'INBOX' },
|
||||
{ id: 'DRAFT', name: 'DRAFT' },
|
||||
],
|
||||
});
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/draft-1?.*'))
|
||||
.reply(
|
||||
200,
|
||||
createMessage({ id: 'draft-1', internalDate: draftEmailTimestamp, labelIds: ['DRAFT'] }),
|
||||
);
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { filters: { includeDrafts: false } } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': { lastTimeChecked: initialTimestamp },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle emails with invalid dates by using startDate fallback', async () => {
|
||||
const initialTimestamp = 1727777957;
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [createListMessage({ id: '1' })],
|
||||
resultSizeEstimate: 1,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, { labels: [{ id: 'testLabelId', name: 'Test Label Name' }] });
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
// Email without any date fields - should be treated as invalid
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', internalDate: undefined }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { simple: true } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': { lastTimeChecked: initialTimestamp },
|
||||
},
|
||||
});
|
||||
|
||||
// Should still return the email even though it has invalid date
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response?.[0]?.[0]?.json?.id).toBe('1');
|
||||
});
|
||||
|
||||
it('should handle mixed valid and filtered emails with same timestamp', async () => {
|
||||
const timestamp = '1727777957000';
|
||||
const messageListResponse: MessageListResponse = {
|
||||
messages: [
|
||||
createListMessage({ id: '1' }),
|
||||
createListMessage({ id: '2' }),
|
||||
createListMessage({ id: '3' }),
|
||||
],
|
||||
resultSizeEstimate: 3,
|
||||
};
|
||||
nock(baseUrl)
|
||||
.get('/gmail/v1/users/me/labels')
|
||||
.reply(200, {
|
||||
labels: [
|
||||
{ id: 'INBOX', name: 'INBOX' },
|
||||
{ id: 'DRAFT', name: 'DRAFT' },
|
||||
],
|
||||
});
|
||||
nock(baseUrl).get(new RegExp('/gmail/v1/users/me/messages?.*')).reply(200, messageListResponse);
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/1?.*'))
|
||||
.reply(200, createMessage({ id: '1', internalDate: timestamp, labelIds: ['INBOX'] }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/2?.*'))
|
||||
.reply(200, createMessage({ id: '2', internalDate: timestamp, labelIds: ['DRAFT'] }));
|
||||
nock(baseUrl)
|
||||
.get(new RegExp('/gmail/v1/users/me/messages/3?.*'))
|
||||
.reply(200, createMessage({ id: '3', internalDate: timestamp, labelIds: ['INBOX'] }));
|
||||
|
||||
const { response } = await testPollingTriggerNode(GmailTrigger, {
|
||||
node: { parameters: { filters: { includeDrafts: false } } },
|
||||
workflowStaticData: {
|
||||
'Gmail Trigger': {
|
||||
lastTimeChecked: Number(timestamp) / 1000 - 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Should return 2 emails (1 and 3), filtering out the draft (2)
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response?.[0]).toHaveLength(2);
|
||||
expect(response?.[0]?.[0]?.json?.id).toBe('1');
|
||||
expect(response?.[0]?.[1]?.json?.id).toBe('3');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GmailTrigger should handle duplicates and different date fields 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"date": "1727777957863",
|
||||
"historyId": "testHistoryId",
|
||||
"id": "2",
|
||||
"labels": [
|
||||
{
|
||||
"id": "testLabelId",
|
||||
"name": "Test Label Name",
|
||||
},
|
||||
],
|
||||
"payload": {
|
||||
"body": {
|
||||
"attachmentId": "testAttachmentId",
|
||||
"data": "dGVzdA==",
|
||||
"size": 4,
|
||||
},
|
||||
"filename": "foo.txt",
|
||||
"mimeType": "text/plain",
|
||||
"partId": "testPartId",
|
||||
"parts": [],
|
||||
},
|
||||
"raw": "dGVzdA==",
|
||||
"sizeEstimate": 4,
|
||||
"snippet": "test",
|
||||
"testHeader": "testHeaderValue",
|
||||
"threadId": "testThreadId",
|
||||
},
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"headers": {
|
||||
"date": "Thu, 5 Dec 2024 08:30:00 -0800",
|
||||
},
|
||||
"historyId": "testHistoryId",
|
||||
"id": "3",
|
||||
"labels": [
|
||||
{
|
||||
"id": "testLabelId",
|
||||
"name": "Test Label Name",
|
||||
},
|
||||
],
|
||||
"payload": {
|
||||
"body": {
|
||||
"attachmentId": "testAttachmentId",
|
||||
"data": "dGVzdA==",
|
||||
"size": 4,
|
||||
},
|
||||
"filename": "foo.txt",
|
||||
"mimeType": "text/plain",
|
||||
"partId": "testPartId",
|
||||
"parts": [],
|
||||
},
|
||||
"raw": "dGVzdA==",
|
||||
"sizeEstimate": 4,
|
||||
"snippet": "test",
|
||||
"testHeader": "testHeaderValue",
|
||||
"threadId": "testThreadId",
|
||||
},
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"historyId": "testHistoryId",
|
||||
"id": "4",
|
||||
"labels": [
|
||||
{
|
||||
"id": "testLabelId",
|
||||
"name": "Test Label Name",
|
||||
},
|
||||
],
|
||||
"payload": {
|
||||
"body": {
|
||||
"attachmentId": "testAttachmentId",
|
||||
"data": "dGVzdA==",
|
||||
"size": 4,
|
||||
},
|
||||
"filename": "foo.txt",
|
||||
"mimeType": "text/plain",
|
||||
"partId": "testPartId",
|
||||
"parts": [],
|
||||
},
|
||||
"raw": "dGVzdA==",
|
||||
"sizeEstimate": 4,
|
||||
"snippet": "test",
|
||||
"testHeader": "testHeaderValue",
|
||||
"threadId": "testThreadId",
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "CHAT",
|
||||
"name": "CHAT",
|
||||
"messageListVisibility": "hide",
|
||||
"labelListVisibility": "labelHide",
|
||||
"type": "system"
|
||||
},
|
||||
{
|
||||
"id": "SENT",
|
||||
"name": "SENT",
|
||||
"type": "system"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,240 @@
|
||||
[
|
||||
{
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "multipart/alternative",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "ABC12345+EXAMPLE123456789"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Thu, 5 Dec 2024 08:30:00 -0800"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "Holiday Deals <promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<12345abc67890@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Exclusive Holiday Discounts!"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 1234,
|
||||
"data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4="
|
||||
}
|
||||
},
|
||||
{
|
||||
"partId": "1",
|
||||
"mimeType": "text/html",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"utf-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 5678,
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4="
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 67890,
|
||||
"historyId": "54321",
|
||||
"internalDate": "1733405400000"
|
||||
},
|
||||
{
|
||||
"id": "z9y8x7w6v5u4t3s2",
|
||||
"threadId": "z9y8x7w6v5u4t3s2",
|
||||
"labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"],
|
||||
"snippet": "Your friend John just shared a new photo with you! Check it out now.",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "multipart/alternative",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "exampleuser2@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id def456ghi789; Fri, 6 Dec 2024 09:45:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "XYZ67890+EXAMPLE0987654321"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 198.51.100.2 with SMTP id 67890def12345; Fri, 6 Dec 2024 09:45:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733490900; cv=none; d=example2.com; s=arc-20241206; b=HIJKLMN987654="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example2.com; s=arc-20241206; bh=EXAMPLEHASH67890="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example2.com; dkim=pass header.i=@social.example2.com; spf=pass smtp.mailfrom=notifications@social.example2.com; dmarc=pass header.from=example2.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<notifications@social.example2.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Fri, 6 Dec 2024 09:45:00 -0800"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "John's Photos <notifications@social.example2.com>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "exampleuser2@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<67890def12345@social.example2.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "John shared a new photo with you!"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_67890_12345.1733490900000\""
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 4321,
|
||||
"data": "U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gSm9obi4gV2UgaG9wZSB5b3UgbGlrZSBpdCE="
|
||||
}
|
||||
},
|
||||
{
|
||||
"partId": "1",
|
||||
"mimeType": "text/html",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"utf-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"size": 8765,
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+U2VlIHRoZSBhdHRhY2hlZCBwaG90byBhbmQgcmVwbHkgdG8gPGI+Sm9obi48L2I+IFdlIGhvcGUgeW91IGxpa2UgaXQhPC9kaXY+"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 54321,
|
||||
"historyId": "98765",
|
||||
"internalDate": "1733490900000"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,631 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
encodeEmail,
|
||||
googleApiRequest,
|
||||
prepareEmailAttachments,
|
||||
prepareEmailBody,
|
||||
prepareEmailsInput,
|
||||
} from '../../GenericFunctions';
|
||||
import type { GmailMessage, GmailMessageMetadata, GmailUserProfile } from '../../types';
|
||||
import { replyToEmail } from '../../utils/replyToEmail';
|
||||
|
||||
jest.mock('../../GenericFunctions', () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
...jest.requireActual('../../GenericFunctions'),
|
||||
googleApiRequest: jest.fn(),
|
||||
prepareEmailsInput: jest.fn(),
|
||||
prepareEmailAttachments: jest.fn(),
|
||||
prepareEmailBody: jest.fn(),
|
||||
encodeEmail: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedGoogleApiRequest = jest.mocked(googleApiRequest);
|
||||
const mockedPrepareEmailsInput = jest.mocked(prepareEmailsInput);
|
||||
const mockedPrepareEmailAttachments = jest.mocked(prepareEmailAttachments);
|
||||
const mockedPrepareEmailBody = jest.mocked(prepareEmailBody);
|
||||
const mockedEncodeEmail = jest.mocked(encodeEmail);
|
||||
|
||||
describe('replyToEmail', () => {
|
||||
let mockExecuteFunctions: IExecuteFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockedPrepareEmailsInput.mockReturnValue('test@example.com, ');
|
||||
mockedPrepareEmailAttachments.mockResolvedValue([]);
|
||||
mockedPrepareEmailBody.mockReturnValue({
|
||||
body: 'Test message body',
|
||||
htmlBody: '',
|
||||
});
|
||||
mockedEncodeEmail.mockResolvedValue('rawString');
|
||||
});
|
||||
|
||||
const mockMessageMetadata: GmailMessageMetadata = {
|
||||
id: 'message123',
|
||||
threadId: 'thread123',
|
||||
labelIds: ['INBOX'],
|
||||
payload: {
|
||||
partId: '',
|
||||
mimeType: 'text/plain',
|
||||
filename: '',
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Original Subject' },
|
||||
{ name: 'Message-ID', value: '<original@example.com>' },
|
||||
{ name: 'From', value: 'John Doe <john@example.com>' },
|
||||
{ name: 'To', value: 'recipient1@example.com,recipient2@example.com' },
|
||||
],
|
||||
body: { attachmentId: '', size: 0, data: '' },
|
||||
parts: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mockUserProfile: GmailUserProfile = {
|
||||
emailAddress: 'user@gmail.com',
|
||||
messagesTotal: 100,
|
||||
threadsTotal: 50,
|
||||
historyId: 'history123',
|
||||
};
|
||||
|
||||
const mockSentMessage: GmailMessage = {
|
||||
id: 'sent123',
|
||||
threadId: 'thread123',
|
||||
labelIds: ['SENT'],
|
||||
snippet: 'Reply message...',
|
||||
historyId: 'history124',
|
||||
sizeEstimate: 1000,
|
||||
raw: 'encoded-email-content',
|
||||
payload: mockMessageMetadata.payload,
|
||||
};
|
||||
|
||||
test('should reply to email with basic options', async () => {
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata) // GET message metadata
|
||||
.mockResolvedValueOnce(mockUserProfile) // GET user profile
|
||||
.mockResolvedValueOnce(mockSentMessage); // POST send message
|
||||
|
||||
const options: IDataObject = {};
|
||||
const result = await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'GET',
|
||||
'/gmail/v1/users/me/messages/message123',
|
||||
{},
|
||||
{ format: 'metadata' },
|
||||
);
|
||||
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(2, 'GET', '/gmail/v1/users/me/profile');
|
||||
|
||||
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'POST',
|
||||
'/gmail/v1/users/me/messages/send',
|
||||
expect.objectContaining({
|
||||
threadId: 'thread123',
|
||||
raw: expect.any(String),
|
||||
}),
|
||||
{ format: 'metadata' },
|
||||
);
|
||||
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
from: '',
|
||||
to: 'John Doe <john@example.com>, <recipient1@example.com>, <recipient2@example.com>',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
subject: 'Original Subject',
|
||||
attachments: [],
|
||||
inReplyTo: '<original@example.com>',
|
||||
reference: '<original@example.com>',
|
||||
body: 'Test message body',
|
||||
htmlBody: '',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockSentMessage);
|
||||
});
|
||||
|
||||
test('should handle CC list when provided', async () => {
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
mockedPrepareEmailsInput.mockReturnValue('cc@example.com, ');
|
||||
|
||||
const options: IDataObject = {
|
||||
ccList: 'cc@example.com',
|
||||
};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedPrepareEmailsInput).toHaveBeenCalledWith('cc@example.com', 'CC', 0);
|
||||
|
||||
// Verify encodeEmail was called with CC list
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cc: 'cc@example.com, ',
|
||||
bcc: '',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle BCC list when provided', async () => {
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
mockedPrepareEmailsInput.mockReturnValue('bcc@example.com, ');
|
||||
|
||||
const options: IDataObject = {
|
||||
bccList: 'bcc@example.com',
|
||||
};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedPrepareEmailsInput).toHaveBeenCalledWith('bcc@example.com', 'BCC', 0);
|
||||
|
||||
// Verify encodeEmail was called with BCC list
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cc: '',
|
||||
bcc: 'bcc@example.com, ',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle attachments when provided', async () => {
|
||||
const mockAttachments = [
|
||||
{ name: 'file1.txt', content: Buffer.from('content'), type: 'text/plain' },
|
||||
{ name: 'file2.pdf', content: Buffer.from('pdf content'), type: 'application/pdf' },
|
||||
];
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
mockedPrepareEmailAttachments.mockResolvedValue(mockAttachments);
|
||||
|
||||
const options: IDataObject = {
|
||||
attachmentsUi: {
|
||||
attachmentsBinary: [{ property: 'attachment1' }, { property: 'attachment2' }],
|
||||
},
|
||||
};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedPrepareEmailAttachments).toHaveBeenCalledWith(options.attachmentsUi, 0);
|
||||
|
||||
// Should use upload media endpoint when attachments are present
|
||||
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'POST',
|
||||
'/gmail/v1/users/me/messages/send',
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
userId: 'me',
|
||||
uploadType: 'media',
|
||||
format: 'metadata',
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify encodeEmail was called with attachments
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attachments: mockAttachments,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should reply to sender only when replyToSenderOnly is true', async () => {
|
||||
const messageWithMultipleRecipients = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Original Subject' },
|
||||
{ name: 'Message-ID', value: '<original@example.com>' },
|
||||
{ name: 'From', value: 'John Doe <john@example.com>' },
|
||||
{ name: 'To', value: 'recipient1@example.com, recipient2@example.com, user@gmail.com' },
|
||||
{ name: 'Cc', value: 'cc1@example.com, cc2@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithMultipleRecipients)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {
|
||||
replyToSenderOnly: true,
|
||||
};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Verify that only the sender is included in the "To" field
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
from: '',
|
||||
to: 'John Doe <john@example.com>',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
subject: 'Original Subject',
|
||||
inReplyTo: '<original@example.com>',
|
||||
reference: '<original@example.com>',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should reply to recipients only when replyToRecipientsOnly is true', async () => {
|
||||
const messageWithUserInTo = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Original Subject' },
|
||||
{ name: 'Message-ID', value: '<original@example.com>' },
|
||||
{ name: 'From', value: '<john@example.com>' },
|
||||
{ name: 'To', value: 'recipient1@example.com,user@gmail.com,recipient2@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithUserInTo)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {
|
||||
replyToRecipientsOnly: true,
|
||||
};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Should filter out the current user's email from recipients and exclude sender
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
from: '',
|
||||
// Should include sender and original recipients but filter out current user
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
to: expect.stringContaining('<recipient1@example.com>'),
|
||||
subject: 'Original Subject',
|
||||
inReplyTo: '<original@example.com>',
|
||||
reference: '<original@example.com>',
|
||||
}),
|
||||
);
|
||||
|
||||
// Should not include the current user's email address
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: '<recipient1@example.com>, <recipient2@example.com>',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should use custom sender name when provided', async () => {
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {
|
||||
senderName: 'Custom Sender Name',
|
||||
};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
from: 'Custom Sender Name <user@gmail.com>',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle emails with angle brackets correctly', async () => {
|
||||
const messageWithBrackets = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Original Subject' },
|
||||
{ name: 'Message-ID', value: '<original@example.com>' },
|
||||
{ name: 'From', value: 'John Doe <john@example.com>' },
|
||||
{ name: 'To', value: 'Regular Email <regular@example.com>, plain@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithBrackets)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Should handle both formats correctly
|
||||
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'POST',
|
||||
'/gmail/v1/users/me/messages/send',
|
||||
expect.objectContaining({
|
||||
threadId: 'thread123',
|
||||
}),
|
||||
{ format: 'metadata' },
|
||||
);
|
||||
});
|
||||
|
||||
test('should filter out current user email from recipients', async () => {
|
||||
const messageWithUserEmail = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Original Subject' },
|
||||
{ name: 'Message-ID', value: '<original@example.com>' },
|
||||
// user@gmail.com - current user, user_from@gmail.com - sender that should be excluded
|
||||
{ name: 'From', value: '<user_from@gmail.com>' },
|
||||
{ name: 'To', value: 'recipient@example.com,user@gmail.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithUserEmail)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedGoogleApiRequest).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: '<user_from@gmail.com>, <recipient@example.com>',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle missing subject header', async () => {
|
||||
const messageWithoutSubject = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Message-ID', value: '<original@example.com>' },
|
||||
{ name: 'From', value: 'John Doe <john@example.com>' },
|
||||
{ name: 'To', value: 'recipient@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithoutSubject)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Should handle missing subject gracefully (empty string)
|
||||
expect(mockedGoogleApiRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should handle missing message ID header', async () => {
|
||||
const messageWithoutMessageId = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Original Subject' },
|
||||
{ name: 'From', value: 'John Doe <john@example.com>' },
|
||||
{ name: 'To', value: 'recipient@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithoutMessageId)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Should handle missing message ID gracefully (empty string)
|
||||
expect(mockedGoogleApiRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should use prepareEmailBody for message content', async () => {
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
mockedPrepareEmailBody.mockReturnValue({
|
||||
body: 'Custom message content',
|
||||
htmlBody: '<p>Custom HTML content</p>',
|
||||
});
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedPrepareEmailBody).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('should encode email with proper structure including all required fields', async () => {
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(mockMessageMetadata)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
mockedPrepareEmailBody.mockReturnValue({
|
||||
body: 'Reply message body',
|
||||
htmlBody: '<p>Reply HTML body</p>',
|
||||
});
|
||||
|
||||
const options: IDataObject = {
|
||||
ccList: 'cc@example.com',
|
||||
bccList: 'bcc@example.com',
|
||||
senderName: 'Test Sender',
|
||||
};
|
||||
|
||||
mockedPrepareEmailsInput
|
||||
.mockReturnValueOnce('cc@example.com, ')
|
||||
.mockReturnValueOnce('bcc@example.com, ');
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith({
|
||||
from: 'Test Sender <user@gmail.com>',
|
||||
to: 'John Doe <john@example.com>, <recipient1@example.com>, <recipient2@example.com>',
|
||||
cc: 'cc@example.com, ',
|
||||
bcc: 'bcc@example.com, ',
|
||||
subject: 'Original Subject',
|
||||
attachments: [],
|
||||
inReplyTo: '<original@example.com>',
|
||||
reference: '<original@example.com>',
|
||||
body: 'Reply message body',
|
||||
htmlBody: '<p>Reply HTML body</p>',
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle missing headers gracefully in encodeEmail', async () => {
|
||||
const messageWithoutHeaders = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'From', value: 'sender@example.com' },
|
||||
// Missing Subject and Message-ID headers
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithoutHeaders)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Should handle missing headers with empty strings
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: '', // Empty when header is missing
|
||||
inReplyTo: '', // Empty when header is missing
|
||||
reference: '', // Empty when header is missing
|
||||
to: '<sender@example.com>',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should properly format email addresses with and without angle brackets', async () => {
|
||||
const messageWithMixedEmailFormats = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
{ name: 'Subject', value: 'Test Subject' },
|
||||
{ name: 'Message-ID', value: '<test@example.com>' },
|
||||
{ name: 'From', value: 'plain@example.com' }, // Without brackets
|
||||
{ name: 'To', value: 'Name <with@brackets.com>, plain2@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithMixedEmailFormats)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
// Should properly format emails with brackets
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
to: expect.stringContaining('<plain@example.com>'), // Should add brackets
|
||||
}),
|
||||
);
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
to: expect.stringContaining('Name <with@brackets.com>'), // Should keep existing brackets
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should use ignore Reply-To header, when Reply-To header is provided for version < 2.2', async () => {
|
||||
const messageWithReplyToHeader = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
...mockMessageMetadata.payload.headers,
|
||||
{ name: 'Reply-To', value: 'reply-to@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithReplyToHeader)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.1);
|
||||
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
to: expect.stringContaining('<john@example.com>'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should use Reply-To header instead of From, when Reply-To header is provided for version >= 2.2', async () => {
|
||||
const messageWithReplyToHeader = {
|
||||
...mockMessageMetadata,
|
||||
payload: {
|
||||
...mockMessageMetadata.payload,
|
||||
headers: [
|
||||
...mockMessageMetadata.payload.headers,
|
||||
{ name: 'Reply-To', value: 'reply-to@example.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockedGoogleApiRequest
|
||||
.mockResolvedValueOnce(messageWithReplyToHeader)
|
||||
.mockResolvedValueOnce(mockUserProfile)
|
||||
.mockResolvedValueOnce(mockSentMessage);
|
||||
|
||||
const options: IDataObject = {};
|
||||
|
||||
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
|
||||
|
||||
expect(mockedEncodeEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
to: expect.stringContaining('<reply-to@example.com>'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
import labels from '../fixtures/labels.json';
|
||||
import messages from '../fixtures/messages.json';
|
||||
|
||||
function normalizeDraftMail(mail: string) {
|
||||
let normalizedMail = mail.replace(/\r\n/g, '\n');
|
||||
normalizedMail = normalizedMail
|
||||
.replace(/boundary=\".*\"/g, 'boundary="--test-boundary"')
|
||||
.replace(/----.*/g, '----test-boundary')
|
||||
.replace(/^From:.*$/gm, '')
|
||||
.replace(/Message-ID:.*/g, 'Message-ID: test-message-id');
|
||||
|
||||
const parts = normalizedMail.split(/\n\n/);
|
||||
if (parts.length > 1) {
|
||||
const headerBlock = parts[0];
|
||||
const bodyBlock = parts.slice(1).join('\n\n');
|
||||
const headers = headerBlock.split(/\n/).filter(Boolean);
|
||||
const map = new Map<string, string>();
|
||||
headers.forEach((line) => {
|
||||
const idx = line.indexOf(':');
|
||||
if (idx > -1) map.set(line.slice(0, idx), line);
|
||||
});
|
||||
const ordered = ['Content-Type', 'Cc', 'Bcc', 'Subject', 'Message-ID', 'Date', 'MIME-Version']
|
||||
.map((k) => map.get(k))
|
||||
.filter(Boolean) as string[];
|
||||
normalizedMail = `${ordered.join('\n')}\n\n${bodyBlock}`;
|
||||
}
|
||||
|
||||
return normalizedMail;
|
||||
}
|
||||
|
||||
describe('Test Gmail Node v1', () => {
|
||||
beforeAll(() => {
|
||||
jest
|
||||
.useFakeTimers({ doNotFake: ['setImmediate', 'nextTick'] })
|
||||
.setSystemTime(new Date('2024-12-16 12:34:56.789Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Messages', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages')
|
||||
.query({
|
||||
includeSpamTrash: 'true',
|
||||
dataPropertyAttachmentsPrefixName: 'custom_attachment_',
|
||||
maxResults: '2',
|
||||
})
|
||||
.reply(200, { messages });
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/a1b2c3d4e5f6g7h8')
|
||||
.query({
|
||||
includeSpamTrash: 'true',
|
||||
dataPropertyAttachmentsPrefixName: 'custom_attachment_',
|
||||
maxResults: '2',
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
...messages[0],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/z9y8x7w6v5u4t3s2')
|
||||
.query({
|
||||
includeSpamTrash: 'true',
|
||||
dataPropertyAttachmentsPrefixName: 'custom_attachment_',
|
||||
maxResults: '2',
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
...messages[1],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
});
|
||||
gmailNock.delete('/v1/users/me/messages/test').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/test')
|
||||
.query({ format: 'raw' })
|
||||
.reply(200, {
|
||||
...messages[1],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
});
|
||||
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/send')
|
||||
.query({ userId: 'me', uploadType: 'media' })
|
||||
.reply(200, messages[0]);
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['messages.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Labels', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock
|
||||
.post('/v1/users/me/labels', {
|
||||
labelListVisibility: 'labelShow',
|
||||
messageListVisibility: 'show',
|
||||
name: 'Test Label Name',
|
||||
})
|
||||
.reply(200, labels[0]);
|
||||
gmailNock.delete('/v1/users/me/labels/test-label-id').reply(200, labels[0]);
|
||||
gmailNock.get('/v1/users/me/labels/test-label-id').reply(200, labels[0]);
|
||||
gmailNock.get('/v1/users/me/labels').reply(200, {
|
||||
labels,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['labels.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message Labels', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/test/modify', (body) => 'addLabelIds' in body)
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/test/modify', (body) => 'removeLabelIds' in body)
|
||||
.reply(200, messages[0]);
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['message-labels.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drafts', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock
|
||||
.filteringRequestBody((body) => {
|
||||
try {
|
||||
const parsedBody = jsonParse<{ message: { raw: string; threadId: string } }>(body);
|
||||
const mail = Buffer.from(parsedBody.message.raw, 'base64').toString('utf-8');
|
||||
|
||||
const normalizedMail = normalizeDraftMail(mail);
|
||||
parsedBody.message.raw = Buffer.from(normalizedMail, 'utf-8').toString('base64');
|
||||
|
||||
return JSON.stringify(parsedBody);
|
||||
} catch (error) {
|
||||
return body;
|
||||
}
|
||||
})
|
||||
.post('/v1/users/me/drafts', (reqBody) => {
|
||||
try {
|
||||
const b = typeof reqBody === 'string' ? JSON.parse(reqBody) : reqBody;
|
||||
const raw = b?.message?.raw as string;
|
||||
if (typeof raw !== 'string') return false;
|
||||
const mail = Buffer.from(raw, 'base64').toString('utf-8');
|
||||
const normalized = normalizeDraftMail(mail);
|
||||
const expectedNormalized = [
|
||||
'Content-Type: multipart/mixed; boundary="--test-boundary"',
|
||||
'Cc: test_cc@n8n.io',
|
||||
'Bcc: test_bcc@n8n.io',
|
||||
'Subject: Test Subject',
|
||||
'Message-ID: test-message-id',
|
||||
'Date: Mon, 16 Dec 2024 12:34:56 +0000',
|
||||
'MIME-Version: 1.0',
|
||||
'',
|
||||
'----test-boundary',
|
||||
'Content-Type: text/plain; charset=utf-8',
|
||||
'Content-Transfer-Encoding: 7bit',
|
||||
'',
|
||||
'Test Message',
|
||||
'----test-boundary',
|
||||
'Content-Type: application/json; name=file.json',
|
||||
'Content-Transfer-Encoding: base64',
|
||||
'Content-Disposition: attachment; filename=file.json',
|
||||
'',
|
||||
'W3siYXR0YWNobWVudCI6dHJ1ZX1d',
|
||||
'----test-boundary',
|
||||
].join('\n');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Normalized (v1) actual:', normalized);
|
||||
return normalized.trimEnd() === expectedNormalized.trimEnd();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.query({ userId: 'me', uploadType: 'media' })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.delete('/v1/users/me/drafts/test').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts/test')
|
||||
.query({ format: 'raw' })
|
||||
.reply(200, {
|
||||
message: {
|
||||
...messages[0],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
},
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts')
|
||||
.query({
|
||||
maxResults: 2,
|
||||
})
|
||||
.reply(200, { drafts: messages });
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts/a1b2c3d4e5f6g7h8')
|
||||
.query({
|
||||
maxResults: 2,
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
message: {
|
||||
...messages[0],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
},
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts/z9y8x7w6v5u4t3s2')
|
||||
.query({
|
||||
maxResults: 2,
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
message: {
|
||||
...messages[1],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['drafts.workflow.json'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,446 @@
|
||||
{
|
||||
"name": "Gmail v1 test - drafts",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [980, 560],
|
||||
"id": "388c50bd-918f-48d5-8fa2-eba4d3e36ab6",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toJson",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.convertToFile",
|
||||
"typeVersion": 1.1,
|
||||
"position": [1420, 860],
|
||||
"id": "1c1112be-de7c-441e-865c-48044601cb40",
|
||||
"name": "Attachment"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "0a4e38fe-ed38-400e-afb1-9d430f167d54",
|
||||
"name": "attachment",
|
||||
"value": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [1200, 860],
|
||||
"id": "381ca2b8-026a-487a-9f5f-5ef2edb9aa1d",
|
||||
"name": "Edit Fields"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"subject": "Test Subject",
|
||||
"message": "Test Message",
|
||||
"additionalFields": {
|
||||
"ccList": ["test_cc@n8n.io"],
|
||||
"bccList": ["test_bcc@n8n.io"],
|
||||
"attachmentsUi": {
|
||||
"attachmentsBinary": [
|
||||
{
|
||||
"property": "data"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [1640, 860],
|
||||
"id": "0119d458-2782-4fea-abe9-8ac8cabe77aa",
|
||||
"name": "Gmail - Drafts - Create",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAll",
|
||||
"limit": 2,
|
||||
"additionalFields": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 260],
|
||||
"id": "94698cf7-7ce2-4d89-8b04-51c59acf3373",
|
||||
"name": "Gmail - Drafts - All",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "delete",
|
||||
"messageId": "test"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 460],
|
||||
"id": "2b518238-3c18-4527-8315-5d2f5d664af7",
|
||||
"name": "Gmail - Drafts - Delete",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "get",
|
||||
"messageId": "test",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 660],
|
||||
"id": "9d33ea25-6866-4e74-9a53-042fbe70655a",
|
||||
"name": "Gmail - Drafts - Get",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Gmail - Drafts - All": [
|
||||
{
|
||||
"json": {
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"from": {
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>",
|
||||
"value": [
|
||||
{
|
||||
"address": "node8qa@gmail.com",
|
||||
"name": "node qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"subject": "Subject: Test draft",
|
||||
"to": "To: test@gmail.com"
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"messageId": "a1b2c3d4e5f6g7h8",
|
||||
"sizeEstimate": 67890,
|
||||
"subject": "Test draft",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"to": {
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com",
|
||||
"value": [
|
||||
{
|
||||
"address": "test@gmail.com",
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"from": {
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>",
|
||||
"value": [
|
||||
{
|
||||
"address": "node8qa@gmail.com",
|
||||
"name": "node qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"subject": "Subject: Test draft",
|
||||
"to": "To: test@gmail.com"
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"],
|
||||
"messageId": "z9y8x7w6v5u4t3s2",
|
||||
"sizeEstimate": 54321,
|
||||
"subject": "Test draft",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"threadId": "z9y8x7w6v5u4t3s2",
|
||||
"to": {
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com",
|
||||
"value": [
|
||||
{
|
||||
"address": "test@gmail.com",
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Drafts - Create": [
|
||||
{
|
||||
"json": {
|
||||
"historyId": "54321",
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"internalDate": "1733405400000",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"payload": {
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "ABC12345+EXAMPLE123456789"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Thu, 5 Dec 2024 08:30:00 -0800"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "Holiday Deals <promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<12345abc67890@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Exclusive Holiday Discounts!"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\""
|
||||
}
|
||||
],
|
||||
"mimeType": "multipart/alternative",
|
||||
"partId": "",
|
||||
"parts": [
|
||||
{
|
||||
"body": {
|
||||
"data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=",
|
||||
"size": 1234
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/plain",
|
||||
"partId": "0"
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=",
|
||||
"size": 5678
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"utf-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/html",
|
||||
"partId": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 67890,
|
||||
"snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
"threadId": "a1b2c3d4e5f6g7h8"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Drafts - Delete": [{ "json": { "success": true } }],
|
||||
"Gmail - Drafts - Get": [
|
||||
{
|
||||
"json": {
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"from": {
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>",
|
||||
"value": [
|
||||
{
|
||||
"address": "node8qa@gmail.com",
|
||||
"name": "node qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"subject": "Subject: Test draft",
|
||||
"to": "To: test@gmail.com"
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"messageId": "a1b2c3d4e5f6g7h8",
|
||||
"sizeEstimate": 67890,
|
||||
"subject": "Test draft",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"to": {
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com",
|
||||
"value": [
|
||||
{
|
||||
"address": "test@gmail.com",
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Drafts - All",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Drafts - Delete",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Drafts - Get",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Attachment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Drafts - Create",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Attachment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "59109e41-be89-484a-b8d0-8f5c8f0407f9",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "cb484ba7b742928a2048bf8829668bed5b5ad9787579adea888f05980292a4a7"
|
||||
},
|
||||
"id": "dbQv4DRzYXIcTNNs",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"name": "Gmail v1 test - labels",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [520, 200],
|
||||
"id": "d52d5cac-d1df-4581-bb97-c14d559a93ad",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "getAll",
|
||||
"limit": 2
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [740, -100],
|
||||
"id": "48634d96-92f3-430f-9063-2e3784e5ba98",
|
||||
"name": "Gmail - Labels - All",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "delete",
|
||||
"labelId": "test-label-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 100],
|
||||
"id": "3ddc7641-ae81-41f1-a987-d7b517f348ea",
|
||||
"name": "Gmail - Labels - Delete",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "get",
|
||||
"labelId": "test-label-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 300],
|
||||
"id": "80443d2e-f1f5-4f30-aeb2-6eea23a7d888",
|
||||
"name": "Gmail - Labels - Get",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"name": "Test Label Name"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 500],
|
||||
"id": "41efa86b-19f7-4acc-b118-e2f605b81c24",
|
||||
"name": "Gmail - Labels - Create",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Gmail - Labels - All": [
|
||||
{
|
||||
"json": {
|
||||
"id": "CHAT",
|
||||
"labelListVisibility": "labelHide",
|
||||
"messageListVisibility": "hide",
|
||||
"name": "CHAT",
|
||||
"type": "system"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "SENT",
|
||||
"name": "SENT",
|
||||
"type": "system"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Labels - Get": [
|
||||
{
|
||||
"json": {
|
||||
"id": "CHAT",
|
||||
"labelListVisibility": "labelHide",
|
||||
"messageListVisibility": "hide",
|
||||
"name": "CHAT",
|
||||
"type": "system"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Labels - Delete": [{ "json": { "success": true } }],
|
||||
"Gmail - Labels - Create": [
|
||||
{
|
||||
"json": {
|
||||
"id": "CHAT",
|
||||
"labelListVisibility": "labelHide",
|
||||
"messageListVisibility": "hide",
|
||||
"name": "CHAT",
|
||||
"type": "system"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Labels - All",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Labels - Delete",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Labels - Get",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Labels - Create",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "59109e41-be89-484a-b8d0-8f5c8f0407f9",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "cb484ba7b742928a2048bf8829668bed5b5ad9787579adea888f05980292a4a7"
|
||||
},
|
||||
"id": "dbQv4DRzYXIcTNNs",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"name": "Gmail v1 test - message labels",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [1220, 600],
|
||||
"id": "84b20fb7-d041-4a49-b7dc-8a09c1ac0071",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "messageLabel",
|
||||
"messageId": "test",
|
||||
"labelIds": "={{ ['label1', 'label2'] }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [1440, 500],
|
||||
"id": "71a7716d-344b-4050-b2b8-f9f049e46642",
|
||||
"name": "Gmail - Message Labels - Add",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "messageLabel",
|
||||
"operation": "remove",
|
||||
"messageId": "test",
|
||||
"labelIds": "={{ ['label1', 'label2'] }}"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [1440, 700],
|
||||
"id": "c92f2d14-f4a3-48d4-b59b-917d4670163d",
|
||||
"name": "Gmail - Message Labels - Delete",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Gmail - Message Labels - Add": [
|
||||
{
|
||||
"json": {
|
||||
"historyId": "54321",
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"internalDate": "1733405400000",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"payload": {
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "ABC12345+EXAMPLE123456789"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Thu, 5 Dec 2024 08:30:00 -0800"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "Holiday Deals <promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<12345abc67890@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Exclusive Holiday Discounts!"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\""
|
||||
}
|
||||
],
|
||||
"mimeType": "multipart/alternative",
|
||||
"partId": "",
|
||||
"parts": [
|
||||
{
|
||||
"body": {
|
||||
"data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=",
|
||||
"size": 1234
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/plain",
|
||||
"partId": "0"
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=",
|
||||
"size": 5678
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"utf-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/html",
|
||||
"partId": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 67890,
|
||||
"snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
"threadId": "a1b2c3d4e5f6g7h8"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Message Labels - Delete": [
|
||||
{
|
||||
"json": {
|
||||
"historyId": "54321",
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"internalDate": "1733405400000",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"payload": {
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "ABC12345+EXAMPLE123456789"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Thu, 5 Dec 2024 08:30:00 -0800"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "Holiday Deals <promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<12345abc67890@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Exclusive Holiday Discounts!"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\""
|
||||
}
|
||||
],
|
||||
"mimeType": "multipart/alternative",
|
||||
"partId": "",
|
||||
"parts": [
|
||||
{
|
||||
"body": {
|
||||
"data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=",
|
||||
"size": 1234
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/plain",
|
||||
"partId": "0"
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=",
|
||||
"size": 5678
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"utf-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/html",
|
||||
"partId": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 67890,
|
||||
"snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
"threadId": "a1b2c3d4e5f6g7h8"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Message Labels - Add",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Message Labels - Delete",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "59109e41-be89-484a-b8d0-8f5c8f0407f9",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "cb484ba7b742928a2048bf8829668bed5b5ad9787579adea888f05980292a4a7"
|
||||
},
|
||||
"id": "dbQv4DRzYXIcTNNs",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
{
|
||||
"name": "Gmail v1 test - messages",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-600, 340],
|
||||
"id": "c72f4b22-a803-4b57-9edb-bde633a39f8e",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "getAll",
|
||||
"limit": 2,
|
||||
"additionalFields": {
|
||||
"dataPropertyAttachmentsPrefixName": "custom_attachment_",
|
||||
"includeSpamTrash": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [-380, 40],
|
||||
"id": "2b668b97-4b65-4484-9f35-9f147e3db2d1",
|
||||
"name": "Gmail - Messages - All",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"subject": "Test Subject",
|
||||
"message": "Test Message",
|
||||
"toList": ["test_to@n8n.io"],
|
||||
"additionalFields": {
|
||||
"attachmentsUi": {
|
||||
"attachmentsBinary": [
|
||||
{
|
||||
"property": "data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bccList": ["test_bcc@n8n.io"],
|
||||
"ccList": ["test_cc@n8n.io"]
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [60, 640],
|
||||
"id": "45dbeae7-d365-4f50-987f-e9b43aaa84aa",
|
||||
"name": "Gmail - Messages - Send",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "delete",
|
||||
"messageId": "test"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [-380, 240],
|
||||
"id": "0bad13bc-b34d-4f40-a709-854bf4a264a3",
|
||||
"name": "Gmail - Messages - Delete",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "get",
|
||||
"messageId": "test",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 1,
|
||||
"position": [-380, 440],
|
||||
"id": "14123413-ba9c-4226-9013-2166e42a1adc",
|
||||
"name": "Gmail - Messages - Get",
|
||||
"webhookId": "f3cbddc1-3cfa-4217-aa73-f0e5a9309661",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toJson",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.convertToFile",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-160, 640],
|
||||
"id": "a8ff6f8b-b23c-4d5a-b926-f0d8f8bdd34d",
|
||||
"name": "Attachment"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "0a4e38fe-ed38-400e-afb1-9d430f167d54",
|
||||
"name": "attachment",
|
||||
"value": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [-380, 640],
|
||||
"id": "03736be9-7d2e-4a06-9911-b9cde3862a83",
|
||||
"name": "Edit Fields"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Gmail - Messages - All": [
|
||||
{
|
||||
"json": {
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"from": {
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>",
|
||||
"value": [
|
||||
{
|
||||
"address": "node8qa@gmail.com",
|
||||
"name": "node qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"subject": "Subject: Test draft",
|
||||
"to": "To: test@gmail.com"
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"messageId": "<CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"sizeEstimate": 67890,
|
||||
"subject": "Test draft",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"to": {
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com",
|
||||
"value": [
|
||||
{
|
||||
"address": "test@gmail.com",
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"from": {
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>",
|
||||
"value": [
|
||||
{
|
||||
"address": "node8qa@gmail.com",
|
||||
"name": "node qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"subject": "Subject: Test draft",
|
||||
"to": "To: test@gmail.com"
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"id": "z9y8x7w6v5u4t3s2",
|
||||
"labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"],
|
||||
"messageId": "<CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"sizeEstimate": 54321,
|
||||
"subject": "Test draft",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"threadId": "z9y8x7w6v5u4t3s2",
|
||||
"to": {
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com",
|
||||
"value": [
|
||||
{
|
||||
"address": "test@gmail.com",
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Messages - Send": [
|
||||
{
|
||||
"json": {
|
||||
"historyId": "54321",
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"internalDate": "1733405400000",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"payload": {
|
||||
"body": {
|
||||
"size": 0
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Delivered-To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "X-Google-Smtp-Source",
|
||||
"value": "ABC12345+EXAMPLE123456789"
|
||||
},
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com"
|
||||
},
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": "<promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Thu, 5 Dec 2024 08:30:00 -0800"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": "Holiday Deals <promo@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": "exampleuser@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": "<12345abc67890@promotion.example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "Exclusive Holiday Discounts!"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": "1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\""
|
||||
}
|
||||
],
|
||||
"mimeType": "multipart/alternative",
|
||||
"partId": "",
|
||||
"parts": [
|
||||
{
|
||||
"body": {
|
||||
"data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4=",
|
||||
"size": 1234
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/plain; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/plain",
|
||||
"partId": "0"
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4=",
|
||||
"size": 5678
|
||||
},
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "text/html; charset=\"utf-8\""
|
||||
},
|
||||
{
|
||||
"name": "Content-Transfer-Encoding",
|
||||
"value": "quoted-printable"
|
||||
}
|
||||
],
|
||||
"mimeType": "text/html",
|
||||
"partId": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 67890,
|
||||
"snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
"threadId": "a1b2c3d4e5f6g7h8"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Messages - Delete": [{ "json": { "success": true } }],
|
||||
"Gmail - Messages - Get": [
|
||||
{
|
||||
"json": {
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"from": {
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>",
|
||||
"value": [
|
||||
{
|
||||
"address": "node8qa@gmail.com",
|
||||
"name": "node qa"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\"",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"subject": "Subject: Test draft",
|
||||
"to": "To: test@gmail.com"
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"id": "z9y8x7w6v5u4t3s2",
|
||||
"labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"],
|
||||
"messageId": "<CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"sizeEstimate": 54321,
|
||||
"subject": "Test draft",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"threadId": "z9y8x7w6v5u4t3s2",
|
||||
"to": {
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com",
|
||||
"value": [
|
||||
{
|
||||
"address": "test@gmail.com",
|
||||
"name": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Messages - All",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Messages - Delete",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Messages - Get",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Attachment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Messages - Send",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Attachment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "790b1b2e-c8aa-4d10-b758-017944e6cf51",
|
||||
"meta": {
|
||||
"instanceId": "cb484ba7b742928a2048bf8829668bed5b5ad9787579adea888f05980292a4a7"
|
||||
},
|
||||
"id": "dbQv4DRzYXIcTNNs",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import { jsonParse, type ILoadOptionsFunctions, type INode } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
import { getGmailAliases, getLabels, getThreadMessages } from '../../v2/loadOptions';
|
||||
import labels from '../fixtures/labels.json';
|
||||
import messages from '../fixtures/messages.json';
|
||||
|
||||
describe('Test Gmail Node v2', () => {
|
||||
beforeAll(() => {
|
||||
jest
|
||||
.useFakeTimers({ doNotFake: ['setImmediate', 'nextTick'] })
|
||||
.setSystemTime(new Date('2024-12-16 12:34:56.789Z'));
|
||||
});
|
||||
|
||||
describe('Messages', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock.get('/v1/users/me/messages').query({ maxResults: 2 }).reply(200, {
|
||||
messages,
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages')
|
||||
.query({
|
||||
includeSpamTrash: 'true',
|
||||
labelIds: 'CHAT',
|
||||
q: 'test from:Test Sender after:1734393600 before:1735171200',
|
||||
maxResults: '2',
|
||||
})
|
||||
.reply(200, { messages });
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/a1b2c3d4e5f6g7h8')
|
||||
.query({
|
||||
maxResults: '2',
|
||||
format: 'metadata',
|
||||
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
|
||||
})
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/a1b2c3d4e5f6g7h8')
|
||||
.query({
|
||||
includeSpamTrash: 'true',
|
||||
labelIds: 'CHAT',
|
||||
q: 'test from:Test Sender after:1734393600 before:1735171200',
|
||||
maxResults: '2',
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
...messages[0],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/z9y8x7w6v5u4t3s2')
|
||||
.query({
|
||||
includeSpamTrash: 'true',
|
||||
labelIds: 'CHAT',
|
||||
q: 'test from:Test Sender after:1734393600 before:1735171200',
|
||||
maxResults: '2',
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
...messages[1],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/z9y8x7w6v5u4t3s2')
|
||||
.query({
|
||||
maxResults: '2',
|
||||
format: 'metadata',
|
||||
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
|
||||
})
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.get('/v1/users/me/labels').reply(200, {
|
||||
labels,
|
||||
});
|
||||
gmailNock.get('/v1/users/me/profile').times(2).reply(200, { emailAddress: 'test@n8n.io' });
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/send')
|
||||
.query({ format: 'metadata' })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.post('/v1/users/me/messages/send').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/send')
|
||||
.query({ userId: 'me', uploadType: 'media' })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/test/modify', (body) => 'addLabelIds' in body)
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/test/modify', (body) => 'removeLabelIds' in body)
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.delete('/v1/users/me/messages/test').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/test')
|
||||
.query({
|
||||
format: 'metadata',
|
||||
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
|
||||
})
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.get('/v1/users/me/labels').reply(200, { labels });
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/test')
|
||||
.query({ format: 'raw' })
|
||||
.reply(200, { raw: 'test email content' });
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/test/modify', { removeLabelIds: ['UNREAD'] })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/test/modify', { addLabelIds: ['UNREAD'] })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/test')
|
||||
.query({
|
||||
format: 'metadata',
|
||||
})
|
||||
.reply(200, messages[0]);
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['messages.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Labels', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock
|
||||
.post('/v1/users/me/labels', {
|
||||
labelListVisibility: 'labelShow',
|
||||
messageListVisibility: 'show',
|
||||
name: 'Test Label Name',
|
||||
})
|
||||
.reply(200, labels[0]);
|
||||
gmailNock.delete('/v1/users/me/labels/test-label-id').reply(200, labels[0]);
|
||||
gmailNock.get('/v1/users/me/labels/test-label-id').reply(200, labels[0]);
|
||||
gmailNock.get('/v1/users/me/labels').reply(200, {
|
||||
labels,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['labels.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drafts', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock
|
||||
.filteringRequestBody((body) => {
|
||||
try {
|
||||
const parsedBody = jsonParse<{ message: { raw: string; threadId: string } }>(body);
|
||||
const mail = Buffer.from(parsedBody.message.raw, 'base64').toString('utf-8');
|
||||
|
||||
// Remove dynamic fields from mail
|
||||
parsedBody.message.raw = Buffer.from(
|
||||
mail
|
||||
.replace(/boundary=".*"/g, 'boundary="--test-boundary"')
|
||||
.replace(/----.*/g, '----test-boundary')
|
||||
.replace(/Message-ID:.*/g, 'Message-ID: <test-message-id@mail.com>'),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
|
||||
return JSON.stringify(parsedBody);
|
||||
} catch (error) {
|
||||
return body;
|
||||
}
|
||||
})
|
||||
.post('/v1/users/me/drafts', (body) => {
|
||||
return (
|
||||
typeof body.message?.raw === 'string' && body.message.threadId === 'test-thread-id'
|
||||
);
|
||||
})
|
||||
.query({ userId: 'me', uploadType: 'media' })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.delete('/v1/users/me/drafts/test-draft-id').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/threads/test-thread-id')
|
||||
.query({
|
||||
format: 'metadata',
|
||||
metadataHeaders: 'Message-ID',
|
||||
})
|
||||
.reply(200, {
|
||||
messages: [
|
||||
{ payload: { headers: [{ name: 'Message-ID', value: '<test-message-id@mail.com>' }] } },
|
||||
],
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts/test-draft-id')
|
||||
.query({ format: 'raw' })
|
||||
.reply(200, {
|
||||
message: {
|
||||
...messages[0],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
},
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts')
|
||||
.query({
|
||||
dataPropertyAttachmentsPrefixName: 'attachment_',
|
||||
downloadAttachments: true,
|
||||
includeSpamTrash: true,
|
||||
maxResults: 100,
|
||||
})
|
||||
.reply(200, { drafts: messages });
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts/a1b2c3d4e5f6g7h8')
|
||||
.query({
|
||||
dataPropertyAttachmentsPrefixName: 'attachment_',
|
||||
downloadAttachments: true,
|
||||
includeSpamTrash: true,
|
||||
maxResults: 100,
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
message: {
|
||||
...messages[0],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
},
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/drafts/z9y8x7w6v5u4t3s2')
|
||||
.query({
|
||||
dataPropertyAttachmentsPrefixName: 'attachment_',
|
||||
downloadAttachments: true,
|
||||
includeSpamTrash: true,
|
||||
maxResults: 100,
|
||||
format: 'raw',
|
||||
})
|
||||
.reply(200, {
|
||||
message: {
|
||||
...messages[1],
|
||||
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['drafts.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Threads', () => {
|
||||
const gmailNock = nock('https://www.googleapis.com/gmail');
|
||||
|
||||
beforeAll(() => {
|
||||
gmailNock.get('/v1/users/me/threads').query({ maxResults: 2 }).reply(200, {
|
||||
threads: messages,
|
||||
});
|
||||
gmailNock
|
||||
.get('/v1/users/me/threads')
|
||||
.query((query) => {
|
||||
return (
|
||||
query.includeSpamTrash === 'true' &&
|
||||
query.labelIds === 'CHAT' &&
|
||||
!!query.q &&
|
||||
query.q.includes('has:attachment') &&
|
||||
query.q.includes('before:') &&
|
||||
query.q.includes('after:')
|
||||
);
|
||||
})
|
||||
.reply(200, { threads: messages });
|
||||
gmailNock
|
||||
.post('/v1/users/me/threads/test-thread-id/modify', { addLabelIds: ['CHAT'] })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock
|
||||
.post('/v1/users/me/threads/test-thread-id/modify', { removeLabelIds: ['CHAT'] })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.delete('/v1/users/me/threads/test-thread-id').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/threads/test-thread-id')
|
||||
.query({
|
||||
format: 'metadata',
|
||||
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
|
||||
})
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.get('/v1/users/me/labels').times(2).reply(200, { labels });
|
||||
gmailNock
|
||||
.get('/v1/users/me/threads/test-thread-id')
|
||||
.query({ format: 'full' })
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.post('/v1/users/me/threads/test-thread-id/trash').reply(200, messages[0]);
|
||||
gmailNock.post('/v1/users/me/threads/test-thread-id/untrash').reply(200, messages[0]);
|
||||
gmailNock
|
||||
.get('/v1/users/me/messages/test%20snippet')
|
||||
.query({
|
||||
userId: 'me',
|
||||
uploadType: 'media',
|
||||
format: 'metadata',
|
||||
})
|
||||
.reply(200, messages[0]);
|
||||
gmailNock.get('/v1/users/me/profile').reply(200, { emailAddress: 'test@n8n.io' });
|
||||
gmailNock
|
||||
.post('/v1/users/me/messages/send')
|
||||
.query({ userId: 'me', uploadType: 'media', format: 'metadata' })
|
||||
.reply(200, messages[0]);
|
||||
});
|
||||
|
||||
afterAll(() => gmailNock.done());
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
workflowFiles: ['threads.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadOptions', () => {
|
||||
describe('getLabels', () => {
|
||||
it('should return a list of Gmail labels', async () => {
|
||||
const loadOptionsFunctions = mockDeep<ILoadOptionsFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>()),
|
||||
helpers: mock<ILoadOptionsFunctions['helpers']>({
|
||||
requestWithAuthentication: jest
|
||||
.fn()
|
||||
// 2 pages of labels
|
||||
.mockImplementationOnce(async () => ({ labels, nextPageToken: 'nextPageToken' }))
|
||||
.mockImplementationOnce(async () => ({ labels })),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(await getLabels.call(loadOptionsFunctions)).toEqual([
|
||||
{ name: 'CHAT', value: 'CHAT' },
|
||||
{ name: 'CHAT', value: 'CHAT' },
|
||||
{ name: 'SENT', value: 'SENT' },
|
||||
{ name: 'SENT', value: 'SENT' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThreadMessages', () => {
|
||||
it('should return a list of Gmail thread messages', async () => {
|
||||
const loadOptionsFunctions = mockDeep<ILoadOptionsFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>()),
|
||||
helpers: mock<ILoadOptionsFunctions['helpers']>({
|
||||
requestWithAuthentication: jest.fn(async () => ({ messages })),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(await getThreadMessages.call(loadOptionsFunctions)).toEqual([
|
||||
{
|
||||
name: "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
value: 'a1b2c3d4e5f6g7h8',
|
||||
},
|
||||
{
|
||||
name: 'Your friend John just shared a new photo with you! Check it out now.',
|
||||
value: 'z9y8x7w6v5u4t3s2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGmailAliases', () => {
|
||||
it('should return a list of Gmail aliases', async () => {
|
||||
const loadOptionsFunctions = mockDeep<ILoadOptionsFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>()),
|
||||
helpers: mock<ILoadOptionsFunctions['helpers']>({
|
||||
requestWithAuthentication: jest.fn(async () => ({
|
||||
sendAs: [
|
||||
{ isDefault: false, sendAsEmail: 'alias1@n8n.io' },
|
||||
{ isDefault: true, sendAsEmail: 'alias2@n8n.io' },
|
||||
],
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(await getGmailAliases.call(loadOptionsFunctions)).toEqual([
|
||||
{
|
||||
name: 'alias1@n8n.io',
|
||||
value: 'alias1@n8n.io',
|
||||
},
|
||||
{
|
||||
name: 'alias2@n8n.io (Default)',
|
||||
value: 'alias2@n8n.io',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"name": "My workflow 130",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-60, 100],
|
||||
"id": "636b40bc-2c98-4b9a-8ce2-9d1322294518",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "draft",
|
||||
"operation": "get",
|
||||
"messageId": "test-draft-id",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [440, 200],
|
||||
"id": "8802fdb5-2741-407b-82a4-ccedc4055076",
|
||||
"name": "Gmail - Drafts - Get",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "draft",
|
||||
"operation": "delete",
|
||||
"messageId": "test-draft-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [440, 20],
|
||||
"id": "ed979c3a-b2ea-413e-be63-0392cc1714a5",
|
||||
"name": "Gmail - Drafts - Delete",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "draft",
|
||||
"subject": "Test Draft Subject",
|
||||
"message": "Test Draft Message",
|
||||
"options": {
|
||||
"attachmentsUi": {
|
||||
"attachmentsBinary": [
|
||||
{
|
||||
"property": "data"
|
||||
}
|
||||
]
|
||||
},
|
||||
"bccList": "test-bcc@n8n.io",
|
||||
"ccList": "test-cc@n8n.io",
|
||||
"fromAlias": "=test-alias@n8n.io",
|
||||
"replyTo": "test-reply@n8n.io",
|
||||
"threadId": "test-thread-id",
|
||||
"sendTo": "test-to@n8n.io"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [840, -180],
|
||||
"id": "45758452-3b5b-478d-aece-001e117ce69d",
|
||||
"name": "Gmail - Drafts - Create",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "draft",
|
||||
"operation": "getAll",
|
||||
"returnAll": true,
|
||||
"options": {
|
||||
"dataPropertyAttachmentsPrefixName": "attachment_",
|
||||
"downloadAttachments": true,
|
||||
"includeSpamTrash": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [440, 400],
|
||||
"id": "bae81586-7641-4fdc-81a4-0006b289bf9d",
|
||||
"name": "Gmail - Drafts - Get Many",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "491590a8-27a6-4d14-b342-493947775d16",
|
||||
"name": "binary",
|
||||
"value": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [420, -180],
|
||||
"id": "d630c018-1d7b-4779-a280-9f4a21c6a764",
|
||||
"name": "Edit Fields"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toJson",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.convertToFile",
|
||||
"typeVersion": 1.1,
|
||||
"position": [640, -180],
|
||||
"id": "fc3bdb76-c278-44f6-9aac-153b79c8177b",
|
||||
"name": "Convert to File"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Gmail - Drafts - Create": [
|
||||
{
|
||||
"json": {
|
||||
"id": "a1b2c3d4e5f6g7h8",
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"snippet": "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "multipart/alternative",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{ "name": "Delivered-To", "value": "exampleuser@gmail.com" },
|
||||
{
|
||||
"name": "Received",
|
||||
"value": "by 2001:db8::abcd with SMTP id xyz123abc456; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{ "name": "X-Google-Smtp-Source", "value": "ABC12345+EXAMPLE123456789" },
|
||||
{
|
||||
"name": "X-Received",
|
||||
"value": "by 192.0.2.1 with SMTP id 12345abc67890; Thu, 5 Dec 2024 08:30:00 -0800 (PST)"
|
||||
},
|
||||
{
|
||||
"name": "ARC-Seal",
|
||||
"value": "i=1; a=rsa-sha256; t=1733405400; cv=none; d=example.com; s=arc-20241205; b=ABCDEFG123456="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Message-Signature",
|
||||
"value": "i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc-20241205; bh=EXAMPLEHASH12345="
|
||||
},
|
||||
{
|
||||
"name": "ARC-Authentication-Results",
|
||||
"value": "i=1; mx.example.com; dkim=pass header.i=@promotion.example.com; spf=pass smtp.mailfrom=promo@promotion.example.com; dmarc=pass header.from=example.com"
|
||||
},
|
||||
{ "name": "Return-Path", "value": "<promo@promotion.example.com>" },
|
||||
{ "name": "Date", "value": "Thu, 5 Dec 2024 08:30:00 -0800" },
|
||||
{ "name": "From", "value": "Holiday Deals <promo@promotion.example.com>" },
|
||||
{ "name": "To", "value": "exampleuser@gmail.com" },
|
||||
{ "name": "Message-ID", "value": "<12345abc67890@promotion.example.com>" },
|
||||
{ "name": "Subject", "value": "Exclusive Holiday Discounts!" },
|
||||
{ "name": "MIME-Version", "value": "1.0" },
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "multipart/alternative; boundary=\"----=_Part_12345_67890.1733405400000\""
|
||||
}
|
||||
],
|
||||
"body": { "size": 0 },
|
||||
"parts": [
|
||||
{
|
||||
"partId": "0",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{ "name": "Content-Type", "value": "text/plain; charset=utf-8" },
|
||||
{ "name": "Content-Transfer-Encoding", "value": "quoted-printable" }
|
||||
],
|
||||
"body": {
|
||||
"size": 1234,
|
||||
"data": "VGhpcyBpcyBhbiBleGFtcGxlIG1lc3NhZ2UuIFRoYW5rIHlvdSBmb3Igc2hvcHBpbmcgd2l0aCB1cy4="
|
||||
}
|
||||
},
|
||||
{
|
||||
"partId": "1",
|
||||
"mimeType": "text/html",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{ "name": "Content-Type", "value": "text/html; charset=\"utf-8\"" },
|
||||
{ "name": "Content-Transfer-Encoding", "value": "quoted-printable" }
|
||||
],
|
||||
"body": {
|
||||
"size": 5678,
|
||||
"data": "PGRpdiBzdHlsZT0nZm9udC1mYW1pbHk6IEFyaWFsLCBzYW5zLXNlcmlmOyc+VGhpcyBpcyBhbiBleGFtcGxlIGh0bWwgbWVzc2FnZS4gPGI+VGhhbmsgeW91IGZvciBzaG9wcGluZyB3aXRoIHVzLjwvYj48L2Rpdj4="
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sizeEstimate": 67890,
|
||||
"historyId": "54321",
|
||||
"internalDate": "1733405400000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Drafts - Delete": [{ "json": { "success": true } }],
|
||||
"Gmail - Drafts - Get": [
|
||||
{
|
||||
"json": {
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"sizeEstimate": 67890,
|
||||
"headers": {
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"subject": "Subject: Test draft",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"to": "To: test@gmail.com",
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\""
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"subject": "Test draft",
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"to": {
|
||||
"value": [{ "address": "test@gmail.com", "name": "" }],
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com"
|
||||
},
|
||||
"from": {
|
||||
"value": [{ "address": "node8qa@gmail.com", "name": "node qa" }],
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>"
|
||||
},
|
||||
"messageId": "a1b2c3d4e5f6g7h8"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Drafts - Get Many": [
|
||||
{
|
||||
"json": {
|
||||
"threadId": "a1b2c3d4e5f6g7h8",
|
||||
"labelIds": ["UNREAD", "CATEGORY_PROMOTIONS", "INBOX"],
|
||||
"sizeEstimate": 67890,
|
||||
"headers": {
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"subject": "Subject: Test draft",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"to": "To: test@gmail.com",
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\""
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"subject": "Test draft",
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"to": {
|
||||
"value": [{ "address": "test@gmail.com", "name": "" }],
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com"
|
||||
},
|
||||
"from": {
|
||||
"value": [{ "address": "node8qa@gmail.com", "name": "node qa" }],
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>"
|
||||
},
|
||||
"messageId": "a1b2c3d4e5f6g7h8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"threadId": "z9y8x7w6v5u4t3s2",
|
||||
"labelIds": ["UNREAD", "CATEGORY_SOCIAL", "INBOX"],
|
||||
"sizeEstimate": 54321,
|
||||
"headers": {
|
||||
"mime-version": "MIME-Version: 1.0",
|
||||
"date": "Date: Fri, 13 Dec 2024 11:15:01 +0100",
|
||||
"message-id": "Message-ID: <CAEGAProwudGnKXxqrhMjOuxamTh7yArjwP7OD9UBq-Jpkb0X9w@mail.gmail.com>",
|
||||
"subject": "Subject: Test draft",
|
||||
"from": "From: node qa <node8qa@gmail.com>",
|
||||
"to": "To: test@gmail.com",
|
||||
"content-type": "Content-Type: multipart/alternative; boundary=\"0000000000009d58b60629241a22\""
|
||||
},
|
||||
"html": "<div dir=\"ltr\">draft body<br></div>\n",
|
||||
"text": "draft body\n",
|
||||
"textAsHtml": "<p>draft body</p>",
|
||||
"subject": "Test draft",
|
||||
"date": "2024-12-13T10:15:01.000Z",
|
||||
"to": {
|
||||
"value": [{ "address": "test@gmail.com", "name": "" }],
|
||||
"html": "<span class=\"mp_address_group\"><a href=\"mailto:test@gmail.com\" class=\"mp_address_email\">test@gmail.com</a></span>",
|
||||
"text": "test@gmail.com"
|
||||
},
|
||||
"from": {
|
||||
"value": [{ "address": "node8qa@gmail.com", "name": "node qa" }],
|
||||
"html": "<span class=\"mp_address_group\"><span class=\"mp_address_name\">node qa</span> <<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>></span>",
|
||||
"text": "\"node qa\" <node8qa@gmail.com>"
|
||||
},
|
||||
"messageId": "z9y8x7w6v5u4t3s2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Drafts - Get Many",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Drafts - Get",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Drafts - Delete",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Convert to File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Convert to File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Drafts - Create",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "84fb11a4-4166-45bd-bd9f-60fa378d9e68",
|
||||
"meta": {
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"id": "09KDcfGmfDrLInDE",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"name": "My workflow 130",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-60, 100],
|
||||
"id": "636b40bc-2c98-4b9a-8ce2-9d1322294518",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "create",
|
||||
"name": "Test Label Name",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [220, -180],
|
||||
"id": "45758452-3b5b-478d-aece-001e117ce69d",
|
||||
"name": "Gmail - Labels - Create",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "delete",
|
||||
"labelId": "test-label-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [220, 20],
|
||||
"id": "ed979c3a-b2ea-413e-be63-0392cc1714a5",
|
||||
"name": "Gmail - Labels - Delete",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"operation": "get",
|
||||
"labelId": "test-label-id"
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [220, 200],
|
||||
"id": "8802fdb5-2741-407b-82a4-ccedc4055076",
|
||||
"name": "Gmail - Labels - Get",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "label",
|
||||
"limit": 2
|
||||
},
|
||||
"type": "n8n-nodes-base.gmail",
|
||||
"typeVersion": 2.1,
|
||||
"position": [220, 400],
|
||||
"id": "bae81586-7641-4fdc-81a4-0006b289bf9d",
|
||||
"name": "Gmail - Labels - Get Many",
|
||||
"webhookId": "3b8b38e0-2f4b-40bc-8b67-37e7ea95cb60",
|
||||
"credentials": {
|
||||
"gmailOAuth2": {
|
||||
"id": "22",
|
||||
"name": "Gmail 0auth"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Gmail - Labels - Create": [
|
||||
{
|
||||
"json": {
|
||||
"id": "CHAT",
|
||||
"name": "CHAT",
|
||||
"messageListVisibility": "hide",
|
||||
"labelListVisibility": "labelHide",
|
||||
"type": "system"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Labels - Delete": [{ "json": { "success": true } }],
|
||||
"Gmail - Labels - Get": [
|
||||
{
|
||||
"json": {
|
||||
"id": "CHAT",
|
||||
"name": "CHAT",
|
||||
"messageListVisibility": "hide",
|
||||
"labelListVisibility": "labelHide",
|
||||
"type": "system"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Gmail - Labels - Get Many": [
|
||||
{
|
||||
"json": {
|
||||
"id": "CHAT",
|
||||
"name": "CHAT",
|
||||
"messageListVisibility": "hide",
|
||||
"labelListVisibility": "labelHide",
|
||||
"type": "system"
|
||||
}
|
||||
},
|
||||
{ "json": { "id": "SENT", "name": "SENT", "type": "system" } }
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Gmail - Labels - Create",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Labels - Delete",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Labels - Get",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Gmail - Labels - Get Many",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "",
|
||||
"meta": {
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import type { IEmail } from '@utils/sendAndWait/interfaces';
|
||||
|
||||
import * as GenericFunctions from '../../GenericFunctions';
|
||||
import { parseRawEmail, prepareQuery, prepareTimestamp } from '../../GenericFunctions';
|
||||
import { addThreadHeadersToEmail } from '../../v2/utils/draft';
|
||||
|
||||
const node: INode = {
|
||||
id: '1',
|
||||
name: 'Gmail node',
|
||||
typeVersion: 2,
|
||||
type: 'n8n-nodes-base.gmail',
|
||||
position: [50, 50],
|
||||
parameters: {
|
||||
operation: 'getAll',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Google Gmail v2, prepareTimestamp', () => {
|
||||
it('should return a valid timestamp from ISO', () => {
|
||||
const dateInput = '2020-01-01T00:00:00.000Z';
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should return a valid timestamp from integer in miliseconds', () => {
|
||||
const dateInput = 1577836800000;
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should return a valid timestamp from integer in seconds', () => {
|
||||
const dateInput = 1577836800;
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should return a valid timestamp from string in miliseconds', () => {
|
||||
const dateInput = '1577836800000';
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should return a valid timestamp from string in seconds', () => {
|
||||
const dateInput = '1577836800';
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should return a valid timestamp from luxon DateTime', () => {
|
||||
const dateInput = DateTime.fromISO('2020-01-01T00:00:00.000Z');
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should return a valid timestamp from luxon DateTime ISO', () => {
|
||||
const dateInput = DateTime.fromISO('2020-01-01T00:00:00.000Z').toISO();
|
||||
const timestampBefore = prepareTimestamp(node, 0, '', dateInput, 'before');
|
||||
const timestampAfter = prepareTimestamp(node, 0, '', dateInput, 'after');
|
||||
|
||||
expect(timestampBefore).toBeDefined();
|
||||
expect(timestampBefore).toBe('before:1577836800');
|
||||
|
||||
expect(timestampAfter).toBeDefined();
|
||||
expect(timestampAfter).toBe('after:1577836800');
|
||||
});
|
||||
|
||||
it('should throw error on invalid data', () => {
|
||||
const dateInput = 'invalid';
|
||||
expect(() => prepareTimestamp(node, 0, '', dateInput, 'before')).toThrow(
|
||||
"Invalid date/time in 'Received Before' field",
|
||||
);
|
||||
expect(() => prepareTimestamp(node, 0, '', dateInput, 'after')).toThrow(
|
||||
"Invalid date/time in 'Received After' field",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRawEmail', () => {
|
||||
it('should return a date string', async () => {
|
||||
// ARRANGE
|
||||
const executionFunctions = mock<IExecuteFunctions>();
|
||||
const rawEmail = 'Date: Wed, 28 Aug 2024 00:36:37 -0700'.replace(/\n/g, '\r\n');
|
||||
|
||||
// ACT
|
||||
const { json } = await parseRawEmail.call(
|
||||
executionFunctions,
|
||||
{ raw: Buffer.from(rawEmail, 'utf8').toString('base64') },
|
||||
'attachment_',
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
expect(typeof json.date).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareQuery', () => {
|
||||
const executionFunctions = mock<IExecuteFunctions>({
|
||||
getNode: jest.fn(() => node),
|
||||
});
|
||||
|
||||
it('should convert sender filter to q parameter', () => {
|
||||
const result = prepareQuery.call(executionFunctions, { sender: 'alice@example.com' }, 0);
|
||||
|
||||
expect(result.q).toBe('from:alice@example.com');
|
||||
expect(result).not.toHaveProperty('sender');
|
||||
});
|
||||
|
||||
it('should append sender to existing q parameter', () => {
|
||||
const result = prepareQuery.call(
|
||||
executionFunctions,
|
||||
{ q: 'subject:hello', sender: 'alice@example.com' },
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result.q).toBe('subject:hello from:alice@example.com');
|
||||
expect(result).not.toHaveProperty('sender');
|
||||
});
|
||||
|
||||
it('should convert readStatus to q parameter when not "both"', () => {
|
||||
const result = prepareQuery.call(executionFunctions, { readStatus: 'unread' }, 0);
|
||||
|
||||
expect(result.q).toBe('is:unread');
|
||||
expect(result).not.toHaveProperty('readStatus');
|
||||
});
|
||||
|
||||
it('should not modify q when readStatus is "both"', () => {
|
||||
const result = prepareQuery.call(executionFunctions, { readStatus: 'both' }, 0);
|
||||
|
||||
expect(result).not.toHaveProperty('q');
|
||||
});
|
||||
|
||||
it('should keep empty labelIds as-is when falsy', () => {
|
||||
const result = prepareQuery.call(executionFunctions, { labelIds: '' }, 0);
|
||||
|
||||
expect(result.labelIds).toBe('');
|
||||
});
|
||||
|
||||
it('should preserve non-empty labelIds', () => {
|
||||
const result = prepareQuery.call(executionFunctions, { labelIds: ['INBOX', 'CHAT'] }, 0);
|
||||
|
||||
expect(result.labelIds).toEqual(['INBOX', 'CHAT']);
|
||||
});
|
||||
|
||||
it('should pass through includeSpamTrash unchanged', () => {
|
||||
const result = prepareQuery.call(executionFunctions, { includeSpamTrash: true }, 0);
|
||||
|
||||
expect(result.includeSpamTrash).toBe(true);
|
||||
});
|
||||
|
||||
it('should combine multiple filters into a single q parameter', () => {
|
||||
const result = prepareQuery.call(
|
||||
executionFunctions,
|
||||
{
|
||||
q: 'test',
|
||||
sender: 'bob@example.com',
|
||||
readStatus: 'read',
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
expect(result.q).toBe('test from:bob@example.com is:read');
|
||||
expect(result).not.toHaveProperty('sender');
|
||||
expect(result).not.toHaveProperty('readStatus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addThreadHeadersToEmail', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set inReplyTo and reference on the email object', async () => {
|
||||
const mockThreadId = 'thread123';
|
||||
const mockMessageId = '<message-id@example.com>';
|
||||
const mockThread = {
|
||||
messages: [
|
||||
{ payload: { headers: [{ name: 'Message-ID', value: '<old-id@example.com>' }] } },
|
||||
{ payload: { headers: [{ name: 'Message-ID', value: mockMessageId }] } },
|
||||
],
|
||||
};
|
||||
|
||||
jest.spyOn(GenericFunctions, 'googleApiRequest').mockImplementation(async function () {
|
||||
return mockThread;
|
||||
});
|
||||
|
||||
const email = mock<IEmail>({});
|
||||
|
||||
const thisArg = mock<IExecuteFunctions>({});
|
||||
|
||||
await addThreadHeadersToEmail.call(thisArg, email, mockThreadId);
|
||||
|
||||
expect(email.inReplyTo).toBe(mockMessageId);
|
||||
expect(email.references).toBe(mockMessageId);
|
||||
});
|
||||
|
||||
it('should set inReplyTo and reference on the email object even if the message has only one item', async () => {
|
||||
const mockThreadId = 'thread123';
|
||||
const mockMessageId = '<message-id@example.com>';
|
||||
const mockThread = {
|
||||
messages: [{ payload: { headers: [{ name: 'Message-ID', value: mockMessageId }] } }],
|
||||
};
|
||||
|
||||
jest.spyOn(GenericFunctions, 'googleApiRequest').mockImplementation(async function () {
|
||||
return mockThread;
|
||||
});
|
||||
|
||||
const email = mock<IEmail>({});
|
||||
|
||||
const thisArg = mock<IExecuteFunctions>({});
|
||||
|
||||
await addThreadHeadersToEmail.call(thisArg, email, mockThreadId);
|
||||
|
||||
expect(email.inReplyTo).toBe(mockMessageId);
|
||||
expect(email.references).toBe(mockMessageId);
|
||||
});
|
||||
|
||||
it('should not do anything if the thread has no messages', async () => {
|
||||
const mockThreadId = 'thread123';
|
||||
const mockThread = {};
|
||||
|
||||
jest.spyOn(GenericFunctions, 'googleApiRequest').mockImplementation(async function () {
|
||||
return mockThread;
|
||||
});
|
||||
|
||||
const email = mock<IEmail>({});
|
||||
|
||||
const thisArg = mock<IExecuteFunctions>({});
|
||||
|
||||
await addThreadHeadersToEmail.call(thisArg, email, mockThreadId);
|
||||
|
||||
// We are using mock<IEmail>({}) which means the value of these will be a mock function
|
||||
expect(typeof email.inReplyTo).toBe('function');
|
||||
expect(typeof email.references).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
export type Message = {
|
||||
id: string;
|
||||
threadId: string;
|
||||
labelIds: string[];
|
||||
snippet: string;
|
||||
historyId: string;
|
||||
date?: string;
|
||||
headers?: Record<string, string>;
|
||||
internalDate?: string;
|
||||
sizeEstimate: number;
|
||||
raw: string;
|
||||
payload: MessagePart;
|
||||
};
|
||||
|
||||
export type ListMessage = Pick<Message, 'id' | 'threadId'>;
|
||||
|
||||
export type MessageListResponse = {
|
||||
messages?: ListMessage[];
|
||||
nextPageToken?: string;
|
||||
resultSizeEstimate: number;
|
||||
};
|
||||
|
||||
type GmailHeader = {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type MessagePart = {
|
||||
partId: string;
|
||||
mimeType: string;
|
||||
filename: string;
|
||||
headers: GmailHeader[];
|
||||
body: MessagePartBody;
|
||||
parts: MessagePart[];
|
||||
};
|
||||
|
||||
type MessagePartBody = {
|
||||
attachmentId: string;
|
||||
size: number;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type Label = {
|
||||
id: string;
|
||||
name: string;
|
||||
messageListVisibility?: 'hide';
|
||||
labelListVisibility?: 'labelHide';
|
||||
type?: 'system';
|
||||
};
|
||||
|
||||
export type GmailWorkflowStaticData = {
|
||||
lastTimeChecked?: number;
|
||||
possibleDuplicates?: string[];
|
||||
};
|
||||
export type GmailWorkflowStaticDataDictionary = Record<string, GmailWorkflowStaticData>;
|
||||
|
||||
export type GmailTriggerOptions = Partial<{
|
||||
dataPropertyAttachmentsPrefixName: string;
|
||||
downloadAttachments: boolean;
|
||||
}>;
|
||||
|
||||
export type GmailTriggerFilters = Partial<{
|
||||
sender: string;
|
||||
q: string;
|
||||
includeSpamTrash: boolean;
|
||||
includeDrafts: boolean;
|
||||
readStatus: 'read' | 'unread' | 'both';
|
||||
labelIds: string[];
|
||||
receivedAfter: number;
|
||||
}>;
|
||||
|
||||
export type GmailMessage = {
|
||||
id: string;
|
||||
threadId: string;
|
||||
labelIds: string[];
|
||||
snippet: string;
|
||||
historyId: string;
|
||||
internalDate?: string;
|
||||
headers?: Record<string, string>;
|
||||
sizeEstimate: number;
|
||||
raw: string;
|
||||
payload: MessagePart;
|
||||
};
|
||||
|
||||
export type GmailMessageMetadata = Pick<GmailMessage, 'id' | 'threadId' | 'labelIds' | 'payload'>;
|
||||
|
||||
export type GmailUserProfile = {
|
||||
emailAddress: string;
|
||||
messagesTotal: number;
|
||||
threadsTotal: number;
|
||||
historyId: string;
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import uniq from 'lodash/uniq';
|
||||
import { NodeOperationError, type IDataObject, type IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import type { IEmail } from '@utils/sendAndWait/interfaces';
|
||||
|
||||
import {
|
||||
encodeEmail,
|
||||
googleApiRequest,
|
||||
prepareEmailAttachments,
|
||||
prepareEmailBody,
|
||||
prepareEmailsInput,
|
||||
} from '../GenericFunctions';
|
||||
import type { GmailMessage, GmailMessageMetadata, GmailUserProfile } from '../types';
|
||||
|
||||
export async function replyToEmail(
|
||||
this: IExecuteFunctions,
|
||||
gmailId: string,
|
||||
options: IDataObject,
|
||||
itemIndex: number,
|
||||
nodeVersion: number,
|
||||
) {
|
||||
if (options.replyToSenderOnly && options.replyToRecipientsOnly) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Both "Reply to Sender Only" and "Reply to Recipient Only" cannot be enabled at the same time. Please select only one option.',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
)) as GmailMessageMetadata;
|
||||
|
||||
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',
|
||||
)) as GmailUserProfile;
|
||||
|
||||
const to: string[] = [];
|
||||
const replyToSenderOnly =
|
||||
options.replyToSenderOnly === undefined ? false : (options.replyToSenderOnly as boolean);
|
||||
const replyToRecipientsOnly =
|
||||
options.replyToRecipientsOnly === undefined
|
||||
? false
|
||||
: (options.replyToRecipientsOnly as boolean);
|
||||
|
||||
const prepareEmailString = (email: string) => {
|
||||
if (email.includes(emailAddress)) return;
|
||||
if (email.includes('<') && email.includes('>')) {
|
||||
to.push(email);
|
||||
} else {
|
||||
to.push(`<${email}>`);
|
||||
}
|
||||
};
|
||||
|
||||
let replyToHeaderName = 'from';
|
||||
if (nodeVersion >= 2.2 && payload.headers.some((h) => h.name.toLowerCase() === 'reply-to')) {
|
||||
replyToHeaderName = 'reply-to';
|
||||
}
|
||||
|
||||
for (const header of payload.headers) {
|
||||
const headerName = (header.name || '').toLowerCase();
|
||||
if (headerName === replyToHeaderName && !replyToRecipientsOnly) {
|
||||
const replyToEmail = header.value;
|
||||
if (replyToEmail.includes('<') && replyToEmail.includes('>')) {
|
||||
to.push(replyToEmail);
|
||||
} else {
|
||||
to.push(`<${replyToEmail}>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (headerName === 'to' && !replyToSenderOnly) {
|
||||
const toEmails = header.value;
|
||||
toEmails.split(',').forEach(prepareEmailString);
|
||||
}
|
||||
}
|
||||
|
||||
let from = '';
|
||||
if (options.senderName) {
|
||||
from = `${options.senderName as string} <${emailAddress}>`;
|
||||
}
|
||||
|
||||
const toString = uniq(to).join(', ');
|
||||
|
||||
const email: IEmail = {
|
||||
from,
|
||||
to: toString,
|
||||
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,
|
||||
)) as GmailMessage;
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
import type { 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 many 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: 'HTML',
|
||||
name: 'includeHtml',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['draft'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether the message should also be included as HTML',
|
||||
},
|
||||
{
|
||||
displayName: 'HTML Message',
|
||||
name: 'htmlMessage',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
includeHtml: [true],
|
||||
resource: ['draft'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
description: 'The HTML message body',
|
||||
},
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['draft'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
placeholder: 'Hello World!',
|
||||
description:
|
||||
'The message body. If HTML formatted, then you have to add and activate the option "HTML content" in the "Additional Options" section.',
|
||||
},
|
||||
{
|
||||
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: 'Attachment',
|
||||
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:
|
||||
'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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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: 'Attachment Prefix',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
type: 'string',
|
||||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
format: ['full', 'metadata', 'minimal', 'raw'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Full',
|
||||
value: 'full',
|
||||
description:
|
||||
'Returns the full email message data with body content parsed in the payload field',
|
||||
},
|
||||
{
|
||||
name: 'Metadata',
|
||||
value: 'metadata',
|
||||
description: 'Returns only email message ID, labels, and email headers',
|
||||
},
|
||||
{
|
||||
name: 'Minimal',
|
||||
value: 'minimal',
|
||||
description:
|
||||
'Returns only email message ID and labels; does not return the email headers, body, or payload',
|
||||
},
|
||||
{
|
||||
name: 'RAW',
|
||||
value: 'raw',
|
||||
description:
|
||||
'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used',
|
||||
},
|
||||
{
|
||||
name: 'Resolved',
|
||||
value: 'resolved',
|
||||
description:
|
||||
'Returns the full email with all data resolved and attachments saved as binary data',
|
||||
},
|
||||
],
|
||||
default: 'resolved',
|
||||
description: 'The format to return the message in',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* draft:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getAll'],
|
||||
resource: ['draft'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: '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: 10,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getAll'],
|
||||
resource: ['draft'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attachment Prefix',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
type: 'string',
|
||||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
format: ['full', 'ids', 'metadata', 'minimal', 'raw'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Full',
|
||||
value: 'full',
|
||||
description:
|
||||
'Returns the full email message data with body content parsed in the payload field',
|
||||
},
|
||||
{
|
||||
name: 'IDs',
|
||||
value: 'ids',
|
||||
description: 'Returns only the IDs of the emails',
|
||||
},
|
||||
{
|
||||
name: 'Metadata',
|
||||
value: 'metadata',
|
||||
description: 'Returns only email message ID, labels, and email headers',
|
||||
},
|
||||
{
|
||||
name: 'Minimal',
|
||||
value: 'minimal',
|
||||
description:
|
||||
'Returns only email message ID and labels; does not return the email headers, body, or payload',
|
||||
},
|
||||
{
|
||||
name: 'RAW',
|
||||
value: 'raw',
|
||||
description:
|
||||
'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used',
|
||||
},
|
||||
{
|
||||
name: 'Resolved',
|
||||
value: 'resolved',
|
||||
description:
|
||||
'Returns the full email with all data resolved and attachments saved as binary data',
|
||||
},
|
||||
],
|
||||
default: 'resolved',
|
||||
description: 'The format to return the message in',
|
||||
},
|
||||
{
|
||||
displayName: 'Include Spam and Trash',
|
||||
name: 'includeSpamTrash',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to include messages from SPAM and TRASH in the results',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,810 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type IBinaryKeyData,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type IHttpRequestMethods,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeBaseDescription,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { oldVersionNotice } from '@utils/descriptions';
|
||||
|
||||
import { draftFields, draftOperations } from './DraftDescription';
|
||||
import { labelFields, labelOperations } from './LabelDescription';
|
||||
import { getLabels } from './loadOptions';
|
||||
import { messageFields, messageOperations } from './MessageDescription';
|
||||
import { messageLabelFields, messageLabelOperations } from './MessageLabelDescription';
|
||||
import type { IEmail } from '../../../../utils/sendAndWait/interfaces';
|
||||
import {
|
||||
encodeEmail,
|
||||
extractEmail,
|
||||
googleApiRequest,
|
||||
googleApiRequestAllItems,
|
||||
parseRawEmail,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
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: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['serviceAccount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gmailOAuth2',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
oldVersionNotice,
|
||||
{
|
||||
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: {
|
||||
getLabels,
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
let method: IHttpRequestMethods = 'GET';
|
||||
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);
|
||||
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/gmail/v1/users/me/labels',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
responseData = responseData.labels;
|
||||
|
||||
if (!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i);
|
||||
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);
|
||||
|
||||
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
|
||||
) {
|
||||
for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) {
|
||||
for (const binaryProperty of (property as string).split(',')) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, 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) {
|
||||
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);
|
||||
|
||||
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
|
||||
) {
|
||||
for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) {
|
||||
for (const binaryProperty of (property as string).split(',')) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, 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) {
|
||||
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);
|
||||
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);
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
Object.assign(qs, additionalFields);
|
||||
|
||||
if (qs.labelIds) {
|
||||
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);
|
||||
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 index = 0; index < responseData.length; index++) {
|
||||
responseData[index] = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/gmail/v1/users/me/messages/${responseData[index].id}`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
if (format === 'resolved') {
|
||||
const dataPropertyNameDownload =
|
||||
(additionalFields.dataPropertyAttachmentsPrefixName as string) || 'attachment_';
|
||||
|
||||
responseData[index] = await parseRawEmail.call(
|
||||
this,
|
||||
responseData[index],
|
||||
dataPropertyNameDownload,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (format !== 'resolved') {
|
||||
responseData = this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
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(',')) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, 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) {
|
||||
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);
|
||||
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);
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
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);
|
||||
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 index = 0; index < responseData.length; index++) {
|
||||
responseData[index] = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/gmail/v1/users/me/drafts/${responseData[index].id}`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
if (format === 'resolved') {
|
||||
const dataPropertyNameDownload =
|
||||
(additionalFields.dataPropertyAttachmentsPrefixName as string) || 'attachment_';
|
||||
const id = responseData[index].id;
|
||||
responseData[index] = await parseRawEmail.call(
|
||||
this,
|
||||
responseData[index].message,
|
||||
dataPropertyNameDownload,
|
||||
);
|
||||
|
||||
// Add the draft-id
|
||||
responseData[index].json.messageId = responseData[index].json.id;
|
||||
responseData[index].json.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (format !== 'resolved') {
|
||||
responseData = this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message } });
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { 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',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
action: 'Get many labels',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
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: '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: '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',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,436 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const messageOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
action: 'Delete a message',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
action: 'Get a message',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
action: 'Get many messages',
|
||||
},
|
||||
{
|
||||
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'],
|
||||
},
|
||||
},
|
||||
placeholder: '172ce2c4a72cc243',
|
||||
},
|
||||
{
|
||||
displayName: 'Thread ID',
|
||||
name: 'threadId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['reply'],
|
||||
},
|
||||
},
|
||||
placeholder: '172ce2c4a72cc243',
|
||||
},
|
||||
{
|
||||
displayName: 'Message ID',
|
||||
name: 'messageId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['reply'],
|
||||
},
|
||||
},
|
||||
placeholder: 'CAHNQoFsC6JMMbOBJgtjsqN0eEc+gDg2a=SQj-tWUebQeHMDgqQ@mail.gmail.com',
|
||||
},
|
||||
{
|
||||
displayName: 'Subject',
|
||||
name: 'subject',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['reply', 'send'],
|
||||
},
|
||||
},
|
||||
placeholder: 'Hello World!',
|
||||
},
|
||||
{
|
||||
displayName: 'HTML',
|
||||
name: 'includeHtml',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['send', 'reply'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether the message should also be included as HTML',
|
||||
},
|
||||
{
|
||||
displayName: 'HTML Message',
|
||||
name: 'htmlMessage',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
includeHtml: [true],
|
||||
resource: ['message'],
|
||||
operation: ['reply', 'send'],
|
||||
},
|
||||
},
|
||||
description: 'The HTML message body',
|
||||
},
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['reply', 'send'],
|
||||
},
|
||||
},
|
||||
description: 'Plain text message body',
|
||||
},
|
||||
{
|
||||
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: 'Attachment',
|
||||
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: '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: '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: '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 <name@gmail.com>". The email address has to match the email address of the logged in user for the API.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'Attachment Prefix',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
type: 'string',
|
||||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
format: ['full', 'metadata', 'minimal', 'raw'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* message:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getAll'],
|
||||
resource: ['message'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: '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: 10,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['getAll'],
|
||||
resource: ['message'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attachment Prefix',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
type: 'string',
|
||||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
format: ['full', 'ids', 'metadata', 'minimal', 'raw'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Prefix for name of the binary property to which to write the attachment. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0".',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Full',
|
||||
value: 'full',
|
||||
description:
|
||||
'Returns the full email message data with body content parsed in the payload field',
|
||||
},
|
||||
{
|
||||
name: 'IDs',
|
||||
value: 'ids',
|
||||
description: 'Returns only the IDs of the emails',
|
||||
},
|
||||
{
|
||||
name: 'Metadata',
|
||||
value: 'metadata',
|
||||
description: 'Returns only email message ID, labels, and email headers',
|
||||
},
|
||||
{
|
||||
name: 'Minimal',
|
||||
value: 'minimal',
|
||||
description:
|
||||
'Returns only email message ID and labels; does not return the email headers, body, or payload',
|
||||
},
|
||||
{
|
||||
name: 'RAW',
|
||||
value: 'raw',
|
||||
description:
|
||||
'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used',
|
||||
},
|
||||
{
|
||||
name: 'Resolved',
|
||||
value: 'resolved',
|
||||
description:
|
||||
'Returns the full email with all data resolved and attachments saved as binary data',
|
||||
},
|
||||
],
|
||||
default: 'resolved',
|
||||
description: 'The format to return the message in',
|
||||
},
|
||||
{
|
||||
displayName: 'Include Spam 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/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'q',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, "from:someuser@example.com rfc822msgid:<somemsgid@example.com> is:unread". Parameter cannot be used when accessing the api using the gmail.metadata scope.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const messageLabelOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['messageLabel'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
action: 'Add a label to a message',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
action: 'Remove a label from a message',
|
||||
},
|
||||
],
|
||||
default: 'add',
|
||||
},
|
||||
];
|
||||
|
||||
export const messageLabelFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Message ID',
|
||||
name: 'messageId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['messageLabel'],
|
||||
operation: ['add', 'remove'],
|
||||
},
|
||||
},
|
||||
placeholder: '172ce2c4a72cc243',
|
||||
},
|
||||
{
|
||||
displayName: 'Label Names or IDs',
|
||||
name: 'labelIds',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLabels',
|
||||
},
|
||||
default: [],
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['messageLabel'],
|
||||
operation: ['add', 'remove'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'The ID of the label. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,3 @@
|
||||
import { getLabels } from '../GenericFunctions';
|
||||
|
||||
export { getLabels };
|
||||
@@ -0,0 +1,311 @@
|
||||
import type { 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 many 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: 'To reply to an existing thread, specify the exact subject title of that thread.',
|
||||
name: 'threadNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['draft'], operation: ['create'] } },
|
||||
},
|
||||
{
|
||||
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: '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: '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: 'From Alias Name or ID',
|
||||
name: 'fromAlias',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Select the alias to send the email from. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getGmailAliases',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Send Replies To',
|
||||
name: 'replyTo',
|
||||
type: 'string',
|
||||
placeholder: 'reply@example.com',
|
||||
default: '',
|
||||
description: 'The email address that the reply message is sent to',
|
||||
},
|
||||
{
|
||||
displayName: 'Thread ID',
|
||||
name: 'threadId',
|
||||
type: 'string',
|
||||
placeholder: '18cc573e2431878f',
|
||||
default: '',
|
||||
description: 'The identifier of the thread to attach the draft',
|
||||
},
|
||||
{
|
||||
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: '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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,832 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||
|
||||
import { draftFields, draftOperations } from './DraftDescription';
|
||||
import { labelFields, labelOperations } from './LabelDescription';
|
||||
import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions';
|
||||
import { messageFields, messageOperations } from './MessageDescription';
|
||||
import { threadFields, threadOperations } from './ThreadDescription';
|
||||
import { addThreadHeadersToEmail } from './utils/draft';
|
||||
import { configureWaitTillDate } from '../../../../utils/sendAndWait/configureWaitTillDate.util';
|
||||
import { sendAndWaitWebhooksDescription } from '../../../../utils/sendAndWait/descriptions';
|
||||
import type { IEmail } from '../../../../utils/sendAndWait/interfaces';
|
||||
import {
|
||||
createEmail,
|
||||
getSendAndWaitProperties,
|
||||
SEND_AND_WAIT_WAITING_TOOLTIP,
|
||||
sendAndWaitWebhook,
|
||||
} from '../../../../utils/sendAndWait/utils';
|
||||
import {
|
||||
encodeEmail,
|
||||
googleApiRequest,
|
||||
googleApiRequestAllItems,
|
||||
parseRawEmail,
|
||||
prepareEmailAttachments,
|
||||
prepareEmailBody,
|
||||
prepareEmailsInput,
|
||||
prepareQuery,
|
||||
simplifyOutput,
|
||||
unescapeSnippets,
|
||||
} from '../GenericFunctions';
|
||||
import { replyToEmail } from '../utils/replyToEmail';
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Gmail',
|
||||
name: 'gmail',
|
||||
icon: 'file:gmail.svg',
|
||||
group: ['transform'],
|
||||
version: [2, 2.1, 2.2],
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume the Gmail API',
|
||||
defaults: {
|
||||
name: 'Gmail',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
usableAsTool: true,
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['serviceAccount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gmailOAuth2',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
waitingNodeTooltip: SEND_AND_WAIT_WAITING_TOOLTIP,
|
||||
webhooks: sendAndWaitWebhooksDescription,
|
||||
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,
|
||||
...getSendAndWaitProperties([
|
||||
{
|
||||
displayName: 'To',
|
||||
name: 'sendTo',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'e.g. info@example.com',
|
||||
},
|
||||
]),
|
||||
//-------------------------------
|
||||
// Thread Operations
|
||||
//-------------------------------
|
||||
...threadOperations,
|
||||
...threadFields,
|
||||
//-------------------------------
|
||||
],
|
||||
};
|
||||
|
||||
export class GmailV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
getLabels,
|
||||
getThreadMessages,
|
||||
getGmailAliases,
|
||||
},
|
||||
};
|
||||
|
||||
webhook = sendAndWaitWebhook;
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
const instanceId = this.getInstanceId();
|
||||
|
||||
if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) {
|
||||
const email: IEmail = createEmail(this);
|
||||
|
||||
await googleApiRequest.call(this, 'POST', '/gmail/v1/users/me/messages/send', {
|
||||
raw: await encodeEmail(email),
|
||||
});
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return [this.getInputData()];
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
responseData = await googleApiRequest.call(this, 'GET', '/gmail/v1/users/me/labels');
|
||||
|
||||
responseData = this.helpers.returnJsonArray(responseData.labels as IDataObject[]);
|
||||
|
||||
if (!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i);
|
||||
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);
|
||||
const sendTo = this.getNodeParameter('sendTo', i) as string;
|
||||
let qs: IDataObject = {};
|
||||
|
||||
const to = prepareEmailsInput.call(this, sendTo, 'To', i);
|
||||
let cc = '';
|
||||
let bcc = '';
|
||||
let replyTo = '';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (options.replyTo) {
|
||||
replyTo = prepareEmailsInput.call(this, options.replyTo as string, 'ReplyTo', i);
|
||||
}
|
||||
|
||||
let attachments: IDataObject[] = [];
|
||||
|
||||
if (options.attachmentsUi) {
|
||||
attachments = await prepareEmailAttachments.call(
|
||||
this,
|
||||
options.attachmentsUi as IDataObject,
|
||||
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}>`;
|
||||
}
|
||||
|
||||
let appendAttribution = options.appendAttribution;
|
||||
if (appendAttribution === undefined) {
|
||||
appendAttribution = nodeVersion >= 2.1;
|
||||
}
|
||||
|
||||
const email: IEmail = {
|
||||
from,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
replyTo,
|
||||
subject: this.getNodeParameter('subject', i) as string,
|
||||
...prepareEmailBody.call(this, i, appendAttribution as boolean, instanceId),
|
||||
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);
|
||||
|
||||
responseData = await replyToEmail.call(this, messageIdGmail, options, i, nodeVersion);
|
||||
}
|
||||
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, {});
|
||||
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 as IDataObject]);
|
||||
nodeExecutionData = { json };
|
||||
}
|
||||
|
||||
responseData = [nodeExecutionData];
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const filters = this.getNodeParameter('filters', i, {});
|
||||
const qs: IDataObject = {};
|
||||
Object.assign(qs, prepareQuery.call(this, filters, i));
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'messages',
|
||||
'GET',
|
||||
'/gmail/v1/users/me/messages',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.maxResults = this.getNodeParameter('limit', i);
|
||||
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 index = 0; index < responseData.length; index++) {
|
||||
responseData[index] = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/gmail/v1/users/me/messages/${responseData[index].id}`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
if (!simple) {
|
||||
const dataPropertyNameDownload =
|
||||
(options.dataPropertyAttachmentsPrefixName as string) || 'attachment_';
|
||||
|
||||
responseData[index] = await parseRawEmail.call(
|
||||
this,
|
||||
responseData[index],
|
||||
dataPropertyNameDownload,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (simple) {
|
||||
responseData = this.helpers.returnJsonArray(
|
||||
await simplifyOutput.call(this, responseData as IDataObject[]),
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
let qs: IDataObject = {};
|
||||
|
||||
let to = '';
|
||||
let cc = '';
|
||||
let bcc = '';
|
||||
let replyTo = '';
|
||||
let fromAlias = '';
|
||||
let threadId = null;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (options.replyTo) {
|
||||
replyTo = prepareEmailsInput.call(this, options.replyTo as string, 'ReplyTo', i);
|
||||
}
|
||||
|
||||
if (options.fromAlias) {
|
||||
fromAlias = options.fromAlias as string;
|
||||
}
|
||||
|
||||
if (options.threadId && typeof options.threadId === 'string') {
|
||||
threadId = options.threadId;
|
||||
}
|
||||
|
||||
let attachments: IDataObject[] = [];
|
||||
if (options.attachmentsUi) {
|
||||
attachments = await prepareEmailAttachments.call(
|
||||
this,
|
||||
options.attachmentsUi as IDataObject,
|
||||
i,
|
||||
);
|
||||
if (attachments.length) {
|
||||
qs = {
|
||||
userId: 'me',
|
||||
uploadType: 'media',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const email: IEmail = {
|
||||
from: fromAlias,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
replyTo,
|
||||
subject: this.getNodeParameter('subject', i) as string,
|
||||
...prepareEmailBody.call(this, i),
|
||||
attachments,
|
||||
};
|
||||
|
||||
if (threadId) {
|
||||
// If a threadId is set, we need to add the Message-ID of the last message in the thread
|
||||
// to the email so that Gmail can correctly associate the draft with the thread
|
||||
await addThreadHeadersToEmail.call(this, email, threadId as string);
|
||||
}
|
||||
|
||||
const body = {
|
||||
message: {
|
||||
raw: await encodeEmail(email),
|
||||
threadId: threadId || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
qs.format = 'raw';
|
||||
|
||||
responseData = await googleApiRequest.call(this, 'GET', endpoint, {}, qs);
|
||||
|
||||
const dataPropertyNameDownload =
|
||||
(options.dataPropertyAttachmentsPrefixName as string) || 'attachment_';
|
||||
|
||||
const 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);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
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);
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/gmail/v1/users/me/drafts',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.drafts;
|
||||
}
|
||||
|
||||
if (responseData === undefined) {
|
||||
responseData = [];
|
||||
}
|
||||
|
||||
qs.format = 'raw';
|
||||
|
||||
for (let index = 0; index < responseData.length; index++) {
|
||||
responseData[index] = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/gmail/v1/users/me/drafts/${responseData[index].id}`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
|
||||
const dataPropertyNameDownload =
|
||||
(options.dataPropertyAttachmentsPrefixName as string) || 'attachment_';
|
||||
const id = responseData[index].id;
|
||||
responseData[index] = await parseRawEmail.call(
|
||||
this,
|
||||
responseData[index].message,
|
||||
dataPropertyNameDownload,
|
||||
);
|
||||
|
||||
// Add the draft-id
|
||||
responseData[index].json.messageId = responseData[index].json.id;
|
||||
responseData[index].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);
|
||||
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 as IDataObject[]),
|
||||
);
|
||||
} else {
|
||||
responseData.messages = await simplifyOutput.call(
|
||||
this,
|
||||
responseData.messages as IDataObject[],
|
||||
);
|
||||
responseData = [{ json: responseData }];
|
||||
}
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
//https://developers.google.com/gmail/api/reference/rest/v1/users.threads/list
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
const qs: IDataObject = {};
|
||||
Object.assign(qs, prepareQuery.call(this, filters, i));
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'threads',
|
||||
'GET',
|
||||
'/gmail/v1/users/me/threads',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.maxResults = this.getNodeParameter('limit', i);
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/gmail/v1/users/me/threads',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.threads;
|
||||
}
|
||||
|
||||
if (responseData === undefined) {
|
||||
responseData = [];
|
||||
}
|
||||
|
||||
responseData = this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
if (operation === 'reply') {
|
||||
const messageIdGmail = this.getNodeParameter('messageId', i) as string;
|
||||
const options = this.getNodeParameter('options', i);
|
||||
|
||||
responseData = await replyToEmail.call(this, messageIdGmail, options, i, nodeVersion);
|
||||
}
|
||||
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 as IDataObject[]),
|
||||
{
|
||||
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 as Error, {
|
||||
description: error.description,
|
||||
itemIndex: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
['draft', 'message', 'thread'].includes(resource) &&
|
||||
['get', 'getAll'].includes(operation)
|
||||
) {
|
||||
return [unescapeSnippets(returnData)];
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { 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 many 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',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,559 @@
|
||||
import { SEND_AND_WAIT_OPERATION, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { appendAttributionOption } from '../../../../utils/descriptions';
|
||||
|
||||
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 many 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',
|
||||
},
|
||||
{
|
||||
name: 'Send and Wait for Response',
|
||||
value: SEND_AND_WAIT_OPERATION,
|
||||
action: 'Send message and wait for response',
|
||||
},
|
||||
],
|
||||
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: 'html',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'html',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['send', 'reply'],
|
||||
},
|
||||
hide: {
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Email Type',
|
||||
name: 'emailType',
|
||||
type: 'options',
|
||||
default: 'html',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'html',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['message'],
|
||||
operation: ['send', 'reply'],
|
||||
'@version': [2],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{
|
||||
...appendAttributionOption,
|
||||
description:
|
||||
'Whether to include the phrase “This email was sent automatically with n8n” to the end of the email',
|
||||
},
|
||||
{
|
||||
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: 'Send Replies To',
|
||||
name: 'replyTo',
|
||||
type: 'string',
|
||||
placeholder: 'reply@example.com',
|
||||
default: '',
|
||||
description: 'The email address that the reply message is sent to',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/operation': ['reply'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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/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/expressions/">expression</a>',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,432 @@
|
||||
import type { 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 many 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/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',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
replyToRecipientsOnly: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Reply to Recipients Only',
|
||||
name: 'replyToRecipientsOnly',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to exclude the sender from the reply',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
replyToSenderOnly: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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/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/expressions/">expression</a>',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
||||
|
||||
import { googleApiRequest, getLabels } from '../GenericFunctions';
|
||||
|
||||
export async function 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;
|
||||
}
|
||||
|
||||
export async function getGmailAliases(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { sendAs } = await googleApiRequest.call(this, 'GET', '/gmail/v1/users/me/settings/sendAs');
|
||||
|
||||
for (const alias of sendAs || []) {
|
||||
const displayName = alias.isDefault ? `${alias.sendAsEmail} (Default)` : alias.sendAsEmail;
|
||||
returnData.push({
|
||||
name: displayName,
|
||||
value: alias.sendAsEmail,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export { getLabels };
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import type { IEmail } from '@utils/sendAndWait/interfaces';
|
||||
|
||||
import { googleApiRequest } from '../../GenericFunctions';
|
||||
|
||||
function setEmailReplyHeaders(email: IEmail, messageId: string | undefined): void {
|
||||
if (messageId) {
|
||||
email.inReplyTo = messageId;
|
||||
email.references = messageId;
|
||||
}
|
||||
}
|
||||
|
||||
function setThreadHeaders(
|
||||
email: IEmail,
|
||||
thread: { messages: Array<{ payload: { headers: Array<{ name: string; value: string }> } }> },
|
||||
): void {
|
||||
if (thread?.messages) {
|
||||
const lastMessage = thread.messages.length - 1;
|
||||
const messageId = thread.messages[lastMessage].payload.headers.find(
|
||||
(header: { name: string; value: string }) =>
|
||||
header.name.toLowerCase().includes('message') && header.name.toLowerCase().includes('id'),
|
||||
)?.value;
|
||||
|
||||
setEmailReplyHeaders(email, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds inReplyTo and reference headers to the email if threadId is provided.
|
||||
*/
|
||||
export async function addThreadHeadersToEmail(
|
||||
this: IExecuteFunctions,
|
||||
email: IEmail,
|
||||
threadId: string,
|
||||
): Promise<void> {
|
||||
const thread = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/gmail/v1/users/me/threads/${threadId}`,
|
||||
{},
|
||||
{ format: 'metadata', metadataHeaders: ['Message-ID'] },
|
||||
);
|
||||
|
||||
setThreadHeaders(email, thread);
|
||||
}
|
||||
Reference in New Issue
Block a user