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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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> &lt;<a href=\"mailto:node8qa@gmail.com\" class=\"mp_address_email\">node8qa@gmail.com</a>&gt;</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');
});
});