EmailBuilder.mjs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. import _ from 'lodash'
  2. import settings from '@overleaf/settings'
  3. import moment from 'moment'
  4. import EmailMessageHelper from './EmailMessageHelper.mjs'
  5. import StringHelper from '../Helpers/StringHelper.mjs'
  6. import BaseEmailLayout from './Layouts/BaseEmailLayout.mjs'
  7. import UpdatedBaseEmailLayout from './Layouts/UpdatedBaseEmailLayout.mjs'
  8. import SpamSafe from './SpamSafe.mjs'
  9. import ctaEmailBody from './Bodies/cta-email.mjs'
  10. import updatedCtaEmailBody from './Bodies/updated-cta-email.mjs'
  11. import NoCTAEmailBody from './Bodies/NoCTAEmailBody.mjs'
  12. import UpdatedNoCTAEmailBody from './Bodies/UpdatedNoCTAEmailBody.mjs'
  13. function _emailBodyPlainText(content, opts, ctaEmail) {
  14. let emailBody = `${content.greeting(opts, true)}`
  15. emailBody += `\r\n\r\n`
  16. emailBody += `${content.message(opts, true).join('\r\n\r\n')}`
  17. if (ctaEmail) {
  18. emailBody += `\r\n\r\n`
  19. emailBody += `${content.ctaText(opts, true)}: ${content.ctaURL(opts, true)}`
  20. }
  21. if (
  22. content.secondaryMessage(opts, true) &&
  23. content.secondaryMessage(opts, true).length > 0
  24. ) {
  25. emailBody += `\r\n\r\n`
  26. emailBody += `${content.secondaryMessage(opts, true).join('\r\n\r\n')}`
  27. }
  28. emailBody += `\r\n\r\n`
  29. emailBody += `Regards,\r\nThe ${settings.appName} Team - ${settings.siteUrl}`
  30. if (
  31. settings.email &&
  32. settings.email.template &&
  33. settings.email.template.customFooter
  34. ) {
  35. emailBody += `\r\n\r\n`
  36. emailBody += settings.email.template.customFooter
  37. }
  38. const footerMessage = content.footerMessage(opts, true)
  39. if (footerMessage) {
  40. emailBody += `\r\n\r\n`
  41. emailBody += footerMessage
  42. }
  43. return emailBody
  44. }
  45. function ctaTemplate(content) {
  46. if (
  47. !content.ctaURL ||
  48. !content.ctaText ||
  49. !content.message ||
  50. !content.subject
  51. ) {
  52. throw new Error('missing required CTA email content')
  53. }
  54. if (!content.title) {
  55. content.title = () => {}
  56. }
  57. if (!content.greeting) {
  58. content.greeting = () => 'Hi,'
  59. }
  60. if (!content.secondaryMessage) {
  61. content.secondaryMessage = () => []
  62. }
  63. if (!content.gmailGoToAction) {
  64. content.gmailGoToAction = () => {}
  65. }
  66. if (!content.footerMessage) {
  67. content.footerMessage = () => {}
  68. }
  69. return {
  70. subject(opts) {
  71. return content.subject(opts)
  72. },
  73. layout(opts) {
  74. const layoutFn = opts.useNewEmailDesign
  75. ? UpdatedBaseEmailLayout
  76. : BaseEmailLayout
  77. return layoutFn(opts)
  78. },
  79. footerMessage(opts) {
  80. return content.footerMessage(opts)
  81. },
  82. plainTextTemplate(opts) {
  83. return _emailBodyPlainText(content, opts, true)
  84. },
  85. compiledTemplate(opts) {
  86. const bodyFn = opts.useNewEmailDesign ? updatedCtaEmailBody : ctaEmailBody
  87. return bodyFn({
  88. title: content.title(opts),
  89. greeting: content.greeting(opts),
  90. message: content.message(opts),
  91. secondaryMessage: content.secondaryMessage(opts),
  92. ctaText: content.ctaText(opts),
  93. ctaURL: content.ctaURL(opts),
  94. gmailGoToAction: content.gmailGoToAction(opts),
  95. StringHelper,
  96. })
  97. },
  98. }
  99. }
  100. function NoCTAEmailTemplate(content) {
  101. if (content.greeting == null) {
  102. content.greeting = () => 'Hi,'
  103. }
  104. if (!content.message) {
  105. throw new Error('missing message')
  106. }
  107. return {
  108. subject(opts) {
  109. return content.subject(opts)
  110. },
  111. layout(opts) {
  112. const layoutFn = opts.useNewEmailDesign
  113. ? UpdatedBaseEmailLayout
  114. : BaseEmailLayout
  115. return layoutFn(opts)
  116. },
  117. plainTextTemplate(opts) {
  118. return `\
  119. ${content.greeting(opts)}
  120. ${content.message(opts, true).join('\r\n\r\n')}
  121. Regards,
  122. The ${settings.appName} Team - ${settings.siteUrl}\
  123. `
  124. },
  125. compiledTemplate(opts) {
  126. const bodyFn = opts.useNewEmailDesign
  127. ? UpdatedNoCTAEmailBody
  128. : NoCTAEmailBody
  129. return bodyFn({
  130. title:
  131. typeof content.title === 'function' ? content.title(opts) : undefined,
  132. greeting: content.greeting(opts),
  133. highlightedText:
  134. typeof content.highlightedText === 'function'
  135. ? content.highlightedText(opts)
  136. : undefined,
  137. message: content.message(opts),
  138. StringHelper,
  139. })
  140. },
  141. }
  142. }
  143. function buildEmail(templateName, opts) {
  144. const template = templates[templateName]
  145. opts.body = template.compiledTemplate(opts)
  146. opts.footerMessage = template.footerMessage
  147. ? template.footerMessage(opts)
  148. : ''
  149. return {
  150. subject: template.subject(opts),
  151. html: template.layout(opts),
  152. text: template.plainTextTemplate && template.plainTextTemplate(opts),
  153. }
  154. }
  155. const templates = {}
  156. templates.registered = ctaTemplate({
  157. subject() {
  158. return `Activate your ${settings.appName} Account`
  159. },
  160. message(opts) {
  161. return [
  162. `Congratulations, you've just had an account created for you on ${
  163. settings.appName
  164. } with the email address '${_.escape(opts.to)}'.`,
  165. 'Click here to set your password and log in:',
  166. ]
  167. },
  168. secondaryMessage() {
  169. return [
  170. `If you have any questions or problems, please contact ${settings.adminEmail}`,
  171. ]
  172. },
  173. ctaText() {
  174. return 'Set password'
  175. },
  176. ctaURL(opts) {
  177. return opts.setNewPasswordUrl
  178. },
  179. })
  180. templates.canceledSubscription = ctaTemplate({
  181. subject() {
  182. return `${settings.appName} thoughts`
  183. },
  184. message() {
  185. return [
  186. `We are sorry to see you cancelled your ${settings.appName} premium subscription. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?`,
  187. ]
  188. },
  189. secondaryMessage() {
  190. return ['Thank you in advance!']
  191. },
  192. ctaText() {
  193. return 'Leave feedback'
  194. },
  195. ctaURL(opts) {
  196. return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
  197. },
  198. })
  199. templates.canceledSubscriptionOrAddOn = ctaTemplate({
  200. subject() {
  201. return `${settings.appName} thoughts`
  202. },
  203. message() {
  204. return [
  205. `We are sorry to see you cancelled your ${settings.appName} subscription. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?`,
  206. ]
  207. },
  208. secondaryMessage() {
  209. return ['Thank you in advance!']
  210. },
  211. ctaText() {
  212. return 'Leave feedback'
  213. },
  214. ctaURL(opts) {
  215. return 'https://digitalscience.qualtrics.com/jfe/form/SV_2n2aSlWgvoxXdGK'
  216. },
  217. })
  218. templates.reactivatedSubscription = ctaTemplate({
  219. subject() {
  220. return `Subscription Reactivated - ${settings.appName}`
  221. },
  222. message(opts) {
  223. return ['Your subscription was reactivated successfully.']
  224. },
  225. ctaText() {
  226. return 'View Subscription Dashboard'
  227. },
  228. ctaURL(opts) {
  229. return `${settings.siteUrl}/user/subscription`
  230. },
  231. })
  232. templates.passwordResetRequested = ctaTemplate({
  233. subject() {
  234. return `Password Reset - ${settings.appName}`
  235. },
  236. title() {
  237. return 'Password Reset'
  238. },
  239. message() {
  240. return [`We got a request to reset your ${settings.appName} password.`]
  241. },
  242. secondaryMessage() {
  243. return [
  244. "If you ignore this message, your password won't be changed.",
  245. "If you didn't request a password reset, let us know.",
  246. ]
  247. },
  248. ctaText() {
  249. return 'Reset password'
  250. },
  251. ctaURL(opts) {
  252. return opts.setNewPasswordUrl
  253. },
  254. })
  255. templates.confirmEmail = ctaTemplate({
  256. subject() {
  257. return `Confirm email - ${settings.appName}`
  258. },
  259. title() {
  260. return 'Confirm email'
  261. },
  262. message(opts) {
  263. return [
  264. `Please confirm that you have added a new email, ${opts.to}, to your ${settings.appName} account.`,
  265. ]
  266. },
  267. secondaryMessage() {
  268. return [
  269. `If you did not request this, please let us know at <a href="mailto:${settings.adminEmail}">${settings.adminEmail}</a>.`,
  270. `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`,
  271. ]
  272. },
  273. ctaText() {
  274. return 'Confirm email'
  275. },
  276. ctaURL(opts) {
  277. return opts.confirmEmailUrl
  278. },
  279. })
  280. templates.confirmCode = NoCTAEmailTemplate({
  281. greeting(opts) {
  282. return ''
  283. },
  284. subject(opts) {
  285. return `Confirm your email address on Overleaf (${opts.confirmCode})`
  286. },
  287. title(opts) {
  288. return 'Confirm your email address'
  289. },
  290. message(opts, isPlainText) {
  291. const msg = opts.welcomeUser
  292. ? [
  293. `Welcome to Overleaf! We're so glad you joined us.`,
  294. 'Use this 6-digit confirmation code to finish your setup.',
  295. ]
  296. : ['Use this 6-digit code to confirm your email address.']
  297. if (isPlainText && opts.confirmCode) {
  298. msg.push(opts.confirmCode)
  299. }
  300. return msg
  301. },
  302. highlightedText(opts) {
  303. return opts.confirmCode
  304. },
  305. })
  306. templates.projectInvite = ctaTemplate({
  307. subject(opts) {
  308. const safeName = SpamSafe.isSafeProjectName(opts.project.name)
  309. const safeEmail = SpamSafe.isSafeEmail(opts.owner.email)
  310. if (safeName && safeEmail) {
  311. return `"${opts.project.name}" — shared by ${_.escape(opts.owner.email)}`
  312. }
  313. if (safeName) {
  314. return `${settings.appName} project shared with you — "${_.escape(
  315. opts.project.name
  316. )}"`
  317. }
  318. if (safeEmail) {
  319. return `${_.escape(opts.owner.email)} shared an ${
  320. settings.appName
  321. } project with you`
  322. }
  323. return `An ${settings.appName} project has been shared with you`
  324. },
  325. title(opts) {
  326. return 'Project Invite'
  327. },
  328. greeting(opts) {
  329. return ''
  330. },
  331. message(opts, isPlainText) {
  332. // build message depending on spam-safe variables
  333. const message = [`You have been invited to an ${settings.appName} project.`]
  334. if (SpamSafe.isSafeProjectName(opts.project.name)) {
  335. message.push('<br/> Project:')
  336. message.push(`<b>${_.escape(opts.project.name)}</b>`)
  337. }
  338. if (SpamSafe.isSafeEmail(opts.owner.email)) {
  339. message.push(`<br/> Shared by:`)
  340. message.push(`<b>${_.escape(opts.owner.email)}</b>`)
  341. }
  342. if (message.length === 1) {
  343. message.push('<br/> Please view the project to find out more.')
  344. }
  345. return message.map(m => {
  346. return EmailMessageHelper.cleanHTML(m, isPlainText)
  347. })
  348. },
  349. ctaText() {
  350. return 'View project'
  351. },
  352. ctaURL(opts) {
  353. return opts.inviteUrl
  354. },
  355. gmailGoToAction(opts) {
  356. return {
  357. target: opts.inviteUrl,
  358. name: 'View project',
  359. description: `Join ${_.escape(
  360. SpamSafe.safeProjectName(opts.project.name, 'project')
  361. )} at ${settings.appName}`,
  362. }
  363. },
  364. })
  365. templates.reconfirmEmail = ctaTemplate({
  366. subject() {
  367. return `Reconfirm Email - ${settings.appName}`
  368. },
  369. title() {
  370. return 'Reconfirm Email'
  371. },
  372. message(opts) {
  373. return [
  374. `Please reconfirm your email address, ${opts.to}, on your ${settings.appName} account.`,
  375. ]
  376. },
  377. secondaryMessage() {
  378. return [
  379. 'If you did not request this, you can simply ignore this message.',
  380. `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`,
  381. ]
  382. },
  383. ctaText() {
  384. return 'Reconfirm email'
  385. },
  386. ctaURL(opts) {
  387. return opts.confirmEmailUrl
  388. },
  389. })
  390. templates.verifyEmailToJoinTeam = ctaTemplate({
  391. subject(opts) {
  392. return `${opts.reminder ? 'Reminder: ' : ''}${_.escape(
  393. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  394. )} has invited you to join a group subscription on ${settings.appName}`
  395. },
  396. title(opts) {
  397. return `${opts.reminder ? 'Reminder: ' : ''}${_.escape(
  398. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  399. )} has invited you to join a group subscription on ${settings.appName}`
  400. },
  401. message(opts) {
  402. return [
  403. `Please click the button below to join the group subscription and enjoy the benefits of an upgraded ${settings.appName} account.`,
  404. ]
  405. },
  406. ctaText(opts) {
  407. return 'Join now'
  408. },
  409. ctaURL(opts) {
  410. return opts.acceptInviteUrl
  411. },
  412. })
  413. templates.verifyEmailToJoinManagedUsers = ctaTemplate({
  414. subject(opts) {
  415. return `${
  416. opts.reminder ? 'Reminder: ' : ''
  417. }You’ve been invited by ${_.escape(
  418. _formatUserNameAndEmail(opts.inviter, 'a collaborator')
  419. )} to join an ${settings.appName} group subscription.`
  420. },
  421. title(opts) {
  422. return `${
  423. opts.reminder ? 'Reminder: ' : ''
  424. }You’ve been invited by ${_.escape(
  425. _formatUserNameAndEmail(opts.inviter, 'a collaborator')
  426. )} to join an ${settings.appName} group subscription.`
  427. },
  428. message(opts) {
  429. return [
  430. `By joining this group, you'll have access to ${settings.appName} premium features such as additional collaborators, greater maximum compile time, and real-time track changes.`,
  431. ]
  432. },
  433. secondaryMessage(opts, isPlainText) {
  434. const changeProjectOwnerLink = EmailMessageHelper.displayLink(
  435. 'change project owner',
  436. `${settings.siteUrl}/learn/how-to/How_to_Transfer_Project_Ownership`,
  437. isPlainText
  438. )
  439. return [
  440. `<b>User accounts in this group are managed by ${_.escape(
  441. _formatUserNameAndEmail(opts.admin, 'an admin')
  442. )}</b>`,
  443. `If you accept, you’ll transfer the management of your ${settings.appName} account to the owner of the group subscription, who will then have admin rights over your account and control over your stuff.`,
  444. `If you have personal projects in your ${settings.appName} account that you want to keep separate, that’s not a problem. You can set up another account under a personal email address and change the ownership of your personal projects to the new account. Find out how to ${changeProjectOwnerLink}.`,
  445. ]
  446. },
  447. ctaURL(opts) {
  448. return opts.acceptInviteUrl
  449. },
  450. ctaText(opts) {
  451. return 'Accept invitation'
  452. },
  453. greeting() {
  454. return ''
  455. },
  456. })
  457. templates.inviteNewUserToJoinManagedUsers = ctaTemplate({
  458. subject(opts) {
  459. return `${
  460. opts.reminder ? 'Reminder: ' : ''
  461. }You’ve been invited by ${_.escape(
  462. _formatUserNameAndEmail(opts.inviter, 'a collaborator')
  463. )} to join an ${settings.appName} group subscription.`
  464. },
  465. title(opts) {
  466. return `${
  467. opts.reminder ? 'Reminder: ' : ''
  468. }You’ve been invited by ${_.escape(
  469. _formatUserNameAndEmail(opts.inviter, 'a collaborator')
  470. )} to join an ${settings.appName} group subscription.`
  471. },
  472. message(opts) {
  473. return ['']
  474. },
  475. secondaryMessage(opts) {
  476. return [
  477. `<b>User accounts in this group are managed by ${_.escape(
  478. _formatUserNameAndEmail(opts.admin, 'an admin')
  479. )}.</b>`,
  480. `If you accept, the owner of the group subscription will have admin rights over your account and control over your stuff.`,
  481. `<b>What is ${settings.appName}?</b>`,
  482. `${settings.appName} is the collaborative online LaTeX editor loved by researchers and technical writers. With thousands of ready-to-use templates and an array of LaTeX learning resources you’ll be up and running in no time.`,
  483. ]
  484. },
  485. ctaURL(opts) {
  486. return opts.acceptInviteUrl
  487. },
  488. ctaText(opts) {
  489. return 'Accept invitation'
  490. },
  491. greeting() {
  492. return ''
  493. },
  494. })
  495. templates.groupSSOLinkingInvite = ctaTemplate({
  496. subject(opts) {
  497. const subjectPrefix = opts.reminder ? 'Reminder: ' : 'Action required: '
  498. return `${subjectPrefix}Authenticate your Overleaf account`
  499. },
  500. title(opts) {
  501. const titlePrefix = opts.reminder ? 'Reminder: ' : ''
  502. return `${titlePrefix}Single sign-on enabled`
  503. },
  504. message(opts) {
  505. return [
  506. `Hi,
  507. <div>
  508. Your group administrator has enabled single sign-on for your group.
  509. </div>
  510. </br>
  511. <div>
  512. <strong>What does this mean for you?</strong>
  513. </div>
  514. </br>
  515. <div>
  516. You won't need to remember a separate email address and password to sign in to Overleaf.
  517. All you need to do is authenticate your existing Overleaf account with your SSO provider.
  518. </div>
  519. `,
  520. ]
  521. },
  522. secondaryMessage(opts) {
  523. return [``]
  524. },
  525. ctaURL(opts) {
  526. return opts.authenticateWithSSO
  527. },
  528. ctaText(opts) {
  529. return 'Authenticate with SSO'
  530. },
  531. greeting() {
  532. return ''
  533. },
  534. })
  535. templates.groupSSOReauthenticate = ctaTemplate({
  536. subject(opts) {
  537. return 'Action required: Reauthenticate your Overleaf account'
  538. },
  539. title(opts) {
  540. return 'Action required: Reauthenticate SSO'
  541. },
  542. message(opts) {
  543. return [
  544. `Hi,
  545. <div>
  546. Single sign-on for your Overleaf group has been updated.
  547. This means you need to reauthenticate your Overleaf account with your group’s SSO provider.
  548. </div>
  549. `,
  550. ]
  551. },
  552. secondaryMessage(opts) {
  553. if (!opts.isManagedUser) {
  554. return ['']
  555. } else {
  556. const passwordResetUrl = `${settings.siteUrl}/user/password/reset`
  557. return [
  558. `If you’re not currently logged in to Overleaf, you'll need to <a href="${passwordResetUrl}">set a new password</a> to reauthenticate.`,
  559. ]
  560. }
  561. },
  562. ctaURL(opts) {
  563. return opts.authenticateWithSSO
  564. },
  565. ctaText(opts) {
  566. return 'Reauthenticate now'
  567. },
  568. greeting() {
  569. return ''
  570. },
  571. })
  572. templates.groupSSODisabled = ctaTemplate({
  573. subject(opts) {
  574. if (opts.userIsManaged) {
  575. return `Action required: Set your Overleaf password`
  576. } else {
  577. return 'A change to your Overleaf login options'
  578. }
  579. },
  580. title(opts) {
  581. return `Single sign-on disabled`
  582. },
  583. message(opts, isPlainText) {
  584. const loginUrl = `${settings.siteUrl}/login`
  585. let whatDoesThisMeanExplanation = [
  586. `You can still log in to Overleaf using one of our other <a href="${loginUrl}" style="color: #0F7A06; text-decoration: none;">login options</a> or with your email address and password.`,
  587. `If you don't have a password, you can set one now.`,
  588. ]
  589. if (opts.userIsManaged) {
  590. whatDoesThisMeanExplanation = [
  591. 'You now need an email address and password to sign in to your Overleaf account.',
  592. ]
  593. }
  594. const message = [
  595. 'Your group administrator has disabled single sign-on for your group.',
  596. '<br/>',
  597. '<b>What does this mean for you?</b>',
  598. ...whatDoesThisMeanExplanation,
  599. ]
  600. return message.map(m => {
  601. return EmailMessageHelper.cleanHTML(m, isPlainText)
  602. })
  603. },
  604. secondaryMessage(opts) {
  605. return [``]
  606. },
  607. ctaURL(opts) {
  608. return opts.setNewPasswordUrl
  609. },
  610. ctaText(opts) {
  611. return 'Set your new password'
  612. },
  613. })
  614. templates.surrenderAccountForManagedUsers = ctaTemplate({
  615. subject(opts) {
  616. const admin = _.escape(_formatUserNameAndEmail(opts.admin, 'an admin'))
  617. const toGroupName = opts.groupName ? ` to ${opts.groupName}` : ''
  618. return `${
  619. opts.reminder ? 'Reminder: ' : ''
  620. }You’ve been invited by ${admin} to transfer management of your ${
  621. settings.appName
  622. } account${toGroupName}`
  623. },
  624. title(opts) {
  625. const admin = _.escape(_formatUserNameAndEmail(opts.admin, 'an admin'))
  626. const toGroupName = opts.groupName ? ` to ${opts.groupName}` : ''
  627. return `${
  628. opts.reminder ? 'Reminder: ' : ''
  629. }You’ve been invited by ${admin} to transfer management of your ${
  630. settings.appName
  631. } account${toGroupName}`
  632. },
  633. message(opts, isPlainText) {
  634. const admin = _.escape(_formatUserNameAndEmail(opts.admin, 'an admin'))
  635. const managedUsersLink = EmailMessageHelper.displayLink(
  636. 'user account management',
  637. `${settings.siteUrl}/learn/how-to/Understanding_Managed_Overleaf_Accounts`,
  638. isPlainText
  639. )
  640. return [
  641. `Your ${settings.appName} account ${_.escape(
  642. opts.to
  643. )} is part of ${admin}'s group. They’ve now enabled ${managedUsersLink} for the group. This will ensure that projects aren’t lost when someone leaves the group.`,
  644. ]
  645. },
  646. secondaryMessage(opts, isPlainText) {
  647. const transferProjectOwnershipLink = EmailMessageHelper.displayLink(
  648. 'change project owner',
  649. `${settings.siteUrl}/learn/how-to/How_to_Transfer_Project_Ownership`,
  650. isPlainText
  651. )
  652. return [
  653. `<b>What does this mean for you?</b>`,
  654. `If you accept, you’ll transfer the management of your ${settings.appName} account to the owner of the group subscription, who will then have admin rights over your account and control over your stuff.`,
  655. `If you have personal projects in your ${settings.appName} account that you want to keep separate, that’s not a problem. You can set up another account under a personal email address and change the ownership of your personal projects to the new account. Find out how to ${transferProjectOwnershipLink}.`,
  656. `If you think this invitation has been sent in error please contact your group administrator.`,
  657. ]
  658. },
  659. ctaURL(opts) {
  660. return opts.acceptInviteUrl
  661. },
  662. ctaText(opts) {
  663. return 'Accept invitation'
  664. },
  665. greeting() {
  666. return ''
  667. },
  668. })
  669. templates.testEmail = ctaTemplate({
  670. subject() {
  671. return `A Test Email from ${settings.appName}`
  672. },
  673. title() {
  674. return `A Test Email from ${settings.appName}`
  675. },
  676. greeting() {
  677. return 'Hi,'
  678. },
  679. message() {
  680. return [`This is a test Email from ${settings.appName}`]
  681. },
  682. ctaText() {
  683. return `Open ${settings.appName}`
  684. },
  685. ctaURL() {
  686. return settings.siteUrl
  687. },
  688. })
  689. templates.ownershipTransferConfirmationPreviousOwner = NoCTAEmailTemplate({
  690. subject(opts) {
  691. return `Project ownership transfer - ${settings.appName}`
  692. },
  693. title(opts) {
  694. const projectName = _.escape(
  695. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  696. )
  697. return `${projectName} - Owner change`
  698. },
  699. message(opts, isPlainText) {
  700. const nameAndEmail = _.escape(
  701. _formatUserNameAndEmail(opts.newOwner, 'a collaborator')
  702. )
  703. const projectName = _.escape(
  704. SpamSafe.safeProjectName(opts.project.name, 'your project')
  705. )
  706. const projectNameDisplay = isPlainText
  707. ? projectName
  708. : `<b>${projectName}</b>`
  709. return [
  710. `As per your request, we have made ${nameAndEmail} the owner of ${projectNameDisplay}.`,
  711. `If you haven't asked to change the owner of ${projectNameDisplay}, please get in touch with us via ${settings.adminEmail}.`,
  712. ]
  713. },
  714. })
  715. templates.ownershipTransferConfirmationNewOwner = ctaTemplate({
  716. subject(opts) {
  717. return `Project ownership transfer - ${settings.appName}`
  718. },
  719. title(opts) {
  720. const projectName = _.escape(
  721. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  722. )
  723. return `${projectName} - Owner change`
  724. },
  725. message(opts, isPlainText) {
  726. const nameAndEmail = _.escape(
  727. _formatUserNameAndEmail(opts.previousOwner, 'A collaborator')
  728. )
  729. const projectName = _.escape(
  730. SpamSafe.safeProjectName(opts.project.name, 'a project')
  731. )
  732. const projectNameEmphasized = isPlainText
  733. ? projectName
  734. : `<b>${projectName}</b>`
  735. return [
  736. `${nameAndEmail} has made you the owner of ${projectNameEmphasized}. You can now manage ${projectName} sharing settings.`,
  737. ]
  738. },
  739. ctaText(opts) {
  740. return 'View project'
  741. },
  742. ctaURL(opts) {
  743. const projectUrl = `${
  744. settings.siteUrl
  745. }/project/${opts.project._id.toString()}`
  746. return projectUrl
  747. },
  748. })
  749. templates.userOnboardingEmail = NoCTAEmailTemplate({
  750. subject(opts) {
  751. return `Getting more out of ${settings.appName}`
  752. },
  753. greeting(opts) {
  754. return ''
  755. },
  756. title(opts) {
  757. return `Getting more out of ${settings.appName}`
  758. },
  759. message(opts, isPlainText) {
  760. const learnLatexLink = EmailMessageHelper.displayLink(
  761. 'Learn LaTeX in 30 minutes',
  762. `${settings.siteUrl}/learn/latex/Learn_LaTeX_in_30_minutes?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  763. isPlainText
  764. )
  765. const templatesLinks = EmailMessageHelper.displayLink(
  766. 'Find a beautiful template',
  767. `${settings.siteUrl}/latex/templates?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  768. isPlainText
  769. )
  770. const collaboratorsLink = EmailMessageHelper.displayLink(
  771. 'Work with your collaborators',
  772. `${settings.siteUrl}/learn/how-to/Sharing_a_project?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  773. isPlainText
  774. )
  775. const siteLink = EmailMessageHelper.displayLink(
  776. 'www.overleaf.com',
  777. settings.siteUrl,
  778. isPlainText
  779. )
  780. const userSettingsLink = EmailMessageHelper.displayLink(
  781. 'here',
  782. `${settings.siteUrl}/user/email-preferences`,
  783. isPlainText
  784. )
  785. const onboardingSurveyLink = EmailMessageHelper.displayLink(
  786. 'Join our user feedback program',
  787. 'https://forms.gle/DB7pdk2B1VFQqVVB9',
  788. isPlainText
  789. )
  790. return [
  791. `Thanks for signing up for ${settings.appName} recently. We hope you've been finding it useful! Here are some key features to help you get the most out of the service:`,
  792. `${learnLatexLink}: In this tutorial we provide a quick and easy first introduction to LaTeX with no prior knowledge required. By the time you are finished, you will have written your first LaTeX document!`,
  793. `${templatesLinks}: If you're looking for a template or example to get started, we've a large selection available in our template gallery, including CVs, project reports, journal articles and more.`,
  794. `${collaboratorsLink}: One of the key features of Overleaf is the ability to share projects and collaborate on them with other users. Find out how to share your projects with your colleagues in this quick how-to guide.`,
  795. `${onboardingSurveyLink} to help us make Overleaf even better!`,
  796. 'Thanks again for using Overleaf :)',
  797. `Lee`,
  798. `Lee Shalit<br />CEO<br />${siteLink}<hr>`,
  799. `You're receiving this email because you've recently signed up for an Overleaf account. If you've previously subscribed to emails about product offers and company news and events, you can unsubscribe ${userSettingsLink}.`,
  800. ]
  801. },
  802. })
  803. templates.securityAlert = NoCTAEmailTemplate({
  804. subject(opts) {
  805. return `Overleaf security note: ${opts.action}`
  806. },
  807. title(opts) {
  808. return opts.action.charAt(0).toUpperCase() + opts.action.slice(1)
  809. },
  810. message(opts, isPlainText) {
  811. const dateFormatted = moment().format('dddd D MMMM YYYY')
  812. const timeFormatted = moment().format('HH:mm')
  813. const helpLink = EmailMessageHelper.displayLink(
  814. 'quick guide',
  815. `${settings.siteUrl}/learn/how-to/Keeping_your_account_secure`,
  816. isPlainText
  817. )
  818. const actionDescribed = EmailMessageHelper.cleanHTML(
  819. opts.actionDescribed,
  820. isPlainText
  821. )
  822. if (!opts.message) {
  823. opts.message = []
  824. }
  825. const message = opts.message.map(m => {
  826. return EmailMessageHelper.cleanHTML(m, isPlainText)
  827. })
  828. return [
  829. `We are writing to let you know that ${actionDescribed} on ${dateFormatted} at ${timeFormatted} GMT.`,
  830. ...message,
  831. `If this was you, you can ignore this email.`,
  832. `If this was not you, we recommend getting in touch with our support team at ${settings.adminEmail} to report this as potentially suspicious activity on your account.`,
  833. `We also encourage you to read our ${helpLink} to keeping your ${settings.appName} account safe.`,
  834. ]
  835. },
  836. })
  837. const GIT_TOKEN_DOCS_URL =
  838. 'https://docs.overleaf.com/integrations-and-add-ons/git-integration-and-github-synchronization/git-integration/git-integration-authentication-tokens#how-to-generate-authentication-tokens'
  839. templates.gitTokenExpiringSoon = NoCTAEmailTemplate({
  840. subject() {
  841. return 'Your Overleaf token is about to expire'
  842. },
  843. title() {
  844. return 'Your token is about to expire'
  845. },
  846. greeting(opts) {
  847. return opts.firstName ? `Hi ${opts.firstName},` : 'Hi,'
  848. },
  849. message(opts, isPlainText) {
  850. const settingsLink = EmailMessageHelper.displayLink(
  851. 'account settings',
  852. `${settings.siteUrl}/user/settings`,
  853. isPlainText
  854. )
  855. const docsLink = EmailMessageHelper.displayLink(
  856. 'our docs',
  857. GIT_TOKEN_DOCS_URL,
  858. isPlainText
  859. )
  860. return [
  861. `One of your Git authentication tokens is about to expire. This means you won't be able to use your token to authenticate when performing git operations.`,
  862. `If you haven't already, you'll need to generate a new token in your ${settingsLink}.`,
  863. `Take a look at ${docsLink} if you need more help.`,
  864. 'All the best,',
  865. 'Team Overleaf',
  866. ]
  867. },
  868. })
  869. templates.gitTokenExpired = NoCTAEmailTemplate({
  870. subject() {
  871. return 'Your Overleaf token has expired'
  872. },
  873. title() {
  874. return 'Token expired'
  875. },
  876. greeting(opts) {
  877. return opts.firstName ? `Hi ${opts.firstName},` : 'Hi,'
  878. },
  879. message(opts, isPlainText) {
  880. const settingsLink = EmailMessageHelper.displayLink(
  881. 'account settings',
  882. `${settings.siteUrl}/user/settings`,
  883. isPlainText
  884. )
  885. const docsLink = EmailMessageHelper.displayLink(
  886. 'our docs',
  887. GIT_TOKEN_DOCS_URL,
  888. isPlainText
  889. )
  890. return [
  891. `One of your Git authentication tokens has expired. This means you won't be able to use it to authenticate when performing git operations.`,
  892. `If you haven't already, you'll need to generate a new token in your ${settingsLink}.`,
  893. `Take a look at ${docsLink} if you need more help.`,
  894. 'All the best,',
  895. 'Team Overleaf',
  896. ]
  897. },
  898. })
  899. templates.SAMLDataCleared = ctaTemplate({
  900. subject(opts) {
  901. return `Institutional Login No Longer Linked - ${settings.appName}`
  902. },
  903. title(opts) {
  904. return 'Institutional Login No Longer Linked'
  905. },
  906. message(opts, isPlainText) {
  907. return [
  908. `We're writing to let you know that due to a bug on our end, we've had to temporarily disable logging into your ${settings.appName} through your institution.`,
  909. `To get it going again, you'll need to relink your institutional email address to your ${settings.appName} account via your settings.`,
  910. ]
  911. },
  912. secondaryMessage() {
  913. return [
  914. `If you ordinarily log in to your ${settings.appName} account through your institution, you may need to set or reset your password to regain access to your account first.`,
  915. 'This bug did not affect the security of any accounts, but it may have affected license entitlements for a small number of users. We are sorry for any inconvenience that this may cause for you.',
  916. `If you have any questions, please get in touch with our support team at ${settings.adminEmail} or by replying to this email.`,
  917. ]
  918. },
  919. ctaText(opts) {
  920. return 'Update my emails and affiliations'
  921. },
  922. ctaURL(opts) {
  923. return `${settings.siteUrl}/user/settings`
  924. },
  925. })
  926. templates.welcome = ctaTemplate({
  927. subject() {
  928. return `Welcome to ${settings.appName}`
  929. },
  930. title() {
  931. return `Welcome to ${settings.appName}`
  932. },
  933. greeting() {
  934. return 'Hi,'
  935. },
  936. message(opts, isPlainText) {
  937. const logInAgainDisplay = EmailMessageHelper.displayLink(
  938. 'log in again',
  939. `${settings.siteUrl}/login`,
  940. isPlainText
  941. )
  942. const helpGuidesDisplay = EmailMessageHelper.displayLink(
  943. 'Help Guides',
  944. `${settings.siteUrl}/learn`,
  945. isPlainText
  946. )
  947. const templatesDisplay = EmailMessageHelper.displayLink(
  948. 'Templates',
  949. `${settings.siteUrl}/templates`,
  950. isPlainText
  951. )
  952. return [
  953. `Thanks for signing up to ${settings.appName}! If you ever get lost, you can ${logInAgainDisplay} with the email address '${opts.to}'.`,
  954. `If you're new to LaTeX, take a look at our ${helpGuidesDisplay} and ${templatesDisplay}.`,
  955. `Please also take a moment to confirm your email address for ${settings.appName}:`,
  956. ]
  957. },
  958. secondaryMessage() {
  959. return [
  960. `PS. We love talking to our users about ${settings.appName}. Reply to this email to get in touch with us directly, whatever the reason. Questions, comments, problems, suggestions, all welcome!`,
  961. ]
  962. },
  963. ctaText() {
  964. return 'Confirm email'
  965. },
  966. ctaURL(opts) {
  967. return opts.confirmEmailUrl
  968. },
  969. })
  970. templates.welcomeWithoutCTA = NoCTAEmailTemplate({
  971. subject() {
  972. return `Welcome to ${settings.appName}`
  973. },
  974. title() {
  975. return `Welcome to ${settings.appName}`
  976. },
  977. greeting() {
  978. return 'Hi,'
  979. },
  980. message(opts, isPlainText) {
  981. const logInAgainDisplay = EmailMessageHelper.displayLink(
  982. 'log in again',
  983. `${settings.siteUrl}/login`,
  984. isPlainText
  985. )
  986. const helpGuidesDisplay = EmailMessageHelper.displayLink(
  987. 'Help Guides',
  988. `${settings.siteUrl}/learn`,
  989. isPlainText
  990. )
  991. const templatesDisplay = EmailMessageHelper.displayLink(
  992. 'Templates',
  993. `${settings.siteUrl}/templates`,
  994. isPlainText
  995. )
  996. return [
  997. `Thanks for signing up to ${settings.appName}! If you ever get lost, you can ${logInAgainDisplay} with the email address '${opts.to}'.`,
  998. `If you're new to LaTeX, take a look at our ${helpGuidesDisplay} and ${templatesDisplay}.`,
  999. `PS. We love talking to our users about ${settings.appName}. Reply to this email to get in touch with us directly, whatever the reason. Questions, comments, problems, suggestions, all welcome!`,
  1000. ]
  1001. },
  1002. })
  1003. templates.removeGroupMember = NoCTAEmailTemplate({
  1004. subject(opts) {
  1005. return `Your ${settings.appName} account has been removed from ${opts.adminName}’s group`
  1006. },
  1007. title(opts) {
  1008. return `Your ${settings.appName} account has been removed from ${opts.adminName}’s group`
  1009. },
  1010. greeting() {
  1011. return ''
  1012. },
  1013. message() {
  1014. const passwordResetUrl = `${settings.siteUrl}/user/password/reset`
  1015. return [
  1016. 'Don’t worry, your account and projects are still accessible. But there are a few changes to be aware of:',
  1017. '<ul>' +
  1018. `<li>Your account will have reverted to a free ${settings.appName} plan.</li>`,
  1019. `<li>Any project collaborators have been set to read-only (you can invite one collaborator per project on the free plan).</li>`,
  1020. `<li>If you previously logged in via SSO, you’ll need to <a href="${passwordResetUrl}">set a password</a> to access your account.</li>` +
  1021. '</ul>',
  1022. `If you think this has been done in error, please contact your group admin.`,
  1023. `Thanks!`,
  1024. `Team ${settings.appName}`,
  1025. ]
  1026. },
  1027. })
  1028. templates.taxExemptCertificateRequired = NoCTAEmailTemplate({
  1029. subject(opts) {
  1030. return `Action required: Tax exemption verification for Overleaf [${opts.ein}]`
  1031. },
  1032. title() {
  1033. return 'Action required: Tax exemption verification'
  1034. },
  1035. greeting() {
  1036. return ''
  1037. },
  1038. message(opts) {
  1039. return [
  1040. 'Thanks for letting us know your organization is tax exempt. To confirm this, we need some additional verification.',
  1041. 'Please reply to this email with one of the following documents attached:',
  1042. '<ul>',
  1043. '<li>Your IRS determination letter (for non-profits and similar organizations)</li>',
  1044. '<li>Your state resale or exemption certificate</li>',
  1045. '</ul>',
  1046. `These should match the EIN you provided: ${opts.ein}.`,
  1047. 'If you have any questions, let us know by replying to this email.',
  1048. '<br/>',
  1049. 'Best wishes,',
  1050. 'Team Overleaf',
  1051. '<br/>',
  1052. `Our reference: ${opts.stripeCustomerId}`,
  1053. ]
  1054. },
  1055. })
  1056. templates.groupMemberLimitWarning = ctaTemplate({
  1057. subject(opts) {
  1058. return `Action needed: Your Overleaf group is nearly out of licenses`
  1059. },
  1060. title(opts) {
  1061. return `Action needed: Your Overleaf group is nearly out of licenses`
  1062. },
  1063. greeting(opts) {
  1064. return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,'
  1065. },
  1066. message(opts) {
  1067. return [
  1068. `Your Overleaf group <b>${opts.groupName}</b> is close to its license limit.`,
  1069. `<b>${opts.currentMembers} of ${opts.membersLimit} licenses are in use (${opts.remainingSeats} remaining).</b>`,
  1070. 'Because domain capture is enabled, users from your domain can join automatically via SSO.' +
  1071. '<br/>' +
  1072. '<b>Once all licenses are used, new users won’t be able to join.</b>',
  1073. '<b>What you can do now:</b>',
  1074. '<ul>' +
  1075. '<li>Add more licenses, or</li>' +
  1076. '<li>Remove inactive users to free up licenses</li>' +
  1077. '</ul>',
  1078. ]
  1079. },
  1080. ctaText() {
  1081. return 'Add licenses'
  1082. },
  1083. ctaURL() {
  1084. return `${settings.siteUrl}/user/subscription/group/add-users`
  1085. },
  1086. })
  1087. function _formatUserNameAndEmail(user, placeholder) {
  1088. if (user.first_name && user.last_name) {
  1089. const fullName = `${user.first_name} ${user.last_name}`
  1090. if (SpamSafe.isSafeUserName(fullName)) {
  1091. if (SpamSafe.isSafeEmail(user.email)) {
  1092. return `${fullName} (${user.email})`
  1093. } else {
  1094. return fullName
  1095. }
  1096. }
  1097. }
  1098. return SpamSafe.safeEmail(user.email, placeholder)
  1099. }
  1100. export default {
  1101. templates,
  1102. ctaTemplate,
  1103. NoCTAEmailTemplate,
  1104. buildEmail,
  1105. }