mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-27 23:07:12 +02:00
fix(Gmail Node): Use Reply-To header when replying to a message (#22145)
This commit is contained in:
parent
070c452d43
commit
2a3cba74ee
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}>`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user