diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs index 190560e746..9db0cbdd00 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.mjs +++ b/services/web/app/src/Features/Email/EmailBuilder.mjs @@ -662,6 +662,10 @@ templates.domainReverificationFailed = ctaTemplate({ } return `${domain} needs re-verifying` }, + greeting(opts, isPlainText) { + const greeting = opts.firstName ? `Hi ${opts.firstName},` : 'Hi,' + return EmailMessageHelper.cleanHTML(greeting, isPlainText) + }, message(opts) { const domain = _.escape(opts.domain) if (opts.capturedByGroup) { @@ -1307,6 +1311,10 @@ templates.groupDomainCapturedByGroupChanged = ctaTemplate({ ? `Domain capture is active for ${_.escape(opts.domain)}` : `Domain capture is inactive for ${_.escape(opts.domain)}` }, + greeting(opts, isPlainText) { + const greeting = opts.firstName ? `Hi ${opts.firstName},` : 'Hi,' + return EmailMessageHelper.cleanHTML(greeting, isPlainText) + }, message(opts) { if (opts.domainCapturedByGroup) { return [ @@ -1334,6 +1342,10 @@ templates.domainVerifiedForGroup = NoCTAEmailTemplate({ return 'Your domain is verified — ready to capture?' } }, + greeting(opts, isPlainText) { + const greeting = opts.firstName ? `Hi ${opts.firstName},` : 'Hi,' + return EmailMessageHelper.cleanHTML(greeting, isPlainText) + }, message(opts, isPlainText) { const message = [ `We've verified ${_.escape(opts.domain)} for your Overleaf group.`, diff --git a/services/web/app/src/Features/Errors/Errors.js b/services/web/app/src/Features/Errors/Errors.js index e968629185..9e4f167d74 100644 --- a/services/web/app/src/Features/Errors/Errors.js +++ b/services/web/app/src/Features/Errors/Errors.js @@ -84,6 +84,8 @@ class EmailExistsError extends OErrorV2CompatibleError { class InvalidError extends BackwardCompatibleError {} +class NotifyGroupAdminError extends OError {} + class NotInV2Error extends BackwardCompatibleError {} class SLInV2Error extends BackwardCompatibleError {} @@ -415,6 +417,7 @@ module.exports = { UnconfirmedEmailError, EmailExistsError, InvalidError, + NotifyGroupAdminError, NotInV2Error, OutputFileFetchFailedError, SAMLAssertionAudienceMismatch, diff --git a/services/web/test/unit/src/Email/EmailBuilder.test.mjs b/services/web/test/unit/src/Email/EmailBuilder.test.mjs index 3280ed1b10..050f01e870 100644 --- a/services/web/test/unit/src/Email/EmailBuilder.test.mjs +++ b/services/web/test/unit/src/Email/EmailBuilder.test.mjs @@ -1140,6 +1140,7 @@ describe('EmailBuilder', function () { expect(email.html).to.contain( 'Domain capture is active for example.com' ) + expect(email.text).to.contain('Hi,') expect(email.html).to.contain('/manage/groups/group-123/settings') expect(email.text).to.contain('/manage/groups/group-123/settings') }) @@ -1164,6 +1165,22 @@ describe('EmailBuilder', function () { expect(email.html).to.contain('admin@overleaf.test') expect(email.text).to.contain('admin@overleaf.test') }) + + it('should use firstName in greeting when provided', function (ctx) { + const email = ctx.EmailBuilder.buildEmail( + 'groupDomainCapturedByGroupChanged', + { + to: 'admin@example.com', + groupId: 'group-123', + domainCapturedByGroup: true, + domain: 'example.com', + firstName: 'Ada', + } + ) + + expect(email.text).to.contain('Hi Ada,') + expect(email.html).to.contain('Hi Ada,') + }) }) describe('domainVerifiedForGroup', function () { @@ -1175,6 +1192,7 @@ describe('EmailBuilder', function () { }) expect(email.subject).to.equal('Your domain is verified') + expect(email.text).to.contain('Hi,') expect(email.html).to.contain("We've verified") expect(email.html).to.contain('example.com') expect(email.html).to.contain( @@ -1199,6 +1217,18 @@ describe('EmailBuilder', function () { "You'll receive a confirmation email once the capture is active." ) }) + + it('should use firstName in greeting when provided', function (ctx) { + const email = ctx.EmailBuilder.buildEmail('domainVerifiedForGroup', { + to: 'admin@example.com', + domain: 'example.com', + capturedByGroup: true, + firstName: 'Ada', + }) + + expect(email.text).to.contain('Hi Ada,') + expect(email.html).to.contain('Hi Ada,') + }) }) describe('domainReverificationFailed', function () { @@ -1244,6 +1274,23 @@ describe('EmailBuilder', function () { expect(ctx.email.html).to.contain(ctx.opts.domainSettingsUrl) expect(ctx.email.text).to.contain(ctx.opts.domainSettingsUrl) }) + + it('uses the default greeting when firstName is not provided', function (ctx) { + expect(ctx.email.text).to.contain('Hi,') + }) + + it('uses firstName in the greeting when provided', function (ctx) { + const email = ctx.EmailBuilder.buildEmail( + 'domainReverificationFailed', + { + ...ctx.opts, + firstName: 'Ada', + } + ) + + expect(email.text).to.contain('Hi Ada,') + expect(email.html).to.contain('Hi Ada,') + }) }) describe('when the domain is not captured by the group', function () { diff --git a/services/web/test/unit/src/Features/Errors.test.mjs b/services/web/test/unit/src/Features/Errors.test.mjs index 870d224a8b..7f0cacd874 100644 --- a/services/web/test/unit/src/Features/Errors.test.mjs +++ b/services/web/test/unit/src/Features/Errors.test.mjs @@ -43,3 +43,13 @@ describe('BackwardCompatibleError', function () { expect(error.cause).to.equal(cause) }) }) + +describe('NotifyGroupAdminError', function () { + it('is exported and can be constructed', function () { + const error = new Errors.NotifyGroupAdminError('notify admin failed') + + expect(error).to.be.instanceOf(Error) + expect(error).to.be.instanceOf(Errors.NotifyGroupAdminError) + expect(error.message).to.equal('notify admin failed') + }) +})