mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
fix(Gmail Node): Do not break threads while creating drafts (#16272)
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -250,7 +250,7 @@ export async function encodeEmail(email: IEmail) {
|
|||||||
|
|
||||||
const mailBody = await mail.build();
|
const mailBody = await mail.build();
|
||||||
|
|
||||||
return mailBody.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
|
return mailBody.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function googleApiRequestAllItems(
|
export async function googleApiRequestAllItems(
|
||||||
|
|||||||
@@ -193,6 +193,15 @@ describe('Test Gmail Node v2', () => {
|
|||||||
.query({ userId: 'me', uploadType: 'media' })
|
.query({ userId: 'me', uploadType: 'media' })
|
||||||
.reply(200, messages[0]);
|
.reply(200, messages[0]);
|
||||||
gmailNock.delete('/v1/users/me/drafts/test-draft-id').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: ['jjkjkjkf@reply.com'] } }],
|
||||||
|
});
|
||||||
gmailNock
|
gmailNock
|
||||||
.get('/v1/users/me/drafts/test-draft-id')
|
.get('/v1/users/me/drafts/test-draft-id')
|
||||||
.query({ format: 'raw' })
|
.query({ format: 'raw' })
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { mock } from 'jest-mock-extended';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { IEmail } from '@utils/sendAndWait/interfaces';
|
||||||
|
|
||||||
|
import * as GenericFunctions from '../../GenericFunctions';
|
||||||
import { parseRawEmail, prepareTimestamp } from '../../GenericFunctions';
|
import { parseRawEmail, prepareTimestamp } from '../../GenericFunctions';
|
||||||
|
import { addThreadHeadersToEmail } from '../../v2/utils/draft';
|
||||||
|
|
||||||
const node: INode = {
|
const node: INode = {
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -128,3 +132,73 @@ describe('parseRawEmail', () => {
|
|||||||
expect(typeof json.date).toBe('string');
|
expect(typeof json.date).toBe('string');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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: [{ value: '<old-id@example.com>' }] } },
|
||||||
|
{ payload: { headers: [{ 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.reference).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: [{ 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.reference).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.reference).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ export const draftFields: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
placeholder: 'Hello World!',
|
placeholder: 'Hello World!',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'To reply to an existing thread, specify the exact subject title of that thread.',
|
||||||
|
name: 'threadNotice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { resource: ['draft'], operation: ['create'] } },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Email Type',
|
displayName: 'Email Type',
|
||||||
name: 'emailType',
|
name: 'emailType',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { labelFields, labelOperations } from './LabelDescription';
|
|||||||
import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions';
|
import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions';
|
||||||
import { messageFields, messageOperations } from './MessageDescription';
|
import { messageFields, messageOperations } from './MessageDescription';
|
||||||
import { threadFields, threadOperations } from './ThreadDescription';
|
import { threadFields, threadOperations } from './ThreadDescription';
|
||||||
|
import { addThreadHeadersToEmail } from './utils/draft';
|
||||||
import { configureWaitTillDate } from '../../../../utils/sendAndWait/configureWaitTillDate.util';
|
import { configureWaitTillDate } from '../../../../utils/sendAndWait/configureWaitTillDate.util';
|
||||||
import { sendAndWaitWebhooksDescription } from '../../../../utils/sendAndWait/descriptions';
|
import { sendAndWaitWebhooksDescription } from '../../../../utils/sendAndWait/descriptions';
|
||||||
import type { IEmail } from '../../../../utils/sendAndWait/interfaces';
|
import type { IEmail } from '../../../../utils/sendAndWait/interfaces';
|
||||||
@@ -560,6 +561,12 @@ export class GmailV2 implements INodeType {
|
|||||||
attachments,
|
attachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (threadId && options.replyTo) {
|
||||||
|
// If a threadId is set, we need to add the Message-ID of the last message in the thread
|
||||||
|
// to the email so that Gmail can correctly associate the draft with the thread
|
||||||
|
await addThreadHeadersToEmail.call(this, email, threadId);
|
||||||
|
}
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
message: {
|
message: {
|
||||||
raw: await encodeEmail(email),
|
raw: await encodeEmail(email),
|
||||||
|
|||||||
30
packages/nodes-base/nodes/Google/Gmail/v2/utils/draft.ts
Normal file
30
packages/nodes-base/nodes/Google/Gmail/v2/utils/draft.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { IEmail } from '@utils/sendAndWait/interfaces';
|
||||||
|
|
||||||
|
import { googleApiRequest } from '../../GenericFunctions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds inReplyTo and reference headers to the email if threadId is provided.
|
||||||
|
*/
|
||||||
|
export async function addThreadHeadersToEmail(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
email: IEmail,
|
||||||
|
threadId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const thread = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/gmail/v1/users/me/threads/${threadId}`,
|
||||||
|
{},
|
||||||
|
{ format: 'metadata', metadataHeaders: ['Message-ID'] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (thread?.messages) {
|
||||||
|
const lastMessage = thread.messages.length - 1;
|
||||||
|
const messageId: string = thread.messages[lastMessage].payload.headers[0].value;
|
||||||
|
|
||||||
|
email.inReplyTo = messageId;
|
||||||
|
email.reference = messageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user