From 78dd0a86819e4ba72d657bac9875865b1057d380 Mon Sep 17 00:00:00 2001
From: Kristina <7614497+khjrtbrg@users.noreply.github.com>
Date: Wed, 27 May 2026 12:32:05 +0200
Subject: [PATCH] [web] update email base templates (#33791)
* update email template design
* gate email template on split test
GitOrigin-RevId: 2e0a1d9abf11a0c3f16e103511191d43d542b8a4
---
.../Email/Bodies/UpdatedNoCTAEmailBody.mjs | 28 +++
.../Email/Bodies/updated-cta-email.mjs | 65 +++++++
.../app/src/Features/Email/EmailBuilder.mjs | 32 +++-
.../app/src/Features/Email/EmailHandler.mjs | 25 ++-
.../Email/Layouts/UpdatedBaseEmailLayout.mjs | 169 ++++++++++++++++++
.../app/src/Features/Email/emailStyles.mjs | 12 ++
.../img/ol-brand/email-footer-tagline@2x.png | Bin 0 -> 10639 bytes
.../web/public/img/ol-brand/email-logo@2x.png | Bin 0 -> 5813 bytes
.../test/unit/src/Email/EmailBuilder.test.mjs | 102 ++++-------
.../test/unit/src/Email/EmailHandler.test.mjs | 21 +++
.../src/Email/UpdatedBaseEmailLayout.test.mjs | 75 ++++++++
11 files changed, 457 insertions(+), 72 deletions(-)
create mode 100644 services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs
create mode 100644 services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs
create mode 100644 services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs
create mode 100644 services/web/app/src/Features/Email/emailStyles.mjs
create mode 100644 services/web/public/img/ol-brand/email-footer-tagline@2x.png
create mode 100644 services/web/public/img/ol-brand/email-logo@2x.png
create mode 100644 services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs
diff --git a/services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs b/services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs
new file mode 100644
index 0000000000..6a314c2926
--- /dev/null
+++ b/services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs
@@ -0,0 +1,28 @@
+import _ from 'lodash'
+import { fontFamily, colors } from '../emailStyles.mjs'
+
+export default _.template(`\
+<% if (title) { %>
+
+ <%= title %>
+
+<% } %>
+
+<% if (greeting) { %>
+
+ <%= greeting %>
+
+<% } %>
+
+<% (message).forEach(function(paragraph) { %>
+
+ <%= paragraph %>
+
+<% }) %>
+
+<% if (highlightedText) { %>
+
+ <%= highlightedText %>
+
+<% } %>\
+`)
diff --git a/services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs b/services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs
new file mode 100644
index 0000000000..8987b62251
--- /dev/null
+++ b/services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs
@@ -0,0 +1,65 @@
+import _ from 'lodash'
+import { fontFamily, colors } from '../emailStyles.mjs'
+
+export default _.template(`\
+<% if (title) { %>
+
+ <%= title %>
+
+<% } %>
+
+<% if (greeting) { %>
+
+ <%= greeting %>
+
+<% } %>
+
+<% (message).forEach(function(paragraph) { %>
+
+ <%= paragraph %>
+
+<% }) %>
+
+
+
+<% if (secondaryMessage && secondaryMessage.length > 0) { %>
+ <% (secondaryMessage).forEach(function(paragraph) { %>
+
+ <%= paragraph %>
+
+ <% }) %>
+<% } %>
+
+
+ If the button above does not appear, please copy and paste this link into your browser's address bar:
+
+
+ <%= ctaURL %>
+
+
+<% if (gmailGoToAction) { %>
+
+<% } %>\
+`)
diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs
index 2635ebf52b..21a1314b08 100644
--- a/services/web/app/src/Features/Email/EmailBuilder.mjs
+++ b/services/web/app/src/Features/Email/EmailBuilder.mjs
@@ -4,9 +4,12 @@ import moment from 'moment'
import EmailMessageHelper from './EmailMessageHelper.mjs'
import StringHelper from '../Helpers/StringHelper.mjs'
import BaseEmailLayout from './Layouts/BaseEmailLayout.mjs'
+import UpdatedBaseEmailLayout from './Layouts/UpdatedBaseEmailLayout.mjs'
import SpamSafe from './SpamSafe.mjs'
import ctaEmailBody from './Bodies/cta-email.mjs'
+import updatedCtaEmailBody from './Bodies/updated-cta-email.mjs'
import NoCTAEmailBody from './Bodies/NoCTAEmailBody.mjs'
+import UpdatedNoCTAEmailBody from './Bodies/UpdatedNoCTAEmailBody.mjs'
function _emailBodyPlainText(content, opts, ctaEmail) {
let emailBody = `${content.greeting(opts, true)}`
@@ -75,7 +78,12 @@ function ctaTemplate(content) {
subject(opts) {
return content.subject(opts)
},
- layout: BaseEmailLayout,
+ layout(opts) {
+ const layoutFn = opts.useNewEmailDesign
+ ? UpdatedBaseEmailLayout
+ : BaseEmailLayout
+ return layoutFn(opts)
+ },
footerMessage(opts) {
return content.footerMessage(opts)
},
@@ -83,7 +91,8 @@ function ctaTemplate(content) {
return _emailBodyPlainText(content, opts, true)
},
compiledTemplate(opts) {
- return ctaEmailBody({
+ const bodyFn = opts.useNewEmailDesign ? updatedCtaEmailBody : ctaEmailBody
+ return bodyFn({
title: content.title(opts),
greeting: content.greeting(opts),
message: content.message(opts),
@@ -108,7 +117,12 @@ function NoCTAEmailTemplate(content) {
subject(opts) {
return content.subject(opts)
},
- layout: BaseEmailLayout,
+ layout(opts) {
+ const layoutFn = opts.useNewEmailDesign
+ ? UpdatedBaseEmailLayout
+ : BaseEmailLayout
+ return layoutFn(opts)
+ },
plainTextTemplate(opts) {
return `\
${content.greeting(opts)}
@@ -120,7 +134,10 @@ The ${settings.appName} Team - ${settings.siteUrl}\
`
},
compiledTemplate(opts) {
- return NoCTAEmailBody({
+ const bodyFn = opts.useNewEmailDesign
+ ? UpdatedNoCTAEmailBody
+ : NoCTAEmailBody
+ return bodyFn({
title:
typeof content.title === 'function' ? content.title(opts) : undefined,
greeting: content.greeting(opts),
@@ -137,7 +154,6 @@ The ${settings.appName} Team - ${settings.siteUrl}\
function buildEmail(templateName, opts) {
const template = templates[templateName]
- opts.siteUrl = settings.siteUrl
opts.body = template.compiledTemplate(opts)
opts.footerMessage = template.footerMessage
? template.footerMessage(opts)
@@ -189,7 +205,7 @@ templates.canceledSubscription = ctaTemplate({
return ['Thank you in advance!']
},
ctaText() {
- return 'Leave Feedback'
+ return 'Leave feedback'
},
ctaURL(opts) {
return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
@@ -394,7 +410,7 @@ templates.reconfirmEmail = ctaTemplate({
]
},
ctaText() {
- return 'Reconfirm Email'
+ return 'Reconfirm email'
},
ctaURL(opts) {
return opts.confirmEmailUrl
@@ -894,7 +910,7 @@ templates.SAMLDataCleared = ctaTemplate({
]
},
ctaText(opts) {
- return 'Update my Emails and affiliations'
+ return 'Update my emails and affiliations'
},
ctaURL(opts) {
return `${settings.siteUrl}/user/settings`
diff --git a/services/web/app/src/Features/Email/EmailHandler.mjs b/services/web/app/src/Features/Email/EmailHandler.mjs
index 85a94d5439..0c0b769688 100644
--- a/services/web/app/src/Features/Email/EmailHandler.mjs
+++ b/services/web/app/src/Features/Email/EmailHandler.mjs
@@ -4,14 +4,37 @@ import logger from '@overleaf/logger'
import EmailBuilder from './EmailBuilder.mjs'
import EmailSender from './EmailSender.mjs'
import Queues from '../../infrastructure/Queues.mjs'
+import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
+import UserGetter from '../User/UserGetter.mjs'
const EMAIL_SETTINGS = Settings.email || {}
/**
* @param {string} emailType
- * @param {opts} any
+ * @param {object} opts
*/
async function sendEmail(emailType, opts) {
+ if (opts.to) {
+ try {
+ const user = await UserGetter.promises.getUserByAnyEmail(opts.to, {
+ _id: 1,
+ })
+ if (user) {
+ const { variant } =
+ await SplitTestHandler.promises.getAssignmentForUser(
+ user._id.toString(),
+ 'new-email-design'
+ )
+ opts.useNewEmailDesign = variant === 'enabled'
+ }
+ } catch (err) {
+ logger.warn(
+ { err },
+ 'failed to get new-email-design split test assignment'
+ )
+ }
+ }
+
const email = EmailBuilder.buildEmail(emailType, opts)
if (email.type === 'lifecycle' && !EMAIL_SETTINGS.lifecycle) {
return
diff --git a/services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs b/services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs
new file mode 100644
index 0000000000..174bd6d8d1
--- /dev/null
+++ b/services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs
@@ -0,0 +1,169 @@
+import _ from 'lodash'
+import settings from '@overleaf/settings'
+import { fontFamily, colors } from '../emailStyles.mjs'
+
+const _isSaas = settings.env === 'saas'
+const _imageBase = settings.cdn?.web?.host || settings.siteUrl || null
+
+let _logoHtml = `${settings.appName}`
+let _taglineHtml = ''
+if (_isSaas && _imageBase) {
+ _logoHtml = `
+
+
+
+ `
+
+ _taglineHtml = `
+
+
+
+
+ `
+}
+
+const _customFooter = settings.email?.template?.customFooter || ''
+
+export default _.template(`\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+ | |
+
+
+
+
+ |
+ ${_taglineHtml}
+ |
+
+
+
+ ${
+ _customFooter
+ ? `
+ |
+ ${_customFooter}
+ |
+ `
+ : ''
+ }
+
+ <% if (footerMessage) { %>
+
+ |
+ <%= footerMessage %>
+ |
+
+ <% } %>
+
+
+
+
+ |
+
+
+
+
+\
+`)
diff --git a/services/web/app/src/Features/Email/emailStyles.mjs b/services/web/app/src/Features/Email/emailStyles.mjs
new file mode 100644
index 0000000000..5ad855e6c2
--- /dev/null
+++ b/services/web/app/src/Features/Email/emailStyles.mjs
@@ -0,0 +1,12 @@
+export const fontFamily = 'Arial, Helvetica, sans-serif'
+
+export const colors = {
+ textDark: '#1b222c',
+ textMuted: '#495365',
+ background: '#f4f5f6',
+ ctaGreen: '#098842',
+ ctaText: '#ffffff',
+ linkGreen: '#1e6b41',
+ linkHover: '#155a30',
+ logoGreen: '#04652f',
+}
diff --git a/services/web/public/img/ol-brand/email-footer-tagline@2x.png b/services/web/public/img/ol-brand/email-footer-tagline@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..de4d7a2b1ebad026dbdf0e45598dc4d9c10c1f20
GIT binary patch
literal 10639
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
literal 0
HcmV?d00001
diff --git a/services/web/public/img/ol-brand/email-logo@2x.png b/services/web/public/img/ol-brand/email-logo@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..900c7a113b57841709b99a4225e7150cb58c89ec
GIT binary patch
literal 5813
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 ({
+ default: ctx.SplitTestHandler,
+ })
+ )
+
+ ctx.UserGetter = {
+ promises: {
+ getUserByAnyEmail: sinon.stub().resolves(null),
+ },
+ }
+ vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
+ default: ctx.UserGetter,
+ }))
+
ctx.EmailHandler = (await import(MODULE_PATH)).default
})
diff --git a/services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs b/services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs
new file mode 100644
index 0000000000..cf2df55888
--- /dev/null
+++ b/services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs
@@ -0,0 +1,75 @@
+import { vi, expect } from 'vitest'
+import { load } from 'cheerio'
+import path from 'node:path'
+
+const LAYOUT_PATH = path.join(
+ import.meta.dirname,
+ '../../../../app/src/Features/Email/Layouts/UpdatedBaseEmailLayout'
+)
+
+async function buildLayoutWithEnv(env) {
+ vi.resetModules()
+ vi.doMock('@overleaf/settings', () => ({
+ default: {
+ appName: 'TestApp',
+ siteUrl: 'https://www.example.com',
+ env,
+ },
+ }))
+ const { default: layout } = await import(LAYOUT_PATH)
+ return layout({ body: 'Test body', footerMessage: '' })
+}
+
+describe('UpdatedBaseEmailLayout branding', function () {
+ afterEach(function () {
+ vi.restoreAllMocks()
+ vi.resetModules()
+ })
+
+ describe('on SaaS', function () {
+ it('should show the logo image', async function () {
+ const html = await buildLayoutWithEnv('saas')
+ const dom = load(html)
+ const logoImg = dom('img[alt="TestApp"]')
+ expect(logoImg.length).to.equal(1)
+ expect(logoImg.attr('src')).to.contain('email-logo@2x.png')
+ })
+
+ it('should show the tagline image', async function () {
+ const html = await buildLayoutWithEnv('saas')
+ expect(html).to.contain('email-footer-tagline@2x.png')
+ })
+ })
+
+ describe('on server-ce', function () {
+ it('should show the app name as text instead of logo', async function () {
+ const html = await buildLayoutWithEnv('server-ce')
+ const dom = load(html)
+ const logoImg = dom('img[alt="TestApp"]')
+ expect(logoImg.length).to.equal(0)
+ expect(html).to.contain('TestApp')
+ expect(html).to.not.contain('email-logo@2x.png')
+ })
+
+ it('should not show the tagline image', async function () {
+ const html = await buildLayoutWithEnv('server-ce')
+ expect(html).to.not.contain('email-footer-tagline@2x.png')
+ })
+ })
+
+ describe('on server-pro', function () {
+ it('should show the app name as text instead of logo', async function () {
+ const html = await buildLayoutWithEnv('server-pro')
+ const dom = load(html)
+ const logoImg = dom('img[alt="TestApp"]')
+ expect(logoImg.length).to.equal(0)
+ expect(html).to.contain('TestApp')
+ expect(html).to.not.contain('email-logo@2x.png')
+ })
+
+ it('should not show the tagline image', async function () {
+ const html = await buildLayoutWithEnv('server-pro')
+ expect(html).to.not.contain('email-footer-tagline@2x.png')
+ })
+ })
+})