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')
+ })
+})