EmailBuilder.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. const _ = require('underscore')
  2. const settings = require('settings-sharelatex')
  3. const marked = require('marked')
  4. const moment = require('moment')
  5. const EmailMessageHelper = require('./EmailMessageHelper')
  6. const StringHelper = require('../Helpers/StringHelper')
  7. const BaseWithHeaderEmailLayout = require(`./Layouts/BaseWithHeaderEmailLayout`)
  8. const SpamSafe = require('./SpamSafe')
  9. const ctaEmailBody = require('./Bodies/cta-email')
  10. const SingleCTAEmailBody = require(`./Bodies/SingleCTAEmailBody`)
  11. const NoCTAEmailBody = require(`./Bodies/NoCTAEmailBody`)
  12. function _emailBodyPlainText(content, opts, ctaEmail) {
  13. let emailBody = `${content.greeting(opts, true)}`
  14. emailBody += `\r\n\r\n`
  15. emailBody += `${content.message(opts, true).join('\r\n\r\n')}`
  16. if (ctaEmail) {
  17. emailBody += `\r\n\r\n`
  18. emailBody += `${content.ctaText(opts, true)}: ${content.ctaURL(opts, true)}`
  19. }
  20. if (
  21. content.secondaryMessage(opts, true) &&
  22. content.secondaryMessage(opts, true).length > 0
  23. ) {
  24. emailBody += `\r\n\r\n`
  25. emailBody += `${content.secondaryMessage(opts, true).join('\r\n\r\n')}`
  26. }
  27. emailBody += `\r\n\r\n`
  28. emailBody += `Regards,\r\nThe ${settings.appName} Team - ${settings.siteUrl}`
  29. return emailBody
  30. }
  31. function ctaTemplate(content) {
  32. if (
  33. !content.ctaURL ||
  34. !content.ctaText ||
  35. !content.message ||
  36. !content.subject
  37. ) {
  38. throw new Error('missing required CTA email content')
  39. }
  40. if (!content.title) {
  41. content.title = () => {}
  42. }
  43. if (!content.greeting) {
  44. content.greeting = () => 'Hi,'
  45. }
  46. if (!content.secondaryMessage) {
  47. content.secondaryMessage = () => []
  48. }
  49. if (!content.gmailGoToAction) {
  50. content.gmailGoToAction = () => {}
  51. }
  52. return {
  53. subject(opts) {
  54. return content.subject(opts)
  55. },
  56. layout: BaseWithHeaderEmailLayout,
  57. plainTextTemplate(opts) {
  58. return _emailBodyPlainText(content, opts, true)
  59. },
  60. compiledTemplate(opts) {
  61. return ctaEmailBody({
  62. title: content.title(opts),
  63. greeting: content.greeting(opts),
  64. message: content.message(opts),
  65. secondaryMessage: content.secondaryMessage(opts),
  66. ctaText: content.ctaText(opts),
  67. ctaURL: content.ctaURL(opts),
  68. gmailGoToAction: content.gmailGoToAction(opts),
  69. StringHelper
  70. })
  71. }
  72. }
  73. }
  74. //
  75. // DEPRECATED
  76. //
  77. // Use ctaTemplate instead of CTAEmailTemplate
  78. //
  79. function CTAEmailTemplate(content) {
  80. if (content.greeting == null) {
  81. content.greeting = () => 'Hi,'
  82. }
  83. if (content.secondaryMessage == null) {
  84. content.secondaryMessage = () => ''
  85. }
  86. return {
  87. subject(opts) {
  88. return content.subject(opts)
  89. },
  90. layout: BaseWithHeaderEmailLayout,
  91. plainTextTemplate(opts) {
  92. return `\
  93. ${content.greeting(opts)}
  94. ${content.message(opts).trim()}
  95. ${content.ctaText(opts)}: ${content.ctaURL(opts)}
  96. ${(typeof content.secondaryMessage === 'function'
  97. ? content.secondaryMessage(opts).trim()
  98. : undefined) || ''}
  99. Regards,
  100. The ${settings.appName} Team - ${settings.siteUrl}\
  101. `
  102. },
  103. compiledTemplate(opts) {
  104. return SingleCTAEmailBody({
  105. title:
  106. typeof content.title === 'function' ? content.title(opts) : undefined,
  107. greeting: content.greeting(opts),
  108. message: marked(content.message(opts).trim()),
  109. secondaryMessage: marked(content.secondaryMessage(opts).trim()),
  110. ctaText: content.ctaText(opts),
  111. ctaURL: content.ctaURL(opts),
  112. gmailGoToAction:
  113. typeof content.gmailGoToAction === 'function'
  114. ? content.gmailGoToAction(opts)
  115. : undefined,
  116. StringHelper
  117. })
  118. }
  119. }
  120. }
  121. function NoCTAEmailTemplate(content) {
  122. if (content.greeting == null) {
  123. content.greeting = () => 'Hi,'
  124. }
  125. if (!content.message) {
  126. throw new Error('missing message')
  127. }
  128. return {
  129. subject(opts) {
  130. return content.subject(opts)
  131. },
  132. layout: BaseWithHeaderEmailLayout,
  133. plainTextTemplate(opts) {
  134. return `\
  135. ${content.greeting(opts)}
  136. ${content.message(opts, true).join('\r\n\r\n')}
  137. Regards,
  138. The ${settings.appName} Team - ${settings.siteUrl}\
  139. `
  140. },
  141. compiledTemplate(opts) {
  142. return NoCTAEmailBody({
  143. title:
  144. typeof content.title === 'function' ? content.title(opts) : undefined,
  145. greeting: content.greeting(opts),
  146. message: content.message(opts),
  147. StringHelper
  148. })
  149. }
  150. }
  151. }
  152. function buildEmail(templateName, opts) {
  153. const template = templates[templateName]
  154. opts.siteUrl = settings.siteUrl
  155. opts.body = template.compiledTemplate(opts)
  156. if (
  157. settings.email &&
  158. settings.email.template &&
  159. settings.email.template.customFooter
  160. ) {
  161. opts.body += settings.email.template.customFooter
  162. }
  163. return {
  164. subject: template.subject(opts),
  165. html: template.layout(opts),
  166. text: template.plainTextTemplate && template.plainTextTemplate(opts)
  167. }
  168. }
  169. const templates = {}
  170. templates.registered = CTAEmailTemplate({
  171. subject() {
  172. return `Activate your ${settings.appName} Account`
  173. },
  174. message(opts) {
  175. return `\
  176. Congratulations, you've just had an account created for you on ${
  177. settings.appName
  178. } with the email address '${_.escape(opts.to)}'.
  179. Click here to set your password and log in:\
  180. `
  181. },
  182. secondaryMessage() {
  183. return `If you have any questions or problems, please contact ${
  184. settings.adminEmail
  185. }`
  186. },
  187. ctaText() {
  188. return 'Set password'
  189. },
  190. ctaURL(opts) {
  191. return opts.setNewPasswordUrl
  192. }
  193. })
  194. templates.canceledSubscription = CTAEmailTemplate({
  195. subject() {
  196. return `${settings.appName} thoughts`
  197. },
  198. message() {
  199. return `\
  200. We are sorry to see you cancelled your ${
  201. settings.appName
  202. } premium subscription. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?\
  203. `
  204. },
  205. secondaryMessage() {
  206. return 'Thank you in advance!'
  207. },
  208. ctaText() {
  209. return 'Leave Feedback'
  210. },
  211. ctaURL(opts) {
  212. return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
  213. }
  214. })
  215. templates.reactivatedSubscription = CTAEmailTemplate({
  216. subject() {
  217. return `Subscription Reactivated - ${settings.appName}`
  218. },
  219. message(opts) {
  220. return `\
  221. Your subscription was reactivated successfully.\
  222. `
  223. },
  224. ctaText() {
  225. return 'View Subscription Dashboard'
  226. },
  227. ctaURL(opts) {
  228. return `${settings.siteUrl}/user/subscription`
  229. }
  230. })
  231. templates.passwordResetRequested = ctaTemplate({
  232. subject() {
  233. return `Password Reset - ${settings.appName}`
  234. },
  235. title() {
  236. return 'Password Reset'
  237. },
  238. message() {
  239. return [`We got a request to reset your ${settings.appName} password.`]
  240. },
  241. secondaryMessage() {
  242. return [
  243. "If you ignore this message, your password won't be changed.",
  244. "If you didn't request a password reset, let us know."
  245. ]
  246. },
  247. ctaText() {
  248. return 'Reset password'
  249. },
  250. ctaURL(opts) {
  251. return opts.setNewPasswordUrl
  252. }
  253. })
  254. templates.confirmEmail = ctaTemplate({
  255. subject() {
  256. return `Confirm Email - ${settings.appName}`
  257. },
  258. title() {
  259. return 'Confirm Email'
  260. },
  261. message() {
  262. return [`Please confirm your email on ${settings.appName}.`]
  263. },
  264. ctaText() {
  265. return 'Confirm Email'
  266. },
  267. ctaURL(opts) {
  268. return opts.confirmEmailUrl
  269. }
  270. })
  271. templates.projectInvite = CTAEmailTemplate({
  272. subject(opts) {
  273. return `${_.escape(
  274. SpamSafe.safeProjectName(opts.project.name, 'New Project')
  275. )} - shared by ${_.escape(
  276. SpamSafe.safeEmail(opts.owner.email, 'a collaborator')
  277. )}`
  278. },
  279. title(opts) {
  280. return `${_.escape(
  281. SpamSafe.safeProjectName(opts.project.name, 'New Project')
  282. )} - shared by ${_.escape(
  283. SpamSafe.safeEmail(opts.owner.email, 'a collaborator')
  284. )}`
  285. },
  286. message(opts) {
  287. return `${_.escape(
  288. SpamSafe.safeEmail(opts.owner.email, 'a collaborator')
  289. )} wants to share ${_.escape(
  290. SpamSafe.safeProjectName(opts.project.name, 'a new project')
  291. )} with you.`
  292. },
  293. ctaText() {
  294. return 'View project'
  295. },
  296. ctaURL(opts) {
  297. return opts.inviteUrl
  298. },
  299. gmailGoToAction(opts) {
  300. return {
  301. target: opts.inviteUrl,
  302. name: 'View project',
  303. description: `Join ${_.escape(
  304. SpamSafe.safeProjectName(opts.project.name, 'project')
  305. )} at ${settings.appName}`
  306. }
  307. }
  308. })
  309. templates.verifyEmailToJoinTeam = ctaTemplate({
  310. subject(opts) {
  311. return `${_.escape(
  312. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  313. )} has invited you to join a team on ${settings.appName}`
  314. },
  315. title(opts) {
  316. return `${_.escape(
  317. _formatUserNameAndEmail(opts.inviter, 'A collaborator')
  318. )} has invited you to join a team on ${settings.appName}`
  319. },
  320. message(opts) {
  321. return [
  322. `Please click the button below to join the team and enjoy the benefits of an upgraded ${
  323. settings.appName
  324. } account.`
  325. ]
  326. },
  327. ctaText(opts) {
  328. return 'Join now'
  329. },
  330. ctaURL(opts) {
  331. return opts.acceptInviteUrl
  332. }
  333. })
  334. templates.testEmail = CTAEmailTemplate({
  335. subject() {
  336. return `A Test Email from ${settings.appName}`
  337. },
  338. title() {
  339. return `A Test Email from ${settings.appName}`
  340. },
  341. greeting() {
  342. return 'Hi,'
  343. },
  344. message() {
  345. return `This is a test Email from ${settings.appName}`
  346. },
  347. ctaText() {
  348. return `Open ${settings.appName}`
  349. },
  350. ctaURL() {
  351. return settings.siteUrl
  352. }
  353. })
  354. templates.ownershipTransferConfirmationPreviousOwner = NoCTAEmailTemplate({
  355. subject(opts) {
  356. return `Project ownership transfer - ${settings.appName}`
  357. },
  358. title(opts) {
  359. const projectName = _.escape(
  360. SpamSafe.safeProjectName(opts.project.name, 'Your project')
  361. )
  362. return `${projectName} - Owner change`
  363. },
  364. message(opts, isPlainText) {
  365. const nameAndEmail = _.escape(
  366. _formatUserNameAndEmail(opts.newOwner, 'a collaborator')
  367. )
  368. const projectName = _.escape(
  369. SpamSafe.safeProjectName(opts.project.name, 'your project')
  370. )
  371. const projectNameDisplay = isPlainText
  372. ? projectName
  373. : `<b>${projectName}</b>`
  374. return [
  375. `As per your request, we have made ${nameAndEmail} the owner of ${projectNameDisplay}.`,
  376. `If you haven't asked to change the owner of ${projectNameDisplay}, please get in touch with us via ${
  377. settings.adminEmail
  378. }.`
  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. `${
  430. settings.siteUrl
  431. }/learn/latex/Learn_LaTeX_in_30_minutes?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  432. isPlainText
  433. )
  434. const templatesLinks = EmailMessageHelper.displayLink(
  435. 'Find a beautiful template',
  436. `${
  437. settings.siteUrl
  438. }/latex/templates?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  439. isPlainText
  440. )
  441. const collaboratorsLink = EmailMessageHelper.displayLink(
  442. 'Work with your collaborators',
  443. `${
  444. settings.siteUrl
  445. }/learn/how-to/Sharing_a_project?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`,
  446. isPlainText
  447. )
  448. const siteLink = EmailMessageHelper.displayLink(
  449. 'www.overleaf.com',
  450. settings.siteUrl,
  451. isPlainText
  452. )
  453. const userSettingsLink = EmailMessageHelper.displayLink(
  454. 'here',
  455. `${settings.siteUrl}/user/settings`,
  456. isPlainText
  457. )
  458. return [
  459. `Thanks for signing up for ${
  460. settings.appName
  461. } recently. We hope you've been finding it useful! Here are some key features to help you get the most out of the service:`,
  462. `${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!`,
  463. `${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.`,
  464. `${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 projecs with your colleagues in this quick how-to guide.`,
  465. `If you have any questions, please let us know, and thanks again for using Overleaf.`,
  466. `John`,
  467. `Dr John Hammersley <br />Co-founder & CEO <br />${siteLink}<hr>`,
  468. `Don't want onboarding emails like this from us? Don't worry, this is the only one. If you've previously subscribed to emails about product offers and company news and events, you can unsubscribe ${userSettingsLink}.`
  469. ]
  470. }
  471. })
  472. templates.securityAlert = NoCTAEmailTemplate({
  473. subject(opts) {
  474. return `Overleaf security note: ${opts.action}`
  475. },
  476. title(opts) {
  477. return opts.action.charAt(0).toUpperCase() + opts.action.slice(1)
  478. },
  479. message(opts, isPlainText) {
  480. const dateFormatted = moment().format('dddd D MMMM YYYY')
  481. const timeFormatted = moment().format('HH:mm')
  482. const helpLink = EmailMessageHelper.displayLink(
  483. 'quick guide',
  484. `${settings.siteUrl}/learn/how-to/Keeping_your_account_secure`,
  485. isPlainText
  486. )
  487. const actionDescribed = EmailMessageHelper.cleanHTML(
  488. opts.actionDescribed,
  489. isPlainText
  490. )
  491. if (!opts.message) {
  492. opts.message = []
  493. }
  494. const message = opts.message.map(m => {
  495. return EmailMessageHelper.cleanHTML(m, isPlainText)
  496. })
  497. return [
  498. `We are writing to let you know that ${actionDescribed} on ${dateFormatted} at ${timeFormatted} GMT.`,
  499. ...message,
  500. `If this was you, you can ignore this email.`,
  501. `If this was not you, we recommend getting in touch with our support team at ${
  502. settings.adminEmail
  503. } to report this as potentially suspicious activity on your account.`,
  504. `We also encourage you to read our ${helpLink} to keeping your ${
  505. settings.appName
  506. } account safe.`
  507. ]
  508. }
  509. })
  510. function _formatUserNameAndEmail(user, placeholder) {
  511. if (user.first_name && user.last_name) {
  512. const fullName = `${user.first_name} ${user.last_name}`
  513. if (SpamSafe.isSafeUserName(fullName)) {
  514. if (SpamSafe.isSafeEmail(user.email)) {
  515. return `${fullName} (${user.email})`
  516. } else {
  517. return fullName
  518. }
  519. }
  520. }
  521. return SpamSafe.safeEmail(user.email, placeholder)
  522. }
  523. module.exports = {
  524. templates,
  525. ctaTemplate,
  526. CTAEmailTemplate,
  527. NoCTAEmailTemplate,
  528. buildEmail
  529. }