fix(Gmail Node): Use Reply-To header when replying to a message (#22145)

This commit is contained in:
RomanDavydchuk 2025-11-28 12:48:24 +02:00 committed by GitHub
parent 070c452d43
commit 2a3cba74ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 24 deletions

View File

@ -13,13 +13,14 @@ export class Gmail extends VersionedNodeType {
group: ['transform'],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Gmail API',
defaultVersion: 2.1,
defaultVersion: 2.2,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new GmailV1(baseDescription),
2: new GmailV2(baseDescription),
2.1: new GmailV2(baseDescription),
2.2: new GmailV2(baseDescription),
};
super(nodeVersions, baseDescription);

View File

@ -87,7 +87,7 @@ describe('replyToEmail', () => {
.mockResolvedValueOnce(mockSentMessage); // POST send message
const options: IDataObject = {};
const result = await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
const result = await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(
1,
@ -139,7 +139,7 @@ describe('replyToEmail', () => {
ccList: 'cc@example.com',
};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedPrepareEmailsInput).toHaveBeenCalledWith('cc@example.com', 'CC', 0);
@ -164,7 +164,7 @@ describe('replyToEmail', () => {
bccList: 'bcc@example.com',
};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedPrepareEmailsInput).toHaveBeenCalledWith('bcc@example.com', 'BCC', 0);
@ -196,7 +196,7 @@ describe('replyToEmail', () => {
},
};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedPrepareEmailAttachments).toHaveBeenCalledWith(options.attachmentsUi, 0);
@ -245,7 +245,7 @@ describe('replyToEmail', () => {
replyToSenderOnly: true,
};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
// Verify that only the sender is included in the "To" field
expect(mockedEncodeEmail).toHaveBeenCalledWith(
@ -284,7 +284,7 @@ describe('replyToEmail', () => {
replyToRecipientsOnly: true,
};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
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(
@ -317,7 +317,7 @@ describe('replyToEmail', () => {
senderName: 'Custom Sender Name',
};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedEncodeEmail).toHaveBeenCalledWith(
expect.objectContaining({
@ -347,7 +347,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
// Should handle both formats correctly
expect(mockedGoogleApiRequest).toHaveBeenNthCalledWith(
@ -383,7 +383,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedGoogleApiRequest).toHaveBeenCalledTimes(3);
@ -414,7 +414,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
// Should handle missing subject gracefully (empty string)
expect(mockedGoogleApiRequest).toHaveBeenCalledTimes(3);
@ -440,7 +440,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
// Should handle missing message ID gracefully (empty string)
expect(mockedGoogleApiRequest).toHaveBeenCalledTimes(3);
@ -459,7 +459,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedPrepareEmailBody).toHaveBeenCalledWith(0);
});
@ -485,7 +485,7 @@ describe('replyToEmail', () => {
.mockReturnValueOnce('cc@example.com, ')
.mockReturnValueOnce('bcc@example.com, ');
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
expect(mockedEncodeEmail).toHaveBeenCalledWith({
from: 'Test Sender <user@gmail.com>',
@ -520,7 +520,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
// Should handle missing headers with empty strings
expect(mockedEncodeEmail).toHaveBeenCalledWith(
@ -554,7 +554,7 @@ describe('replyToEmail', () => {
const options: IDataObject = {};
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0);
await replyToEmail.call(mockExecuteFunctions, 'message123', options, 0, 2.2);
// Should properly format emails with brackets
expect(mockedEncodeEmail).toHaveBeenCalledWith(
@ -570,4 +570,62 @@ describe('replyToEmail', () => {
}),
);
});
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>'),
}),
);
});
});

View File

@ -17,6 +17,7 @@ export async function replyToEmail(
gmailId: string,
options: IDataObject,
itemIndex: number,
nodeVersion: number,
) {
if (options.replyToSenderOnly && options.replyToRecipientsOnly) {
throw new NodeOperationError(
@ -97,14 +98,19 @@ export async function replyToEmail(
}
};
let replyToHeaderName = 'from';
if (nodeVersion >= 2.2 && payload.headers.some((h) => h.name.toLowerCase() === 'reply-to')) {
replyToHeaderName = 'reply-to';
}
for (const header of payload.headers) {
const headerName = (header.name || '').toLowerCase();
if (headerName === 'from' && !replyToRecipientsOnly) {
const from = header.value;
if (from.includes('<') && from.includes('>')) {
to.push(from);
if (headerName === replyToHeaderName && !replyToRecipientsOnly) {
const replyToEmail = header.value;
if (replyToEmail.includes('<') && replyToEmail.includes('>')) {
to.push(replyToEmail);
} else {
to.push(`<${from}>`);
to.push(`<${replyToEmail}>`);
}
}

View File

@ -59,7 +59,7 @@ const versionDescription: INodeTypeDescription = {
name: 'gmail',
icon: 'file:gmail.svg',
group: ['transform'],
version: [2, 2.1],
version: [2, 2.1, 2.2],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Gmail API',
defaults: {
@ -353,7 +353,7 @@ export class GmailV2 implements INodeType {
const messageIdGmail = this.getNodeParameter('messageId', i) as string;
const options = this.getNodeParameter('options', i);
responseData = await replyToEmail.call(this, messageIdGmail, options, i);
responseData = await replyToEmail.call(this, messageIdGmail, options, i, nodeVersion);
}
if (operation === 'get') {
//https://developers.google.com/gmail/api/v1/reference/users/messages/get
@ -777,7 +777,7 @@ export class GmailV2 implements INodeType {
const messageIdGmail = this.getNodeParameter('messageId', i) as string;
const options = this.getNodeParameter('options', i);
responseData = await replyToEmail.call(this, messageIdGmail, options, i);
responseData = await replyToEmail.call(this, messageIdGmail, options, i, nodeVersion);
}
if (operation === 'trash') {
//https://developers.google.com/gmail/api/reference/rest/v1/users.threads/trash