Skip to content

Commit ea9b6b5

Browse files
ZhongpinWangcloud-sdk-jsdeekshas8
authored
fix: Mail client issue with port 465 on premise (#5150)
* fix: skip workaround for port 465 * chore: add test for checking if retrieve or resend is skipped * Changes from lint:fix * chore: add changeset * fix: use callback * chore: make test work again * fix: lint * fix: e2e due to proxy option * fix: lint * fix: mailClientOptions and proxyConfiguration * fix: remove unused mock implementation * refactor: add Options type back * refactor: buildSocksProxyUrl * fix: lint * fix: lint --------- Co-authored-by: cloud-sdk-js <[email protected]> Co-authored-by: Deeksha Sinha <[email protected]>
1 parent 7a2034c commit ea9b6b5

File tree

4 files changed

+47
-135
lines changed

4 files changed

+47
-135
lines changed

.changeset/lucky-badgers-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-cloud-sdk/mail-client': patch
3+
---
4+
5+
[Fixed Issue] Fix mail client issue for port 465 with on-premise setup.

packages/mail-client/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
"dependencies": {
4141
"@sap-cloud-sdk/connectivity": "^3.22.2",
4242
"@sap-cloud-sdk/util": "^3.22.2",
43-
"async-retry": "^1.3.3",
4443
"nodemailer": "6.9.16",
4544
"socks": "2.8.3"
4645
},

packages/mail-client/src/mail-client.spec.ts

Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../../../test-resources/test/test-util';
1111
import {
1212
buildSocksProxy,
13+
buildSocksProxyUrl,
1314
isMailSentInSequential,
1415
sendMail
1516
} from './mail-client';
@@ -29,7 +30,8 @@ describe('mail client', () => {
2930
const mockTransport = {
3031
sendMail: jest.fn(),
3132
close: jest.fn(),
32-
verify: jest.fn()
33+
verify: jest.fn(),
34+
set: jest.fn()
3335
};
3436

3537
it('should work with destination from service - proxy-type Internet', async () => {
@@ -239,98 +241,26 @@ describe('mail client', () => {
239241
'proxy-authorization': 'jwt'
240242
}
241243
};
244+
242245
const mailOptions: MailConfig = {
243246
244247
245248
};
246249

247250
it('should create transport/socket, send mails and close the transport/socket', async () => {
248-
const { connection, createConnectionSpy } = mockSocketConnection();
249251
const spyCreateTransport = jest
250252
.spyOn(nodemailer, 'createTransport')
251253
.mockReturnValue(mockTransport as any);
252-
const spySendMail = jest
253-
.spyOn(mockTransport, 'sendMail')
254-
.mockImplementation(() => {
255-
connection.socket.on('data', () => {});
256-
});
257-
254+
const spySendMail = jest.spyOn(mockTransport, 'sendMail');
258255
const spyCloseTransport = jest.spyOn(mockTransport, 'close');
259-
const spyEndSocket = jest.spyOn(connection.socket, 'end');
260-
const spyDestroySocket = jest.spyOn(connection.socket, 'destroy');
261256

262257
await expect(
263258
sendMail(destination, mailOptions, { sdkOptions: { parallel: false } })
264259
).resolves.not.toThrow();
265-
expect(createConnectionSpy).toHaveBeenCalledTimes(1);
266260
expect(spyCreateTransport).toHaveBeenCalledTimes(1);
267261
expect(spySendMail).toHaveBeenCalledTimes(1);
268262
expect(spySendMail).toHaveBeenCalledWith(mailOptions);
269263
expect(spyCloseTransport).toHaveBeenCalledTimes(1);
270-
expect(spyEndSocket).toHaveBeenCalledTimes(1);
271-
expect(spyDestroySocket).toHaveBeenCalledTimes(1);
272-
});
273-
274-
it('should resend greeting', async () => {
275-
const { connection } = mockSocketConnection();
276-
jest
277-
.spyOn(nodemailer, 'createTransport')
278-
.mockReturnValue(mockTransport as any);
279-
280-
const req = sendMail(destination, mailOptions, {
281-
sdkOptions: { parallel: false }
282-
});
283-
284-
// The socket emits data for the first time before nodemailer listens to it.
285-
// We re-emit the data until a listener listened for it.
286-
// In this test we listen for the data event to check that we in fact re-emit the message.
287-
const emitsTwice = new Promise(resolve => {
288-
let dataEmitCount = 0;
289-
const collectedData: string[] = [];
290-
connection.socket.on('data', data => {
291-
dataEmitCount++;
292-
collectedData.push(data.toString());
293-
if (dataEmitCount === 2) {
294-
resolve(collectedData);
295-
}
296-
});
297-
});
298-
299-
await expect(emitsTwice).resolves.toEqual([
300-
'220 smtp.gmail.com ESMTP',
301-
'220 smtp.gmail.com ESMTP'
302-
]);
303-
await expect(req).resolves.not.toThrow();
304-
});
305-
306-
it('should fail if nodemailer never listens to greeting', async () => {
307-
mockSocketConnection();
308-
309-
jest
310-
.spyOn(nodemailer, 'createTransport')
311-
.mockReturnValue(mockTransport as any);
312-
313-
const req = sendMail(destination, mailOptions, {
314-
sdkOptions: { parallel: false }
315-
});
316-
317-
await expect(req).rejects.toThrowErrorMatchingInlineSnapshot(
318-
'"Failed to re-emit greeting message. No data listener found."'
319-
);
320-
}, 15000);
321-
322-
it('should throw if greeting (really) was not received', async () => {
323-
const { connection } = mockSocketConnection(true);
324-
325-
jest.spyOn(mockTransport, 'sendMail').mockImplementation(() => {
326-
connection.socket.on('data', () => {});
327-
});
328-
329-
await expect(() =>
330-
sendMail(destination, mailOptions, {
331-
sdkOptions: { parallel: false }
332-
})
333-
).rejects.toThrowErrorMatchingInlineSnapshot('"Something went wrong"');
334264
});
335265
});
336266
});
@@ -378,6 +308,19 @@ describe('buildSocksProxy', () => {
378308
const proxy = buildSocksProxy(dest);
379309
expect(isValidSocksProxy(proxy)).toBe(true);
380310
});
311+
312+
it('build valid socks proxy url', () => {
313+
const dest: MailDestination = {
314+
proxyConfiguration: {
315+
host: 'www.proxy.com',
316+
port: 12345,
317+
protocol: 'socks',
318+
'proxy-authorization': 'jwt'
319+
}
320+
};
321+
const proxyUrl = buildSocksProxyUrl(dest);
322+
expect(proxyUrl).toBe('socks5://www.proxy.com:12345');
323+
});
381324
});
382325

383326
// copied from socks lib

packages/mail-client/src/mail-client.ts

Lines changed: 24 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import { resolveDestination } from '@sap-cloud-sdk/connectivity/internal';
33
import { createLogger } from '@sap-cloud-sdk/util';
44
import nodemailer from 'nodemailer';
55
import { SocksClient } from 'socks';
6-
import retry from 'async-retry';
76
import {
87
customAuthRequestHandler,
98
customAuthResponseHandler
109
} from './socket-proxy';
10+
// eslint-disable-next-line import/no-internal-modules
11+
import type { Options } from 'nodemailer/lib/smtp-pool';
1112
import type { Socket } from 'node:net';
1213
import type { SentMessageInfo, Transporter } from 'nodemailer';
1314
import type { SocksClientOptions, SocksProxy } from 'socks';
14-
// eslint-disable-next-line import/no-internal-modules
15-
import type { Options } from 'nodemailer/lib/smtp-pool';
1615
import type {
1716
MailClientOptions,
1817
MailConfig,
@@ -130,6 +129,18 @@ export function buildSocksProxy(mailDestination: MailDestination): SocksProxy {
130129
};
131130
}
132131

132+
/**
133+
* @internal
134+
*/
135+
export function buildSocksProxyUrl(mailDestination: MailDestination): string {
136+
if (!mailDestination.proxyConfiguration) {
137+
throw Error(
138+
'The proxy configuration is undefined, which is mandatory for creating a socket connection.'
139+
);
140+
}
141+
return `socks5://${mailDestination.proxyConfiguration?.host}:${mailDestination.proxyConfiguration?.port}`;
142+
}
143+
133144
async function createSocket(mailDestination: MailDestination): Promise<Socket> {
134145
const connectionOptions: SocksClientOptions = {
135146
proxy: buildSocksProxy(mailDestination),
@@ -140,54 +151,12 @@ async function createSocket(mailDestination: MailDestination): Promise<Socket> {
140151
}
141152
};
142153
const { socket } = await SocksClient.createConnection(connectionOptions);
143-
144154
return socket;
145155
}
146156

147-
function retrieveGreeting(socket: Socket): Promise<Buffer> {
148-
logger.debug('Waiting for SMTP greeting message...');
149-
return new Promise((resolve, reject) => {
150-
const onData = data => {
151-
logger.debug(`Data received from mail socket: ${data?.toString()}`);
152-
if (data?.toString().startsWith('220')) {
153-
logger.debug('Removing mail socket listeners...');
154-
socket.removeListener('data', onData);
155-
socket.removeListener('error', onError);
156-
resolve(data);
157-
}
158-
};
159-
160-
const onError = err => {
161-
reject(new Error(err));
162-
};
163-
164-
socket.on('data', onData);
165-
socket.on('error', onError);
166-
});
167-
}
168-
169-
async function resendGreetingUntilReceived(
170-
greeting: Buffer,
171-
socket: Socket
172-
): Promise<void> {
173-
return retry(
174-
() => {
175-
// resend the greeting message until a listener is attached
176-
// note: this is dangerous because there could be another listener that is not the mailer
177-
if (!socket.emit('data', greeting)) {
178-
throw new Error(
179-
'Failed to re-emit greeting message. No data listener found.'
180-
);
181-
}
182-
},
183-
{ maxRetryTime: 5000 }
184-
);
185-
}
186-
187157
function createTransport(
188158
mailDestination: MailDestination,
189-
mailClientOptions?: MailClientOptions,
190-
socket?: Socket
159+
mailClientOptions?: MailClientOptions
191160
): Transporter<SentMessageInfo> {
192161
const baseOptions: Options = {
193162
pool: true,
@@ -199,8 +168,11 @@ function createTransport(
199168
port: mailDestination.port
200169
};
201170

202-
if (mailDestination.proxyType === 'OnPremise' && socket) {
203-
baseOptions.connection = socket;
171+
if (mailDestination.proxyType === 'OnPremise') {
172+
mailClientOptions = {
173+
...(mailClientOptions || {}),
174+
proxy: buildSocksProxyUrl(mailDestination)
175+
};
204176
}
205177

206178
return nodemailer.createTransport({
@@ -274,14 +246,11 @@ async function sendMailWithNodemailer<T extends MailConfig>(
274246
mailClientOptions?: MailClientOptions
275247
): Promise<MailResponse[]> {
276248
let socket: Socket | undefined;
277-
let resendGreeting: Promise<void> | undefined;
278-
if (mailDestination.proxyType === 'OnPremise') {
249+
const transport = createTransport(mailDestination, mailClientOptions);
250+
transport.set('proxy_handler_socks5', async (_, __, callback) => {
279251
socket = await createSocket(mailDestination);
280-
// Workaround for incorrect order of events in nodemailer https://github.com/nodemailer/nodemailer/issues/1684
281-
const greeting = await retrieveGreeting(socket);
282-
resendGreeting = resendGreetingUntilReceived(greeting, socket);
283-
}
284-
const transport = createTransport(mailDestination, mailClientOptions, socket);
252+
callback(null, { connection: socket });
253+
});
285254

286255
const mailConfigsFromDestination =
287256
buildMailConfigsFromDestination(mailDestination);
@@ -298,10 +267,6 @@ async function sendMailWithNodemailer<T extends MailConfig>(
298267
mailConfigs
299268
);
300269

301-
if (resendGreeting) {
302-
await resendGreeting;
303-
}
304-
305270
teardown(transport, socket);
306271
return response;
307272
}

0 commit comments

Comments
 (0)