-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: add workaround for “greeting never received” (#5079)
* fix: add workaround for “greeting never received” * add changeset * include suggestions from review * Changes from lint:fix * improve tests --------- Co-authored-by: cloud-sdk-js <[email protected]>
- Loading branch information
Showing
6 changed files
with
252 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sap-cloud-sdk/mail-client': patch | ||
--- | ||
|
||
[Fixed Issue] Fix sending e-mails through socks proxies in Node 20 and higher. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import { EventEmitter } from 'node:stream'; | ||
import nodemailer from 'nodemailer'; | ||
import { SocksClient } from 'socks'; | ||
import { registerDestination } from '@sap-cloud-sdk/connectivity'; | ||
|
@@ -25,15 +26,6 @@ describe('mail client', () => { | |
jest.resetAllMocks(); | ||
}); | ||
|
||
const mockSocket = { | ||
socket: { | ||
_readableState: { | ||
readableListening: false | ||
}, | ||
end: jest.fn(), | ||
destroy: jest.fn() | ||
} | ||
}; | ||
const mockTransport = { | ||
sendMail: jest.fn(), | ||
close: jest.fn(), | ||
|
@@ -86,12 +78,11 @@ describe('mail client', () => { | |
}); | ||
|
||
it('should work with destination from service - proxy-type OnPremise', async () => { | ||
jest | ||
.spyOn(SocksClient, 'createConnection') | ||
.mockReturnValue(mockSocket as any); | ||
mockSocketConnection(); | ||
jest | ||
.spyOn(nodemailer, 'createTransport') | ||
.mockReturnValue(mockTransport as any); | ||
|
||
const mailOptions1: MailConfig = { | ||
from: '[email protected]', | ||
to: '[email protected]' | ||
|
@@ -215,27 +206,17 @@ describe('mail client', () => { | |
await expect( | ||
sendMail(destination, [mailOptions1, mailOptions2], mailClientOptions) | ||
).resolves.not.toThrow(); | ||
expect(spyCreateTransport).toBeCalledTimes(1); | ||
expect(spyCreateTransport).toBeCalledWith( | ||
expect(spyCreateTransport).toHaveBeenCalledTimes(1); | ||
expect(spyCreateTransport).toHaveBeenCalledWith( | ||
expect.objectContaining(mailClientOptions) | ||
); | ||
expect(spySendMail).toBeCalledTimes(2); | ||
expect(spySendMail).toBeCalledWith(mailOptions1); | ||
expect(spySendMail).toBeCalledWith(mailOptions2); | ||
expect(spyCloseTransport).toBeCalledTimes(1); | ||
expect(spySendMail).toHaveBeenCalledTimes(2); | ||
expect(spySendMail).toHaveBeenCalledWith(mailOptions1); | ||
expect(spySendMail).toHaveBeenCalledWith(mailOptions2); | ||
expect(spyCloseTransport).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('[OnPrem] should create transport/socket, send mails and close the transport/socket', async () => { | ||
const spyCreateSocket = jest | ||
.spyOn(SocksClient, 'createConnection') | ||
.mockReturnValue(mockSocket as any); | ||
const spyCreateTransport = jest | ||
.spyOn(nodemailer, 'createTransport') | ||
.mockReturnValue(mockTransport as any); | ||
const spySendMail = jest.spyOn(mockTransport, 'sendMail'); | ||
const spyCloseTransport = jest.spyOn(mockTransport, 'close'); | ||
const spyEndSocket = jest.spyOn(mockSocket.socket, 'end'); | ||
const spyDestroySocket = jest.spyOn(mockSocket.socket, 'destroy'); | ||
describe('on premise', () => { | ||
const destination: any = { | ||
originalProperties: { | ||
'mail.password': 'password', | ||
|
@@ -258,25 +239,96 @@ describe('mail client', () => { | |
from: '[email protected]', | ||
to: '[email protected]' | ||
}; | ||
await expect( | ||
sendMail(destination, mailOptions, { sdkOptions: { parallel: false } }) | ||
).resolves.not.toThrow(); | ||
expect(spyCreateSocket).toBeCalledTimes(1); | ||
expect(spyCreateTransport).toBeCalledTimes(1); | ||
expect(spySendMail).toBeCalledTimes(1); | ||
expect(spySendMail).toBeCalledWith(mailOptions); | ||
expect(spyCloseTransport).toBeCalledTimes(1); | ||
expect(spyEndSocket).toBeCalledTimes(1); | ||
expect(spyDestroySocket).toBeCalledTimes(1); | ||
|
||
it('should create transport/socket, send mails and close the transport/socket', async () => { | ||
const { connection, createConnectionSpy } = mockSocketConnection(); | ||
const spyCreateTransport = jest | ||
.spyOn(nodemailer, 'createTransport') | ||
.mockReturnValue(mockTransport as any); | ||
const spySendMail = jest.spyOn(mockTransport, 'sendMail'); | ||
|
||
const spyCloseTransport = jest.spyOn(mockTransport, 'close'); | ||
const spyEndSocket = jest.spyOn(connection.socket, 'end'); | ||
const spyDestroySocket = jest.spyOn(connection.socket, 'destroy'); | ||
|
||
await expect( | ||
sendMail(destination, mailOptions, { sdkOptions: { parallel: false } }) | ||
).resolves.not.toThrow(); | ||
expect(createConnectionSpy).toHaveBeenCalledTimes(1); | ||
expect(spyCreateTransport).toHaveBeenCalledTimes(1); | ||
expect(spySendMail).toHaveBeenCalledTimes(1); | ||
expect(spySendMail).toHaveBeenCalledWith(mailOptions); | ||
expect(spyCloseTransport).toHaveBeenCalledTimes(1); | ||
expect(spyEndSocket).toHaveBeenCalledTimes(1); | ||
expect(spyDestroySocket).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should resend greeting', async () => { | ||
const { connection } = mockSocketConnection(); | ||
jest | ||
.spyOn(nodemailer, 'createTransport') | ||
.mockReturnValue(mockTransport as any); | ||
|
||
const req = sendMail(destination, mailOptions, { | ||
sdkOptions: { parallel: false } | ||
}); | ||
|
||
// The socket emits data for the first time before nodemailer listens to it. | ||
// We re-emit the data until a listener listened for it. | ||
// In this test we listen for the data event to check that we in fact re-emit the message. | ||
const emitsTwice = new Promise(resolve => { | ||
let dataEmitCount = 0; | ||
const collectedData: string[] = []; | ||
connection.socket.on('data', data => { | ||
dataEmitCount++; | ||
collectedData.push(data.toString()); | ||
if (dataEmitCount === 2) { | ||
resolve(collectedData); | ||
} | ||
}); | ||
}); | ||
|
||
await expect(emitsTwice).resolves.toEqual([ | ||
'220 smtp.gmail.com ESMTP', | ||
'220 smtp.gmail.com ESMTP' | ||
]); | ||
await expect(req).resolves.not.toThrow(); | ||
}); | ||
|
||
it('should fail if nodemailer never listens to greeting', async () => { | ||
mockSocketConnection(); | ||
|
||
jest | ||
.spyOn(nodemailer, 'createTransport') | ||
.mockReturnValue(mockTransport as any); | ||
|
||
const req = sendMail(destination, mailOptions, { | ||
sdkOptions: { parallel: false } | ||
}); | ||
// jest.useFakeTimers(); | ||
// jest.advanceTimersByTime(1000); | ||
|
||
await expect(req).rejects.toThrow(); | ||
}, 10000); | ||
|
||
it('should throw if greeting (really) was not received', async () => { | ||
mockSocketConnection(true); | ||
|
||
await expect(() => | ||
sendMail(destination, mailOptions, { | ||
sdkOptions: { parallel: false } | ||
}) | ||
).rejects.toThrowErrorMatchingInlineSnapshot('"Something went wrong"'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('isMailSentInSequential', () => { | ||
it('should return false when the mail client options is undefined', () => { | ||
it('should return false when the mail client options are undefined', () => { | ||
expect(isMailSentInSequential()).toBe(false); | ||
}); | ||
|
||
it('should return false when the sdk options is undefined', () => { | ||
it('should return false when the sdk options are undefined', () => { | ||
const mailClientOptions: MailClientOptions = {}; | ||
expect(isMailSentInSequential(mailClientOptions)).toBe(false); | ||
}); | ||
|
@@ -327,3 +379,28 @@ function isValidSocksProxy(proxy) { | |
(proxy.type === 4 || proxy.type === 5) | ||
); | ||
} | ||
|
||
function mockSocketConnection(fail = false) { | ||
class MockSocket extends EventEmitter { | ||
end = jest.fn(); | ||
destroy = jest.fn(); | ||
} | ||
|
||
const connection = { | ||
socket: new MockSocket() | ||
}; | ||
const createConnectionSpy = jest | ||
.spyOn(SocksClient, 'createConnection') | ||
.mockImplementation(() => { | ||
setImmediate(() => { | ||
if (fail) { | ||
connection.socket.emit('error', 'Something went wrong'); | ||
} else { | ||
connection.socket.emit('data', '220 smtp.gmail.com ESMTP'); | ||
} | ||
}); | ||
return connection as any; | ||
}); | ||
|
||
return { connection, createConnectionSpy }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.