EmailBuilder.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. const _ = require('underscore')
  2. const settings = require('settings-sharelatex')
  3. const marked = require('marked')
  4. const StringHelper = require('../Helpers/StringHelper')
  5. const BaseWithHeaderEmailLayout = require(`./Layouts/BaseWithHeaderEmailLayout`)
  6. const SpamSafe = require('./SpamSafe')
  7. const SingleCTAEmailBody = require(`./Bodies/SingleCTAEmailBody`)
  8. const NoCTAEmailBody = require(`./Bodies/NoCTAEmailBody`)
  9. function CTAEmailTemplate(content) {
  10. if (content.greeting == null) {
  11. content.greeting = () => 'Hi,'
  12. }
  13. if (content.secondaryMessage == null) {
  14. content.secondaryMessage = () => ''
  15. }
  16. return {
  17. subject(opts) {
  18. return content.subject(opts)
  19. },
  20. layout: BaseWithHeaderEmailLayout,
  21. plainTextTemplate(opts) {
  22. return `\
  23. ${content.greeting(opts)}
  24. ${content.message(opts).trim()}
  25. ${content.ctaText(opts)}: ${content.ctaURL(opts)}
  26. ${(typeof content.secondaryMessage === 'function'
  27. ? content.secondaryMessage(opts).trim()
  28. : undefined) || ''}
  29. Regards,
  30. The ${settings.appName} Team - ${settings.siteUrl}\
  31. `
  32. },
  33. compiledTemplate(opts) {
  34. return SingleCTAEmailBody({
  35. title:
  36. typeof content.title === 'function' ? content.title(opts) : undefined,
  37. greeting: content.greeting(opts),
  38. message: marked(content.message(opts).trim()),
  39. secondaryMessage: marked(content.secondaryMessage(opts).trim()),
  40. ctaText: content.ctaText(opts),
  41. ctaURL: content.ctaURL(opts),
  42. gmailGoToAction:
  43. typeof content.gmailGoToAction === 'function'
  44. ? content.gmailGoToAction(opts)
  45. : undefined,
  46. StringHelper
  47. })
  48. }
  49. }
  50. }
  51. function NoCTAEmailTemplate(content) {
  52. if (content.greeting == null) {
  53. content.greeting = () => 'Hi,'
  54. }
  55. if (content.secondaryMessage == null) {
  56. content.secondaryMessage = () => ''
  57. }
  58. return {
  59. subject(opts) {
  60. return content.subject(opts)
  61. },
  62. layout: BaseWithHeaderEmailLayout,
  63. plainTextTemplate(opts) {
  64. return `\
  65. ${content.greeting(opts)}
  66. ${content.message(opts).trim()}
  67. ${(typeof content.secondaryMessage === 'function'
  68. ? content.secondaryMessage(opts).trim()
  69. : undefined) || ''}
  70. Regards,
  71. The ${settings.appName} Team - ${settings.siteUrl}\
  72. `
  73. },
  74. compiledTemplate(opts) {
  75. return NoCTAEmailBody({
  76. title:
  77. typeof content.title === 'function' ? content.title(opts) : undefined,
  78. greeting: content.greeting(opts),
  79. message: marked(content.message(opts).trim()),
  80. secondaryMessage: marked(content.secondaryMessage(opts).trim()),
  81. gmailGoToAction:
  82. typeof content.gmailGoToAction === 'function'
  83. ? content.gmailGoToAction(opts)
  84. : undefined,
  85. StringHelper
  86. })
  87. }
  88. }
  89. }
  90. function buildEmail(templateName, opts) {
  91. const template = templates[templateName]
  92. opts.siteUrl = settings.siteUrl
  93. opts.body = template.compiledTemplate(opts)
  94. if (
  95. settings.email &&
  96. settings.email.template &&
  97. settings.email.template.customFooter
  98. ) {
  99. opts.body += settings.email.template.customFooter
  100. }
  101. return {
  102. subject: template.subject(opts),
  103. html: template.layout(opts),
  104. text: template.plainTextTemplate && template.plainTextTemplate(opts)
  105. }
  106. }
  107. const templates = {}
  108. templates.accountMergeToOverleafAddress = CTAEmailTemplate({
  109. subject() {
  110. return `Confirm Account Merge - ${settings.appName}`
  111. },
  112. title() {
  113. return 'Confirm Account Merge'
  114. },
  115. message() {
  116. return `\
  117. To merge your ShareLaTeX and Overleaf accounts, click the button below.
  118. If you think you have received this message in error,
  119. please contact us at https://www.overleaf.com/contact\
  120. `
  121. },
  122. ctaText() {
  123. return 'Confirm Account Merge'
  124. },
  125. ctaURL(opts) {
  126. return opts.tokenLinkUrl
  127. }
  128. })
  129. templates.accountMergeToSharelatexAddress =
  130. templates.accountMergeToOverleafAddress
  131. templates.registered = CTAEmailTemplate({
  132. subject() {
  133. return `Activate your ${settings.appName} Account`
  134. },
  135. message(opts) {
  136. return `\
  137. Congratulations, you've just had an account created for you on ${
  138. settings.appName
  139. } with the email address '${_.escape(opts.to)}'.
  140. Click here to set your password and log in:\
  141. `
  142. },
  143. secondaryMessage() {
  144. return `If you have any questions or problems, please contact ${
  145. settings.adminEmail
  146. }`
  147. },
  148. ctaText() {
  149. return 'Set password'
  150. },
  151. ctaURL(opts) {
  152. return opts.setNewPasswordUrl
  153. }
  154. })
  155. templates.canceledSubscription = CTAEmailTemplate({
  156. subject() {
  157. return `${settings.appName} thoughts`
  158. },
  159. message() {
  160. return `\
  161. I'm sorry to see you cancelled your ${
  162. settings.appName
  163. } premium account. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?\
  164. `
  165. },
  166. secondaryMessage() {
  167. return 'Thank you in advance!'
  168. },
  169. ctaText() {
  170. return 'Leave Feedback'
  171. },
  172. ctaURL(opts) {
  173. return 'https://docs.google.com/forms/d/e/1FAIpQLScqU6Je1r4Afz6ul6oY0RAfN7RabdWv_oL1u7Rj1YBmXS4fiQ/viewform?usp=sf_link'
  174. }
  175. })
  176. templates.reactivatedSubscription = CTAEmailTemplate({
  177. subject() {
  178. return `Subscription Reactivated - ${settings.appName}`
  179. },
  180. message(opts) {
  181. return `\
  182. Your subscription was reactivated successfully.\
  183. `
  184. },
  185. ctaText() {
  186. return 'View Subscription Dashboard'
  187. },
  188. ctaURL(opts) {
  189. return `${settings.siteUrl}/user/subscription`
  190. }
  191. })
  192. templates.passwordResetRequested = CTAEmailTemplate({
  193. subject() {
  194. return `Password Reset - ${settings.appName}`
  195. },
  196. title() {
  197. return 'Password Reset'
  198. },
  199. message() {
  200. return `We got a request to reset your ${settings.appName} password.`
  201. },
  202. secondaryMessage() {
  203. return `\
  204. If you ignore this message, your password won't be changed.
  205. If you didn't request a password reset, let us know.\
  206. `
  207. },
  208. ctaText() {
  209. return 'Reset password'
  210. },
  211. ctaURL(opts) {
  212. return opts.setNewPasswordUrl
  213. }
  214. })
  215. templates.passwordChanged = NoCTAEmailTemplate({
  216. subject(opts) {
  217. return `Password Changed - ${settings.appName}`
  218. },
  219. title(opts) {
  220. return `Password Changed`
  221. },
  222. message(opts) {
  223. return `We're contacting you to notify you that your password has been set or changed.
  224. If you have recently set your password for the first time, or if you just changed your password, you don't need to take any further action. If you didn't set or change your password, please contact us.`
  225. }
  226. })
  227. templates.confirmEmail = CTAEmailTemplate({
  228. subject() {
  229. return `Confirm Email - ${settings.appName}`
  230. },
  231. title() {
  232. return 'Confirm Email'
  233. },
  234. message() {
  235. return `Please confirm your email on ${settings.appName}.`
  236. },
  237. ctaText() {
  238. return 'Confirm Email'
  239. },
  240. ctaURL(opts) {
  241. return opts.confirmEmailUrl
  242. }
  243. })
  244. templates.projectInvite = CTAEmailTemplate({
  245. subject(opts) {
  246. return `${_.escape(
  247. SpamSafe.safeProjectName(opts.project.name, 'New Project')
  248. )} - shared by ${_.escape(
  249. SpamSafe.safeEmail(opts.owner.email, 'a collaborator')
  250. )}`
  251. },
  252. title(opts) {
  253. return `${_.escape(
  254. SpamSafe.safeProjectName(opts.project.name, 'New Project')
  255. )} - shared by ${_.escape(
  256. SpamSafe.safeEmail(opts.owner.email, 'a collaborator')
  257. )}`
  258. },
  259. message(opts) {
  260. return `${_.escape(
  261. SpamSafe.safeEmail(opts.owner.email, 'a collaborator')
  262. )} wants to share ${_.escape(
  263. SpamSafe.safeProjectName(opts.project.name, 'a new project')
  264. )} with you.`
  265. },
  266. ctaText() {
  267. return 'View project'
  268. },
  269. ctaURL(opts) {
  270. return opts.inviteUrl
  271. },
  272. gmailGoToAction(opts) {
  273. return {
  274. target: opts.inviteUrl,
  275. name: 'View project',
  276. description: `Join ${_.escape(
  277. SpamSafe.safeProjectName(opts.project.name, 'project')
  278. )} at ${settings.appName}`
  279. }
  280. }
  281. })
  282. templates.verifyEmailToJoinTeam = CTAEmailTemplate({
  283. subject(opts) {
  284. return `${_.escape(
  285. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  286. )} has invited you to join a team on ${settings.appName}`
  287. },
  288. title(opts) {
  289. return `${_.escape(
  290. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  291. )} has invited you to join a team on ${settings.appName}`
  292. },
  293. message(opts) {
  294. return `Please click the button below to join the team and enjoy the benefits of an upgraded ${
  295. settings.appName
  296. } account.`
  297. },
  298. ctaText(opts) {
  299. return 'Join now'
  300. },
  301. ctaURL(opts) {
  302. return opts.acceptInviteUrl
  303. }
  304. })
  305. templates.dropboxUnlinkedDuplicate = CTAEmailTemplate({
  306. subject() {
  307. return `Your Dropbox Account has been Unlinked - ${settings.appName}`
  308. },
  309. message(opts) {
  310. return `\
  311. Our automated systems have detected that your Dropbox account was linked to more than one Overleaf accounts. This should not have been allowed and might be causing issues with the Dropbox sync feature.
  312. We have now unlinked all your Dropbox and Overleaf Accounts. To ensure your project will keep syncing you can link your Dropbox account to the Overleaf account of your choice now.\
  313. `
  314. },
  315. ctaText() {
  316. return 'Link Dropbox Account'
  317. },
  318. ctaURL(opts) {
  319. return `${settings.siteUrl}/user/settings`
  320. }
  321. })
  322. templates.testEmail = CTAEmailTemplate({
  323. subject() {
  324. return `A Test Email from ${settings.appName}`
  325. },
  326. title() {
  327. return `A Test Email from ${settings.appName}`
  328. },
  329. greeting() {
  330. return 'Hi,'
  331. },
  332. message() {
  333. return `This is a test Email from ${settings.appName}`
  334. },
  335. ctaText() {
  336. return `Open ${settings.appName}`
  337. },
  338. ctaURL() {
  339. return settings.siteUrl
  340. }
  341. })
  342. templates.projectsTransferredFromSharelatex = CTAEmailTemplate({
  343. subject() {
  344. return 'ShareLaTeX projects transferred to your Overleaf account'
  345. },
  346. title() {
  347. return 'ShareLaTeX projects transferred to your Overleaf account'
  348. },
  349. message(opts) {
  350. return `\
  351. We are writing with important information about your Overleaf and ShareLaTeX accounts.
  352. As part of our ongoing work to [integrate Overleaf and ShareLaTeX](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf),
  353. we found a ShareLaTeX account with the email address ${
  354. opts.to
  355. } that matches your Overleaf account.
  356. We have now transferred the projects from this ShareLaTeX account into your Overleaf account, so you may notice some new
  357. projects on your Overleaf projects page.
  358. When you next log in, you may be prompted to reconfirm your email address in order to regain access to your account.
  359. If you have any questions, please contact our support team by reply.\
  360. `
  361. },
  362. ctaText() {
  363. return `Log in to ${settings.appName}`
  364. },
  365. ctaURL() {
  366. return settings.siteUrl + '/login'
  367. }
  368. })
  369. templates.emailAddressPoachedEmail = CTAEmailTemplate({
  370. subject() {
  371. return `One of your email addresses has been moved to another ${
  372. settings.appName
  373. } account`
  374. },
  375. title() {
  376. return `One of your email addresses has been moved to another ${
  377. settings.appName
  378. } account`
  379. },
  380. message(opts) {
  381. let message = `\
  382. We are writing with important information about your Overleaf account.
  383. You added the email address ${opts.poached} to your ${
  384. opts.to
  385. } Overleaf account as a secondary (or affiliation)
  386. email address, but we have had to remove it.
  387. This is because your ${
  388. opts.poached
  389. } email address was also in use as the primary email address for an older Overleaf
  390. account from before our [integration with ShareLaTeX to create Overleaf v2](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf).
  391. ### What do I need to do?
  392. You now have two Overleaf accounts, one under ${opts.poached} and one under ${
  393. opts.to
  394. }.
  395. You may wish to log in to Overleaf as ${
  396. opts.poached
  397. } to check whether you have projects there that you would like to
  398. keep. If you are not sure of the password, you can send yourself a password reset email to ${
  399. opts.poached
  400. }, via
  401. https://www.overleaf.com/user/password/reset
  402. Once you have downloaded your projects, you may wish to delete your
  403. ${
  404. opts.poached
  405. } Overleaf account, which you can do from your account settings. You will then be able to add
  406. ${opts.poached} as a secondary email address on your ${opts.to} account again.
  407. \
  408. `
  409. if (opts.proFeatures) {
  410. message += `\
  411. Because your ${
  412. opts.poached
  413. } email address was an institutional affiliation through which you had Pro features. Your Pro
  414. features have been transferred to your ${
  415. opts.poached
  416. } account. If you would like to transfer them back to your
  417. ${opts.to} account, you will need to delete the ${
  418. opts.poached
  419. } account and re-add it as a secondary email address,
  420. as described above.
  421. \
  422. `
  423. }
  424. message += `\
  425. If you have any questions, you can contact our support team by reply.\
  426. `
  427. return message
  428. },
  429. ctaText() {
  430. return `Log in to ${settings.appName}`
  431. },
  432. ctaURL() {
  433. return settings.siteUrl + '/login'
  434. }
  435. })
  436. templates.emailThirdPartyIdentifierLinked = NoCTAEmailTemplate({
  437. subject(opts) {
  438. return `Your ${settings.appName} account is now linked with ${
  439. opts.provider
  440. }`
  441. },
  442. title(opts) {
  443. return `Accounts Linked`
  444. },
  445. message(opts) {
  446. let message = `We're contacting you to notify you that your ${opts.provider}
  447. account is now linked to your ${settings.appName} account.`
  448. return message
  449. }
  450. })
  451. templates.emailThirdPartyIdentifierUnlinked = NoCTAEmailTemplate({
  452. subject(opts) {
  453. return `Your ${settings.appName} account is no longer linked with ${
  454. opts.provider
  455. }`
  456. },
  457. title(opts) {
  458. return `Accounts No Longer Linked`
  459. },
  460. message(opts) {
  461. let message = `We're contacting you to notify you that your ${opts.provider}
  462. account is no longer linked with your ${settings.appName} account.`
  463. return message
  464. }
  465. })
  466. templates.ownershipTransferConfirmationPreviousOwner = NoCTAEmailTemplate({
  467. subject(opts) {
  468. return `Project ownership transfer - ${settings.appName}`
  469. },
  470. title(opts) {
  471. const projectName = _.escape(
  472. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  473. )
  474. return `${projectName} - Owner change`
  475. },
  476. message(opts) {
  477. const nameAndEmail = _.escape(
  478. _formatUserNameAndEmail(opts.newOwner, 'a collaborator')
  479. )
  480. const projectName = _.escape(
  481. SpamSafe.safeProjectName(opts.project.name, 'your project')
  482. )
  483. return `\
  484. As per your request, we have made ${nameAndEmail} the owner of ${projectName}.
  485. If you haven't asked to change the owner of **${projectName}**, please get in touch
  486. with us via ${settings.adminEmail}.
  487. `
  488. }
  489. })
  490. templates.ownershipTransferConfirmationNewOwner = CTAEmailTemplate({
  491. subject(opts) {
  492. return `Project ownership transfer - ${settings.appName}`
  493. },
  494. title(opts) {
  495. const projectName = _.escape(
  496. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  497. )
  498. return `${projectName} - Owner change`
  499. },
  500. message(opts) {
  501. const nameAndEmail = _.escape(
  502. _formatUserNameAndEmail(opts.previousOwner, 'A collaborator')
  503. )
  504. const projectName = _.escape(
  505. SpamSafe.safeProjectName(opts.project.name, 'a project')
  506. )
  507. return `\
  508. ${nameAndEmail} has made you the owner of **${projectName}**. You can now
  509. manage ${projectName} sharing settings.
  510. `
  511. },
  512. ctaText(opts) {
  513. return 'View project'
  514. },
  515. ctaURL(opts) {
  516. const projectUrl = `${
  517. settings.siteUrl
  518. }/project/${opts.project._id.toString()}`
  519. return projectUrl
  520. }
  521. })
  522. templates.userOnboardingEmail = NoCTAEmailTemplate({
  523. subject(opts) {
  524. return `Getting more out of ${settings.appName}`
  525. },
  526. greeting(opts) {
  527. return ''
  528. },
  529. title(opts) {
  530. return `Getting more out of ${settings.appName}`
  531. },
  532. message(opts) {
  533. return `\
  534. Thanks for signing up for ${
  535. settings.appName
  536. } recently. We hope you've been finding it useful!
  537. Here are some key features to help you get the most out of the service:
  538. <a href="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding">Learn LaTeX in 30 minutes</a>: 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!
  539. <a href="https://www.overleaf.com/latex/templates?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding">Find a beautiful template</a>: 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.
  540. <a href="https://www.overleaf.com/learn/how-to/Sharing_a_project?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding">Work with your collaborators</a>: 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 projecs with your colleagues in this quick how-to guide.
  541. If you have any questions, please let us know, and thanks again for using Overleaf.
  542. John
  543. Dr John Hammersley <br />
  544. Co-founder & CEO <br />
  545. <a href="http://www.overleaf.com">www.overleaf.com</a>
  546. <hr>
  547. `
  548. },
  549. secondaryMessage() {
  550. return `Don't want onboarding emails like this from us? Don't worry, this is the only one.
  551. If you've previously subscribed to emails about product offers and company news and events,
  552. you can unsubscribe <a href="${settings.siteUrl}/user/settings"> here </a>.`
  553. }
  554. })
  555. function _formatUserNameAndEmail(user, placeholder) {
  556. if (user.first_name && user.last_name) {
  557. const fullName = `${user.first_name} ${user.last_name}`
  558. if (SpamSafe.isSafeUserName(fullName)) {
  559. if (SpamSafe.isSafeEmail(user.email)) {
  560. return `${fullName} (${user.email})`
  561. } else {
  562. return fullName
  563. }
  564. }
  565. }
  566. return SpamSafe.safeEmail(user.email, placeholder)
  567. }
  568. module.exports = {
  569. templates,
  570. CTAEmailTemplate,
  571. NoCTAEmailTemplate,
  572. buildEmail
  573. }