Skip to content

Commit ac34e97

Browse files
authored
Merge pull request #19735 from mozilla/upgrade-to-nodemailer7
chore(): upgrade to nodemailer 7
2 parents eafa9e7 + 2983fe3 commit ac34e97

8 files changed

Lines changed: 1049 additions & 116 deletions

File tree

libs/accounts/email-sender/src/email-sender.spec.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { ILogger } from '@fxa/shared/log';
99
import { AppError } from '../../errors/src';
1010

1111
jest.mock('nodemailer');
12-
jest.mock('@aws-sdk/client-ses');
1312
jest.mock('@sentry/node');
1413

1514
// additional imports after Jest has mocked modules
@@ -159,7 +158,7 @@ describe('EmailSender', () => {
159158
expect(mockTransport.sendMail).not.toHaveBeenCalled();
160159
});
161160

162-
it('falls back to ses when no username/password provided', async () => {
161+
it('uses SMTP without auth when no username/password provided', async () => {
163162
config = {
164163
...config,
165164
user: undefined,
@@ -173,14 +172,18 @@ describe('EmailSender', () => {
173172
mockLogger
174173
);
175174

176-
// Verify constructor called createTransport with SES configuration
177-
expect(mockNodemailer.createTransport).toHaveBeenCalledWith(
175+
expect(mockNodemailer.createTransport).toHaveBeenLastCalledWith(
178176
expect.objectContaining({
179-
SES: expect.objectContaining({
180-
ses: expect.any(Object),
181-
}),
182-
sendingRate: 5,
183-
maxConnections: 10,
177+
host: config.host,
178+
port: config.port,
179+
secure: config.secure,
180+
sendingRate: config.sendingRate,
181+
})
182+
);
183+
expect(mockNodemailer.createTransport).toHaveBeenLastCalledWith(
184+
expect.not.objectContaining({
185+
SES: expect.anything(),
186+
auth: expect.anything(),
184187
})
185188
);
186189

libs/accounts/email-sender/src/email-sender.ts

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { SES } from '@aws-sdk/client-ses';
65
import { Bounces } from './bounces';
76
import { StatsD } from 'hot-shots';
87
import { ILogger } from '@fxa/shared/log';
@@ -37,10 +36,14 @@ export type MailerConfig = {
3736
/** DNS timeout for smtp server connection. */
3837
dnsTimeout: number;
3938

40-
/** Optional user name. If not supplied, we fallback to local SES config. */
39+
/** Optional user name for SMTP authentication. */
4140
user?: string;
42-
/** Optional password. If not supplied, we fallback to local SES config. */
41+
/** Optional password for SMTP authentication. */
4342
password?: string;
43+
/** Optional flag to ignore STARTTLS even if the server advertises it. */
44+
ignoreTLS?: boolean;
45+
/** Optional flag to require STARTTLS even if the server does not advertise it. */
46+
requireTLS?: boolean;
4447
sesConfigurationSet?: string;
4548
sender: string;
4649
retry: {
@@ -81,6 +84,26 @@ export type Email = {
8184
headers: Record<string, string>;
8285
};
8386

87+
type SmtpTransportOptions = nodemailer.TransportOptions & {
88+
host: string;
89+
port: number;
90+
secure: boolean;
91+
pool: boolean;
92+
maxConnections: number;
93+
maxMessages: number;
94+
connectionTimeout: number;
95+
greetingTimeout: number;
96+
socketTimeout: number;
97+
dnsTimeout: number;
98+
sendingRate: number;
99+
ignoreTLS?: boolean;
100+
requireTLS?: boolean;
101+
auth?: {
102+
user: string;
103+
pass: string;
104+
};
105+
};
106+
84107
/**
85108
* Sends an email to end end user.
86109
*/
@@ -93,36 +116,10 @@ export class EmailSender {
93116
private readonly statsd: StatsD,
94117
private readonly log: ILogger
95118
) {
96-
// Determine auth credentials
97-
const auth = (() => {
98-
// If the user name and password are set use this
99-
if (config.user && config.password) {
100-
return {
101-
auth: {
102-
user: config.user,
103-
pass: config.password,
104-
},
105-
};
106-
}
107-
108-
// Otherwise fallback to the SES configuration
109-
const ses = new SES({
110-
// The key apiVersion is no longer supported in v3, and can be removed.
111-
// @deprecated The client uses the "latest" apiVersion.
112-
apiVersion: '2010-12-01',
113-
});
114-
return {
115-
SES: { ses },
116-
sendingRate: 5,
117-
maxConnections: 10,
118-
};
119-
})();
120-
121-
// Build node mailer options
122-
const options = {
119+
// Build SMTP-only nodemailer options
120+
const options: SmtpTransportOptions = {
123121
host: config.host,
124122
secure: config.secure,
125-
ignoreTLS: !config.secure,
126123
port: config.port,
127124
pool: config.pool,
128125
maxConnections: config.maxConnections,
@@ -131,9 +128,24 @@ export class EmailSender {
131128
greetingTimeout: config.greetingTimeout,
132129
socketTimeout: config.socketTimeout,
133130
dnsTimeout: config.dnsTimeout,
134-
sendingRate: this.config.sendingRate,
135-
...auth,
131+
sendingRate: config.sendingRate,
136132
};
133+
134+
if (config.user && config.password) {
135+
options.auth = {
136+
user: config.user,
137+
pass: config.password,
138+
};
139+
}
140+
141+
if (typeof config.ignoreTLS === 'boolean') {
142+
options.ignoreTLS = config.ignoreTLS;
143+
}
144+
145+
if (typeof config.requireTLS === 'boolean') {
146+
options.requireTLS = config.requireTLS;
147+
}
148+
137149
this.emailClient = nodemailer.createTransport(options);
138150
}
139151

@@ -261,9 +273,9 @@ export class EmailSender {
261273
attempt = 0
262274
): Promise<{
263275
sent: boolean;
264-
message?: string;
265276
messageId?: string;
266277
response?: string;
278+
message?: string;
267279
}> {
268280
const { maxAttempts, backOffMs } = this.config.retry;
269281
const isRetry = attempt > 0;
@@ -292,7 +304,7 @@ export class EmailSender {
292304
await this.emailClient.sendMail(sendMailPayload);
293305

294306
this.log.debug('mailer.send', {
295-
status: info.message,
307+
status: info.response,
296308
id: info.messageId,
297309
to: email.to,
298310
isRetry,
@@ -307,13 +319,23 @@ export class EmailSender {
307319
retryAttempt: attempt,
308320
});
309321

310-
// Relay email payload and send status back to calling code.
311-
return {
322+
const result: {
323+
sent: boolean;
324+
messageId?: string;
325+
response?: string;
326+
message?: string;
327+
} = {
312328
sent: true,
313-
message: info?.message,
314329
messageId: info?.messageId,
315330
response: info?.response,
316331
};
332+
333+
if (info?.message) {
334+
result.message = info.message;
335+
}
336+
337+
// Relay email payload and send status back to calling code.
338+
return result;
317339
} catch (err) {
318340
// retry if configured
319341
if (!isFinalAttempt && backOffMs > 0) {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"@apollo/server": "^4.11.3",
4545
"@aws-sdk/client-config-service": "^3.879.0",
4646
"@aws-sdk/client-s3": "^3.878.0",
47-
"@aws-sdk/client-ses": "^3.876.0",
4847
"@aws-sdk/client-sns": "^3.876.0",
4948
"@aws-sdk/client-sqs": "^3.876.0",
5049
"@faker-js/faker": "^9.0.0",
@@ -124,6 +123,7 @@
124123
"node-fetch": "^2.6.7",
125124
"node-hkdf": "^0.0.2",
126125
"node-jose": "^2.2.0",
126+
"nodemailer": "^7.0.7",
127127
"nps": "^5.10.0",
128128
"objection": "^3.1.3",
129129
"os-browserify": "^0.3.0",
@@ -226,6 +226,7 @@
226226
"@types/module-alias": "^2",
227227
"@types/mysql": "^2",
228228
"@types/node": "^22.13.5",
229+
"@types/nodemailer": "^7.0.4",
229230
"@types/passport": "^1.0.6",
230231
"@types/passport-http-bearer": "^1.0.36",
231232
"@types/passport-jwt": "^4",

packages/fxa-auth-server/config/dev.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"host": "localhost",
1919
"port": 9999,
2020
"secure": false,
21+
"ignoreTLS": true,
2122
"redirectDomain": "localhost",
2223
"subscriptionTermsUrl": "https://www.mozilla.org/about/legal/terms/subscription-services",
2324
"subscriptionSettingsUrl": "http://localhost:3035/",

packages/fxa-auth-server/config/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,12 @@ const convictConf = convict({
429429
default: false,
430430
env: 'SMTP_SECURE',
431431
},
432+
ignoreTLS: {
433+
doc: 'Ignore STARTTLS even if the server advertises it (needed for local mail helper)',
434+
format: Boolean,
435+
default: false,
436+
env: 'SMTP_IGNORE_TLS',
437+
},
432438
user: {
433439
doc: 'SMTP username',
434440
format: String,

packages/fxa-auth-server/lib/senders/email.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
const emailUtils = require('../email/utils/helpers');
88
const moment = require('moment-timezone');
9-
const { SES } = require('@aws-sdk/client-ses');
109
const nodemailer = require('nodemailer');
1110
const safeUserAgent = require('fxa-shared/lib/user-agent').default;
1211
const url = require('url');
@@ -302,10 +301,9 @@ module.exports = function (log, config, bounces, statsd) {
302301
const validCardTypes = Object.keys(CARD_TYPE_TO_TEXT);
303302

304303
function Mailer(mailerConfig, sender) {
305-
let options = {
304+
const options = {
306305
host: mailerConfig.host,
307306
secure: mailerConfig.secure,
308-
ignoreTLS: !mailerConfig.secure,
309307
port: mailerConfig.port,
310308
pool: mailerConfig.pool,
311309
maxConnections: mailerConfig.maxConnections,
@@ -314,24 +312,22 @@ module.exports = function (log, config, bounces, statsd) {
314312
greetingTimeout: mailerConfig.greetingTimeout,
315313
socketTimeout: mailerConfig.socketTimeout,
316314
dnsTimeout: mailerConfig.dnsTimeout,
315+
sendingRate: mailerConfig.sendingRate,
317316
};
318317

319318
if (mailerConfig.user && mailerConfig.password) {
320319
options.auth = {
321320
user: mailerConfig.user,
322321
pass: mailerConfig.password,
323322
};
324-
} else {
325-
const ses = new SES({
326-
// The key apiVersion is no longer supported in v3, and can be removed.
327-
// @deprecated The client uses the "latest" apiVersion.
328-
apiVersion: '2010-12-01',
329-
});
330-
options = {
331-
SES: { ses },
332-
sendingRate: 5,
333-
maxConnections: 10,
334-
};
323+
}
324+
325+
if (typeof mailerConfig.ignoreTLS === 'boolean') {
326+
options.ignoreTLS = mailerConfig.ignoreTLS;
327+
}
328+
329+
if (typeof mailerConfig.requireTLS === 'boolean') {
330+
options.requireTLS = mailerConfig.requireTLS;
335331
}
336332

337333
this.accountSettingsUrl = mailerConfig.accountSettingsUrl;

packages/fxa-auth-server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
"mozlog": "^3.0.2",
106106
"mysql": "^2.18.1",
107107
"node-zendesk": "^2.2.0",
108-
"nodemailer": "^6.9.9",
108+
"nodemailer": "^7.0.7",
109109
"openapi-fetch": "^0.13.5",
110110
"otplib": "^11.0.1",
111111
"p-queue": "^8.1.0",
@@ -139,7 +139,7 @@
139139
"@types/nock": "^11.1.0",
140140
"@types/node": "^22.13.5",
141141
"@types/node-zendesk": "^2.0.2",
142-
"@types/nodemailer": "^6.4.2",
142+
"@types/nodemailer": "^7.0.4",
143143
"@types/request": "2.48.5",
144144
"@types/sass": "^1",
145145
"@types/uuid": "^10.0.0",

0 commit comments

Comments
 (0)