EmailBuilder.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. const _ = require('lodash')
  2. const settings = require('@overleaf/settings')
  3. const moment = require('moment')
  4. const EmailMessageHelper = require('./EmailMessageHelper')
  5. const StringHelper = require('../Helpers/StringHelper')
  6. const BaseWithHeaderEmailLayout = require('./Layouts/BaseWithHeaderEmailLayout')
  7. const SpamSafe = require('./SpamSafe')
  8. const ctaEmailBody = require('./Bodies/cta-email')
  9. const NoCTAEmailBody = require('./Bodies/NoCTAEmailBody')
  10. function _emailBodyPlainText(content, opts, ctaEmail) {
  11. let emailBody = `${content.greeting(opts, true)}`
  12. emailBody += `\r\n\r\n`
  13. emailBody += `${content.message(opts, true).join('\r\n\r\n')}`
  14. if (ctaEmail) {
  15. emailBody += `\r\n\r\n`
  16. emailBody += `${content.ctaText(opts, true)}: ${content.ctaURL(opts, true)}`
  17. }
  18. if (
  19. content.secondaryMessage(opts, true) &&
  20. content.secondaryMessage(opts, true).length > 0
  21. ) {
  22. emailBody += `\r\n\r\n`
  23. emailBody += `${content.secondaryMessage(opts, true).join('\r\n\r\n')}`
  24. }
  25. emailBody += `\r\n\r\n`
  26. emailBody += `Regards,\r\nThe ${settings.appName} Team - ${settings.siteUrl}`
  27. if (
  28. settings.email &&
  29. settings.email.template &&
  30. settings.email.template.customFooter
  31. ) {
  32. emailBody += `\r\n\r\n`
  33. emailBody += settings.email.template.customFooter
  34. }
  35. return emailBody
  36. }
  37. function ctaTemplate(content) {
  38. if (
  39. !content.ctaURL ||
  40. !content.ctaText ||
  41. !content.message ||
  42. !content.subject
  43. ) {
  44. throw new Error('missing required CTA email content')
  45. }
  46. if (!content.title) {
  47. content.title = () => {}
  48. }
  49. if (!content.greeting) {
  50. content.greeting = () => 'Hi,'
  51. }
  52. if (!content.secondaryMessage) {
  53. content.secondaryMessage = () => []
  54. }
  55. if (!content.gmailGoToAction) {
  56. content.gmailGoToAction = () => {}
  57. }
  58. return {
  59. subject(opts) {
  60. return content.subject(opts)
  61. },
  62. layout: BaseWithHeaderEmailLayout,
  63. plainTextTemplate(opts) {
  64. return _emailBodyPlainText(content, opts, true)
  65. },
  66. compiledTemplate(opts) {
  67. return ctaEmailBody({
  68. title: content.title(opts),
  69. greeting: content.greeting(opts),
  70. message: content.message(opts),
  71. secondaryMessage: content.secondaryMessage(opts),
  72. ctaText: content.ctaText(opts),
  73. ctaURL: content.ctaURL(opts),
  74. gmailGoToAction: content.gmailGoToAction(opts),
  75. StringHelper,
  76. })
  77. },
  78. }
  79. }
  80. function NoCTAEmailTemplate(content) {
  81. if (content.greeting == null) {
  82. content.greeting = () => 'Hi,'
  83. }
  84. if (!content.message) {
  85. throw new Error('missing message')
  86. }
  87. return {
  88. subject(opts) {
  89. return content.subject(opts)
  90. },
  91. layout: BaseWithHeaderEmailLayout,
  92. plainTextTemplate(opts) {
  93. return `\
  94. ${content.greeting(opts)}
  95. ${content.message(opts, true).join('\r\n\r\n')}
  96. Regards,
  97. The ${settings.appName} Team - ${settings.siteUrl}\
  98. `
  99. },
  100. compiledTemplate(opts) {
  101. return NoCTAEmailBody({
  102. title:
  103. typeof content.title === 'function' ? content.title(opts) : undefined,
  104. greeting: content.greeting(opts),
  105. message: content.message(opts),
  106. StringHelper,
  107. })
  108. },
  109. }
  110. }
  111. function buildEmail(templateName, opts) {
  112. const template = templates[templateName]
  113. opts.siteUrl = settings.siteUrl
  114. opts.body = template.compiledTemplate(opts)
  115. return {
  116. subject: template.subject(opts),
  117. html: template.layout(opts),
  118. text: template.plainTextTemplate && template.plainTextTemplate(opts),
  119. }
  120. }
  121. const templates = {}
  122. templates.registered = ctaTemplate({
  123. subject() {
  124. return `Activate your ${settings.appName} Account`
  125. },
  126. message(opts) {
  127. return [
  128. `Congratulations, you've just had an account created for you on ${
  129. settings.appName
  130. } with the email address '${_.escape(opts.to)}'.`,
  131. 'Click here to set your password and log in:',
  132. ]
  133. },
  134. secondaryMessage() {
  135. return [
  136. `If you have any questions or problems, please contact ${settings.adminEmail}`,
  137. ]
  138. },
  139. ctaText() {
  140. return 'Set password'
  141. },
  142. ctaURL(opts) {
  143. return opts.setNewPasswordUrl
  144. },
  145. })
  146. templates.canceledSubscription = ctaTemplate({
  147. subject() {
  148. return `${settings.appName} thoughts`
  149. },
  150. message() {
  151. return [
  152. `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?`,
  153. ]
  154. },
  155. secondaryMessage() {
  156. return ['Thank you in advance!']
  157. },
  158. ctaText() {
  159. return 'Leave Feedback'
  160. },
  161. ctaURL(opts) {
  162. return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
  163. },
  164. })
  165. templates.reactivatedSubscription = ctaTemplate({
  166. subject() {
  167. return `Subscription Reactivated - ${settings.appName}`
  168. },
  169. message(opts) {
  170. return ['Your subscription was reactivated successfully.']
  171. },
  172. ctaText() {
  173. return 'View Subscription Dashboard'
  174. },
  175. ctaURL(opts) {
  176. return `${settings.siteUrl}/user/subscription`
  177. },
  178. })
  179. templates.passwordResetRequested = ctaTemplate({
  180. subject() {
  181. return `Password Reset - ${settings.appName}`
  182. },
  183. title() {
  184. return 'Password Reset'
  185. },
  186. message() {
  187. return [`We got a request to reset your ${settings.appName} password.`]
  188. },
  189. secondaryMessage() {
  190. return [
  191. "If you ignore this message, your password won't be changed.",
  192. "If you didn't request a password reset, let us know.",
  193. ]
  194. },
  195. ctaText() {
  196. return 'Reset password'
  197. },
  198. ctaURL(opts) {
  199. return opts.setNewPasswordUrl
  200. },
  201. })
  202. templates.confirmEmail = ctaTemplate({
  203. subject() {
  204. return `Confirm Email - ${settings.appName}`
  205. },
  206. title() {
  207. return 'Confirm Email'
  208. },
  209. message(opts) {
  210. return [
  211. `Please confirm that you have added a new email, ${opts.to}, to your ${settings.appName} account.`,
  212. ]
  213. },
  214. secondaryMessage() {
  215. return [
  216. 'If you did not request this, please let us know at <a href="mailto:support@overleaf.com">support@overleaf.com</a>.',
  217. `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`,
  218. ]
  219. },
  220. ctaText() {
  221. return 'Confirm Email'
  222. },
  223. ctaURL(opts) {
  224. return opts.confirmEmailUrl
  225. },
  226. })
  227. templates.projectInvite = ctaTemplate({
  228. subject(opts) {
  229. const safeName = SpamSafe.isSafeProjectName(opts.project.name)
  230. const safeEmail = SpamSafe.isSafeEmail(opts.owner.email)
  231. if (safeName && safeEmail) {
  232. return `"${_.escape(opts.project.name)}" — shared by ${_.escape(
  233. opts.owner.email
  234. )}`
  235. }
  236. if (safeName) {
  237. return `${settings.appName} project shared with you — "${_.escape(
  238. opts.project.name
  239. )}"`
  240. }
  241. if (safeEmail) {
  242. return `${_.escape(opts.owner.email)} shared an ${
  243. settings.appName
  244. } project with you`
  245. }
  246. return `An ${settings.appName} project has been shared with you`
  247. },
  248. title(opts) {
  249. return 'Project Invite'
  250. },
  251. greeting(opts) {
  252. return ''
  253. },
  254. message(opts, isPlainText) {
  255. // build message depending on spam-safe variables
  256. const message = [`You have been invited to an ${settings.appName} project.`]
  257. if (SpamSafe.isSafeProjectName(opts.project.name)) {
  258. message.push('<br/> Project:')
  259. message.push(`<b>${_.escape(opts.project.name)}</b>`)
  260. }
  261. if (SpamSafe.isSafeEmail(opts.owner.email)) {
  262. message.push(`<br/> Shared by:`)
  263. message.push(`<b>${_.escape(opts.owner.email)}</b>`)
  264. }
  265. if (message.length === 1) {
  266. message.push('<br/> Please view the project to find out more.')
  267. }
  268. return message.map(m => {
  269. return EmailMessageHelper.cleanHTML(m, isPlainText)
  270. })
  271. },
  272. ctaText() {
  273. return 'View project'
  274. },
  275. ctaURL(opts) {
  276. return opts.inviteUrl
  277. },
  278. gmailGoToAction(opts) {
  279. return {
  280. target: opts.inviteUrl,
  281. name: 'View project',
  282. description: `Join ${_.escape(
  283. SpamSafe.safeProjectName(opts.project.name, 'project')
  284. )} at ${settings.appName}`,
  285. }
  286. },
  287. })
  288. templates.reconfirmEmail = ctaTemplate({
  289. subject() {
  290. return `Reconfirm Email - ${settings.appName}`
  291. },
  292. title() {
  293. return 'Reconfirm Email'
  294. },
  295. message(opts) {
  296. return [
  297. `Please reconfirm your email address, ${opts.to}, on your ${settings.appName} account.`,
  298. ]
  299. },
  300. secondaryMessage() {
  301. return [
  302. 'If you did not request this, you can simply ignore this message.',
  303. `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`,
  304. ]
  305. },
  306. ctaText() {
  307. return 'Reconfirm Email'
  308. },
  309. ctaURL(opts) {
  310. return opts.confirmEmailUrl
  311. },
  312. })
  313. templates.verifyEmailToJoinTeam = ctaTemplate({
  314. subject(opts) {
  315. return `${_.escape(
  316. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  317. )} has invited you to join a group subscription on ${settings.appName}`
  318. },
  319. title(opts) {
  320. return `${_.escape(
  321. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  322. )} has invited you to join a group subscription on ${settings.appName}`
  323. },
  324. message(opts) {
  325. return [
  326. `Please click the button below to join the group subscription and enjoy the benefits of an upgraded ${settings.appName} account.`,
  327. ]
  328. },
  329. ctaText(opts) {
  330. return 'Join now'
  331. },
  332. ctaURL(opts) {
  333. return opts.acceptInviteUrl
  334. },
  335. })
  336. templates.testEmail = ctaTemplate({
  337. subject() {
  338. return `A Test Email from ${settings.appName}`
  339. },
  340. title() {
  341. return `A Test Email from ${settings.appName}`
  342. },
  343. greeting() {
  344. return 'Hi,'
  345. },
  346. message() {
  347. return [`This is a test Email from ${settings.appName}`]
  348. },
  349. ctaText() {
  350. return `Open ${settings.appName}`
  351. },
  352. ctaURL() {
  353. return settings.siteUrl
  354. },
  355. })
  356. templates.ownershipTransferConfirmationPreviousOwner = NoCTAEmailTemplate({
  357. subject(opts) {
  358. return `Project ownership transfer - ${settings.appName}`
  359. },
  360. title(opts) {
  361. const projectName = _.escape(
  362. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  363. )
  364. return `${projectName} - Owner change`
  365. },
  366. message(opts, isPlainText) {
  367. const nameAndEmail = _.escape(
  368. _formatUserNameAndEmail(opts.newOwner, 'a collaborator')
  369. )
  370. const projectName = _.escape(
  371. SpamSafe.safeProjectName(opts.project.name, 'your project')
  372. )
  373. const projectNameDisplay = isPlainText
  374. ? projectName
  375. : `<b>${projectName}</b>`
  376. return [
  377. `As per your request, we have made ${nameAndEmail} the owner of ${projectNameDisplay}.`,
  378. `If you haven't asked to change the owner of ${projectNameDisplay}, please get in touch with us via ${settings.adminEmail}.`,
  379. ]
  380. },
  381. })
  382. templates.ownershipTransferConfirmationNewOwner = ctaTemplate({
  383. subject(opts) {
  384. return `Project ownership transfer - ${settings.appName}`
  385. },
  386. title(opts) {
  387. const projectName = _.escape(
  388. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  389. )
  390. return `${projectName} - Owner change`
  391. },
  392. message(opts, isPlainText) {
  393. const nameAndEmail = _.escape(
  394. _formatUserNameAndEmail(opts.previousOwner, 'A collaborator')
  395. )
  396. const projectName = _.escape(
  397. SpamSafe.safeProjectName(opts.project.name, 'a project')
  398. )
  399. const projectNameEmphasized = isPlainText
  400. ? projectName
  401. : `<b>${projectName}</b>`
  402. return [
  403. `${nameAndEmail} has made you the owner of ${projectNameEmphasized}. You can now manage ${projectName} sharing settings.`,
  404. ]
  405. },
  406. ctaText(opts) {
  407. return 'View project'
  408. },
  409. ctaURL(opts) {
  410. const projectUrl = `${
  411. settings.siteUrl
  412. }/project/${opts.project._id.toString()}`
  413. return projectUrl
  414. },
  415. })
  416. templates.userOnboardingEmail = NoCTAEmailTemplate({
  417. subject(opts) {
  418. return `Getting more out of ${settings.appName}`
  419. },
  420. greeting(opts) {
  421. return ''
  422. },
  423. title(opts) {
  424. return `Getting more out of ${settings.appName}`
  425. },
  426. message(opts, isPlainText) {
  427. const learnLatexLink = EmailMessageHelper.displayLink(
  428. 'Learn LaTeX in 30 minutes',
  429. `${settings.siteUrl}/learn/latex/Learn_LaTeX_in_30_minutes?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  430. isPlainText
  431. )
  432. const templatesLinks = EmailMessageHelper.displayLink(
  433. 'Find a beautiful template',
  434. `${settings.siteUrl}/latex/templates?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  435. isPlainText
  436. )
  437. const collaboratorsLink = EmailMessageHelper.displayLink(
  438. 'Work with your collaborators',
  439. `${settings.siteUrl}/learn/how-to/Sharing_a_project?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  440. isPlainText
  441. )
  442. const siteLink = EmailMessageHelper.displayLink(
  443. 'www.overleaf.com',
  444. settings.siteUrl,
  445. isPlainText
  446. )
  447. const userSettingsLink = EmailMessageHelper.displayLink(
  448. 'here',
  449. `${settings.siteUrl}/user/email-preferences`,
  450. isPlainText
  451. )
  452. const onboardingSurveyLink = EmailMessageHelper.displayLink(
  453. 'Join our user feedback program',
  454. 'https://forms.gle/DB7pdk2B1VFQqVVB9',
  455. isPlainText
  456. )
  457. return [
  458. `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:`,
  459. `${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!`,
  460. `${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.`,
  461. `${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.`,
  462. `${onboardingSurveyLink} to help us make Overleaf even better!`,
  463. 'Thanks again for using Overleaf :)',
  464. `Lee`,
  465. `Lee Shalit<br />CEO<br />${siteLink}<hr>`,
  466. `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}.`,
  467. ]
  468. },
  469. })
  470. templates.securityAlert = NoCTAEmailTemplate({
  471. subject(opts) {
  472. return `Overleaf security note: ${opts.action}`
  473. },
  474. title(opts) {
  475. return opts.action.charAt(0).toUpperCase() + opts.action.slice(1)
  476. },
  477. message(opts, isPlainText) {
  478. const dateFormatted = moment().format('dddd D MMMM YYYY')
  479. const timeFormatted = moment().format('HH:mm')
  480. const helpLink = EmailMessageHelper.displayLink(
  481. 'quick guide',
  482. `${settings.siteUrl}/learn/how-to/Keeping_your_account_secure`,
  483. isPlainText
  484. )
  485. const actionDescribed = EmailMessageHelper.cleanHTML(
  486. opts.actionDescribed,
  487. isPlainText
  488. )
  489. if (!opts.message) {
  490. opts.message = []
  491. }
  492. const message = opts.message.map(m => {
  493. return EmailMessageHelper.cleanHTML(m, isPlainText)
  494. })
  495. return [
  496. `We are writing to let you know that ${actionDescribed} on ${dateFormatted} at ${timeFormatted} GMT.`,
  497. ...message,
  498. `If this was you, you can ignore this email.`,
  499. `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.`,
  500. `We also encourage you to read our ${helpLink} to keeping your ${settings.appName} account safe.`,
  501. ]
  502. },
  503. })
  504. templates.SAMLDataCleared = ctaTemplate({
  505. subject(opts) {
  506. return `Institutional Login No Longer Linked - ${settings.appName}`
  507. },
  508. title(opts) {
  509. return 'Institutional Login No Longer Linked'
  510. },
  511. message(opts, isPlainText) {
  512. return [
  513. `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.`,
  514. `To get it going again, you'll need to relink your institutional email address to your ${settings.appName} account via your settings.`,
  515. ]
  516. },
  517. secondaryMessage() {
  518. return [
  519. `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.`,
  520. '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.',
  521. `If you have any questions, please get in touch with our support team at ${settings.adminEmail} or by replying to this email.`,
  522. ]
  523. },
  524. ctaText(opts) {
  525. return 'Update my Emails and Affiliations'
  526. },
  527. ctaURL(opts) {
  528. return `${settings.siteUrl}/user/settings`
  529. },
  530. })
  531. function _formatUserNameAndEmail(user, placeholder) {
  532. if (user.first_name && user.last_name) {
  533. const fullName = `${user.first_name} ${user.last_name}`
  534. if (SpamSafe.isSafeUserName(fullName)) {
  535. if (SpamSafe.isSafeEmail(user.email)) {
  536. return `${fullName} (${user.email})`
  537. } else {
  538. return fullName
  539. }
  540. }
  541. }
  542. return SpamSafe.safeEmail(user.email, placeholder)
  543. }
  544. module.exports = {
  545. templates,
  546. ctaTemplate,
  547. NoCTAEmailTemplate,
  548. buildEmail,
  549. }