Skip to content

Commit a5ff41a

Browse files
authored
Merge pull request #20007 from mozilla/FXA-13029
fix(fxa-mailer): Extract email address when formatting CMS sender names
2 parents 5a76a54 + 8ba0cab commit a5ff41a

2 files changed

Lines changed: 268 additions & 1 deletion

File tree

packages/fxa-auth-server/lib/senders/fxa-mailer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1690,8 +1690,16 @@ export class FxaMailer extends FxaEmailRenderer {
16901690
throwErrorOnSendFailure = true
16911691
) {
16921692
const { cmsRpFromName, to, cc } = opts;
1693+
// Extract just the email address from the sender config (e.g., "Name <[email protected]>" -> "[email protected]")
1694+
const senderEmail =
1695+
this.mailerConfig.sender.indexOf('<') >= 0
1696+
? this.mailerConfig.sender.substring(
1697+
this.mailerConfig.sender.indexOf('<') + 1,
1698+
this.mailerConfig.sender.indexOf('>')
1699+
)
1700+
: this.mailerConfig.sender;
16931701
const from = cmsRpFromName
1694-
? `${cmsRpFromName} <${this.mailerConfig.sender}>`
1702+
? `${cmsRpFromName} <${senderEmail}>`
16951703
: this.mailerConfig.sender;
16961704

16971705
return this.emailSender.send(
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
const ROOT_DIR = '../../..';
6+
7+
import { assert } from 'chai';
8+
import sinon from 'sinon';
9+
import { FxaMailer } from '../../../lib/senders/fxa-mailer';
10+
import { EmailSender } from '@fxa/accounts/email-sender';
11+
import {
12+
EmailLinkBuilder,
13+
NodeRendererBindings,
14+
} from '@fxa/accounts/email-renderer';
15+
16+
describe('lib/senders/fxa-mailer', () => {
17+
let fxaMailer: FxaMailer;
18+
let mockEmailSender: sinon.SinonStubbedInstance<EmailSender>;
19+
let mockLinkBuilder: sinon.SinonStubbedInstance<EmailLinkBuilder>;
20+
let mockBindings: NodeRendererBindings;
21+
let mockConfig: any;
22+
23+
beforeEach(() => {
24+
mockEmailSender = {
25+
send: sinon.stub().resolves({
26+
sent: true,
27+
messageId: 'test-message-id',
28+
message: 'Email sent',
29+
response: '250 OK',
30+
}),
31+
buildHeaders: sinon.stub().returns({}),
32+
} as any;
33+
34+
mockLinkBuilder = {
35+
buildPrivacyLink: sinon.stub().returns('https://privacy.link'),
36+
buildSupportLink: sinon.stub().returns('https://support.link'),
37+
buildPasswordChangeLink: sinon.stub().returns('https://password.link'),
38+
buildAccountSettingsLink: sinon.stub().returns('https://settings.link'),
39+
buildMozillaSupportUrl: sinon.stub().returns('https://mozilla.support'),
40+
} as any;
41+
42+
mockBindings = {} as any;
43+
44+
mockConfig = {
45+
sender: 'Firefox Accounts <[email protected]>',
46+
fxaMailerDisableSend: [],
47+
};
48+
49+
fxaMailer = new FxaMailer(
50+
mockEmailSender as any,
51+
mockLinkBuilder as any,
52+
mockConfig,
53+
mockBindings
54+
);
55+
});
56+
57+
describe('sendNewDeviceLoginEmail', () => {
58+
describe('with cmsRpFromName', () => {
59+
it('should extract email address from sender config', async () => {
60+
const opts = {
61+
62+
uid: 'test-uid',
63+
metricsEnabled: true,
64+
acceptLanguage: 'en',
65+
timeZone: 'America/New_York',
66+
cmsRpFromName: 'Mozilla AI',
67+
clientName: 'Test Client',
68+
device: 'Firefox on Mac',
69+
time: '10:00 AM',
70+
date: 'January 1, 2026',
71+
location: { city: 'San Francisco', stateCode: 'CA', country: 'USA' },
72+
showBannerWarning: false,
73+
};
74+
75+
// Mock the rendering method
76+
sinon.stub(fxaMailer as any, 'renderNewDeviceLogin').resolves({
77+
subject: 'New sign-in to Test Client',
78+
html: '<html>test</html>',
79+
text: 'test',
80+
preview: 'test preview',
81+
});
82+
83+
await fxaMailer.sendNewDeviceLoginEmail(opts);
84+
85+
assert.isTrue(mockEmailSender.send.calledOnce);
86+
const sendArgs = mockEmailSender.send.getCall(0).args[0];
87+
88+
// Should be "Mozilla AI <[email protected]>" not "Mozilla AI <Firefox Accounts <[email protected]>>"
89+
assert.equal(sendArgs.from, 'Mozilla AI <[email protected]>');
90+
assert.equal(sendArgs.to, '[email protected]');
91+
});
92+
93+
it('should work with Firefox relying party name', async () => {
94+
const opts = {
95+
96+
uid: 'test-uid',
97+
metricsEnabled: true,
98+
acceptLanguage: 'en',
99+
timeZone: 'America/New_York',
100+
cmsRpFromName: 'Firefox',
101+
clientName: 'Test Client',
102+
device: 'Firefox on Mac',
103+
time: '10:00 AM',
104+
date: 'January 1, 2026',
105+
location: { city: 'San Francisco', stateCode: 'CA', country: 'USA' },
106+
showBannerWarning: false,
107+
};
108+
109+
sinon.stub(fxaMailer as any, 'renderNewDeviceLogin').resolves({
110+
subject: 'New sign-in to Test Client',
111+
html: '<html>test</html>',
112+
text: 'test',
113+
preview: 'test preview',
114+
});
115+
116+
await fxaMailer.sendNewDeviceLoginEmail(opts);
117+
118+
assert.isTrue(mockEmailSender.send.calledOnce);
119+
const sendArgs = mockEmailSender.send.getCall(0).args[0];
120+
assert.equal(sendArgs.from, 'Firefox <[email protected]>');
121+
});
122+
123+
it('should work with Mozilla VPN relying party name', async () => {
124+
const opts = {
125+
126+
uid: 'test-uid',
127+
metricsEnabled: true,
128+
acceptLanguage: 'en',
129+
timeZone: 'America/New_York',
130+
cmsRpFromName: 'Mozilla VPN',
131+
clientName: 'Test Client',
132+
device: 'Firefox on Mac',
133+
time: '10:00 AM',
134+
date: 'January 1, 2026',
135+
location: { city: 'San Francisco', stateCode: 'CA', country: 'USA' },
136+
showBannerWarning: false,
137+
};
138+
139+
sinon.stub(fxaMailer as any, 'renderNewDeviceLogin').resolves({
140+
subject: 'New sign-in to Test Client',
141+
html: '<html>test</html>',
142+
text: 'test',
143+
preview: 'test preview',
144+
});
145+
146+
await fxaMailer.sendNewDeviceLoginEmail(opts);
147+
148+
assert.isTrue(mockEmailSender.send.calledOnce);
149+
const sendArgs = mockEmailSender.send.getCall(0).args[0];
150+
assert.equal(sendArgs.from, 'Mozilla VPN <[email protected]>');
151+
});
152+
});
153+
154+
describe('without cmsRpFromName', () => {
155+
it('should use full sender config', async () => {
156+
const opts = {
157+
158+
uid: 'test-uid',
159+
metricsEnabled: true,
160+
acceptLanguage: 'en',
161+
timeZone: 'America/New_York',
162+
clientName: 'Test Client',
163+
device: 'Firefox on Mac',
164+
time: '10:00 AM',
165+
date: 'January 1, 2026',
166+
location: { city: 'San Francisco', stateCode: 'CA', country: 'USA' },
167+
showBannerWarning: false,
168+
};
169+
170+
sinon.stub(fxaMailer as any, 'renderNewDeviceLogin').resolves({
171+
subject: 'New sign-in to Test Client',
172+
html: '<html>test</html>',
173+
text: 'test',
174+
preview: 'test preview',
175+
});
176+
177+
await fxaMailer.sendNewDeviceLoginEmail(opts);
178+
179+
assert.isTrue(mockEmailSender.send.calledOnce);
180+
const sendArgs = mockEmailSender.send.getCall(0).args[0];
181+
// Should use the full configured sender
182+
assert.equal(sendArgs.from, 'Firefox Accounts <[email protected]>');
183+
});
184+
});
185+
186+
describe('with sender config without angle brackets', () => {
187+
it('should handle plain email address sender', async () => {
188+
// Test when sender is just an email address without a display name
189+
mockConfig.sender = '[email protected]';
190+
fxaMailer = new FxaMailer(
191+
mockEmailSender as any,
192+
mockLinkBuilder as any,
193+
mockConfig,
194+
mockBindings
195+
);
196+
197+
const opts = {
198+
199+
uid: 'test-uid',
200+
metricsEnabled: true,
201+
acceptLanguage: 'en',
202+
timeZone: 'America/New_York',
203+
cmsRpFromName: 'Mozilla Monitor',
204+
clientName: 'Test Client',
205+
device: 'Firefox on Mac',
206+
time: '10:00 AM',
207+
date: 'January 1, 2026',
208+
location: { city: 'San Francisco', stateCode: 'CA', country: 'USA' },
209+
showBannerWarning: false,
210+
};
211+
212+
sinon.stub(fxaMailer as any, 'renderNewDeviceLogin').resolves({
213+
subject: 'New sign-in to Test Client',
214+
html: '<html>test</html>',
215+
text: 'test',
216+
preview: 'test preview',
217+
});
218+
219+
await fxaMailer.sendNewDeviceLoginEmail(opts);
220+
221+
assert.isTrue(mockEmailSender.send.calledOnce);
222+
const sendArgs = mockEmailSender.send.getCall(0).args[0];
223+
// Should be "Mozilla Monitor <[email protected]>"
224+
assert.equal(sendArgs.from, 'Mozilla Monitor <[email protected]>');
225+
});
226+
});
227+
});
228+
229+
describe('canSend', () => {
230+
it('should return true when template is not in disable list', () => {
231+
assert.isTrue(fxaMailer.canSend('newDeviceLogin'));
232+
});
233+
234+
it('should return false when template is in disable list', () => {
235+
mockConfig.fxaMailerDisableSend = ['newDeviceLogin'];
236+
fxaMailer = new FxaMailer(
237+
mockEmailSender as any,
238+
mockLinkBuilder as any,
239+
mockConfig,
240+
mockBindings
241+
);
242+
243+
assert.isFalse(fxaMailer.canSend('newDeviceLogin'));
244+
});
245+
246+
it('should return true for different template when one is disabled', () => {
247+
mockConfig.fxaMailerDisableSend = ['verifyLogin'];
248+
fxaMailer = new FxaMailer(
249+
mockEmailSender as any,
250+
mockLinkBuilder as any,
251+
mockConfig,
252+
mockBindings
253+
);
254+
255+
assert.isFalse(fxaMailer.canSend('verifyLogin'));
256+
assert.isTrue(fxaMailer.canSend('newDeviceLogin'));
257+
});
258+
});
259+
});

0 commit comments

Comments
 (0)