Skip to content

Commit 2983fe3

Browse files
committed
chore(): upgrade to nodemailer 7
1 parent 04725ea commit 2983fe3

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
@@ -147,7 +146,7 @@ describe('EmailSender', () => {
147146
expect(mockTransport.sendMail).not.toHaveBeenCalled();
148147
});
149148

150-
it('falls back to ses when no username/password provided', async () => {
149+
it('uses SMTP without auth when no username/password provided', async () => {
151150
config = {
152151
...config,
153152
user: undefined,
@@ -161,14 +160,18 @@ describe('EmailSender', () => {
161160
mockLogger
162161
);
163162

164-
// Verify constructor called createTransport with SES configuration
165-
expect(mockNodemailer.createTransport).toHaveBeenCalledWith(
163+
expect(mockNodemailer.createTransport).toHaveBeenLastCalledWith(
166164
expect.objectContaining({
167-
SES: expect.objectContaining({
168-
ses: expect.any(Object),
169-
}),
170-
sendingRate: 5,
171-
maxConnections: 10,
165+
host: config.host,
166+
port: config.port,
167+
secure: config.secure,
168+
sendingRate: config.sendingRate,
169+
})
170+
);
171+
expect(mockNodemailer.createTransport).toHaveBeenLastCalledWith(
172+
expect.not.objectContaining({
173+
SES: expect.anything(),
174+
auth: expect.anything(),
172175
})
173176
);
174177

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
};
@@ -71,6 +74,26 @@ export type Email = {
7174
headers: Record<string, string>;
7275
};
7376

77+
type SmtpTransportOptions = nodemailer.TransportOptions & {
78+
host: string;
79+
port: number;
80+
secure: boolean;
81+
pool: boolean;
82+
maxConnections: number;
83+
maxMessages: number;
84+
connectionTimeout: number;
85+
greetingTimeout: number;
86+
socketTimeout: number;
87+
dnsTimeout: number;
88+
sendingRate: number;
89+
ignoreTLS?: boolean;
90+
requireTLS?: boolean;
91+
auth?: {
92+
user: string;
93+
pass: string;
94+
};
95+
};
96+
7497
/**
7598
* Sends an email to end end user.
7699
*/
@@ -83,36 +106,10 @@ export class EmailSender {
83106
private readonly statsd: StatsD,
84107
private readonly log: ILogger
85108
) {
86-
// Determine auth credentials
87-
const auth = (() => {
88-
// If the user name and password are set use this
89-
if (config.user && config.password) {
90-
return {
91-
auth: {
92-
user: config.user,
93-
pass: config.password,
94-
},
95-
};
96-
}
97-
98-
// Otherwise fallback to the SES configuration
99-
const ses = new SES({
100-
// The key apiVersion is no longer supported in v3, and can be removed.
101-
// @deprecated The client uses the "latest" apiVersion.
102-
apiVersion: '2010-12-01',
103-
});
104-
return {
105-
SES: { ses },
106-
sendingRate: 5,
107-
maxConnections: 10,
108-
};
109-
})();
110-
111-
// Build node mailer options
112-
const options = {
109+
// Build SMTP-only nodemailer options
110+
const options: SmtpTransportOptions = {
113111
host: config.host,
114112
secure: config.secure,
115-
ignoreTLS: !config.secure,
116113
port: config.port,
117114
pool: config.pool,
118115
maxConnections: config.maxConnections,
@@ -121,9 +118,24 @@ export class EmailSender {
121118
greetingTimeout: config.greetingTimeout,
122119
socketTimeout: config.socketTimeout,
123120
dnsTimeout: config.dnsTimeout,
124-
sendingRate: this.config.sendingRate,
125-
...auth,
121+
sendingRate: config.sendingRate,
126122
};
123+
124+
if (config.user && config.password) {
125+
options.auth = {
126+
user: config.user,
127+
pass: config.password,
128+
};
129+
}
130+
131+
if (typeof config.ignoreTLS === 'boolean') {
132+
options.ignoreTLS = config.ignoreTLS;
133+
}
134+
135+
if (typeof config.requireTLS === 'boolean') {
136+
options.requireTLS = config.requireTLS;
137+
}
138+
127139
this.emailClient = nodemailer.createTransport(options);
128140
}
129141

@@ -234,9 +246,9 @@ export class EmailSender {
234246

235247
private async sendMail(email: Email): Promise<{
236248
sent: boolean;
237-
message?: string;
238249
messageId?: string;
239250
response?: string;
251+
message?: string;
240252
}> {
241253
try {
242254
// Make sure X-Mailer: '' is set in headers. This used to be done by setting
@@ -257,7 +269,7 @@ export class EmailSender {
257269
// xMailer: false,
258270
});
259271
this.log.debug('mailer.send', {
260-
status: info.message,
272+
status: info.response,
261273
id: info.messageId,
262274
to: email.to,
263275
});
@@ -268,13 +280,23 @@ export class EmailSender {
268280
headers: Object.keys(email.headers).join(','),
269281
});
270282

271-
// Relay email payload and send status back to calling code.
272-
return {
283+
const result: {
284+
sent: boolean;
285+
messageId?: string;
286+
response?: string;
287+
message?: string;
288+
} = {
273289
sent: true,
274-
message: info?.message,
275290
messageId: info?.messageId,
276291
response: info?.response,
277292
};
293+
294+
if (info?.message) {
295+
result.message = info.message;
296+
}
297+
298+
// Relay email payload and send status back to calling code.
299+
return result;
278300
} catch (err) {
279301
// Make sure error is logged & captured
280302
if (isAppError(err)) {

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)