EmailSender.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. const { callbackify } = require('util')
  2. const logger = require('@overleaf/logger')
  3. const metrics = require('@overleaf/metrics')
  4. const Settings = require('@overleaf/settings')
  5. const nodemailer = require('nodemailer')
  6. const aws = require('@aws-sdk/client-ses')
  7. const OError = require('@overleaf/o-error')
  8. const { RateLimiter } = require('../../infrastructure/RateLimiter')
  9. const _ = require('lodash')
  10. const EMAIL_SETTINGS = Settings.email || {}
  11. module.exports = {
  12. sendEmail: callbackify(sendEmail),
  13. promises: {
  14. sendEmail,
  15. },
  16. }
  17. const client = getClient()
  18. const rateLimiter = new RateLimiter('send_email', {
  19. points: 100,
  20. duration: 3 * 60 * 60,
  21. })
  22. function getClient() {
  23. let client
  24. if (EMAIL_SETTINGS.parameters) {
  25. const emailParameters = EMAIL_SETTINGS.parameters
  26. if (emailParameters.AWSAccessKeyID || EMAIL_SETTINGS.driver === 'ses') {
  27. logger.debug('using aws ses for email')
  28. const ses = new aws.SESClient({
  29. apiVersion: '2010-12-01',
  30. region: emailParameters.region,
  31. credentials: {
  32. accessKeyId: emailParameters.AWSAccessKeyID,
  33. secretAccessKey: emailParameters.AWSSecretKey,
  34. },
  35. })
  36. client = nodemailer.createTransport({ SES: { ses, aws } })
  37. } else if (emailParameters.sendgridApiKey) {
  38. throw new OError(
  39. 'sendgridApiKey configuration option is deprecated, use SMTP instead'
  40. )
  41. } else if (emailParameters.MandrillApiKey) {
  42. throw new OError(
  43. 'MandrillApiKey configuration option is deprecated, use SMTP instead'
  44. )
  45. } else {
  46. logger.debug('using smtp for email')
  47. const smtp = _.pick(
  48. emailParameters,
  49. 'host',
  50. 'port',
  51. 'secure',
  52. 'auth',
  53. 'ignoreTLS',
  54. 'tls',
  55. 'logger',
  56. 'name'
  57. )
  58. client = nodemailer.createTransport(smtp)
  59. }
  60. } else {
  61. logger.warn(
  62. 'Email transport and/or parameters not defined. No emails will be sent.'
  63. )
  64. client = {
  65. async sendMail(options) {
  66. logger.info({ options }, 'Would send email if enabled.')
  67. },
  68. }
  69. }
  70. return client
  71. }
  72. async function sendEmail(options, emailType) {
  73. try {
  74. const canContinue = await checkCanSendEmail(options)
  75. metrics.inc('email_status', {
  76. status: canContinue ? 'sent' : 'rate_limited',
  77. path: emailType,
  78. })
  79. if (!canContinue) {
  80. logger.debug(
  81. {
  82. sendingUserId: options.sendingUser_id,
  83. to: options.to,
  84. subject: options.subject,
  85. canContinue,
  86. },
  87. 'rate limit hit for sending email, not sending'
  88. )
  89. throw new OError('rate limit hit sending email')
  90. }
  91. metrics.inc('email')
  92. const sendMailOptions = {
  93. to: options.to,
  94. from: EMAIL_SETTINGS.fromAddress || '',
  95. subject: options.subject,
  96. html: options.html,
  97. text: options.text,
  98. replyTo: options.replyTo || EMAIL_SETTINGS.replyToAddress,
  99. socketTimeout: 30 * 1000,
  100. }
  101. if (EMAIL_SETTINGS.textEncoding != null) {
  102. sendMailOptions.textEncoding = EMAIL_SETTINGS.textEncoding
  103. }
  104. if (options.category) {
  105. // category support for sendgrid
  106. sendMailOptions.headers = {
  107. 'X-SMTPAPI': JSON.stringify({ category: options.category }),
  108. }
  109. }
  110. await client.sendMail(sendMailOptions)
  111. } catch (err) {
  112. throw new OError('error sending message').withCause(err)
  113. }
  114. }
  115. async function checkCanSendEmail(options) {
  116. if (options.sendingUser_id == null) {
  117. // email not sent from user, not rate limited
  118. return true
  119. }
  120. try {
  121. await rateLimiter.consume(options.sendingUser_id, 1, { method: 'userId' })
  122. } catch (err) {
  123. if (err instanceof Error) {
  124. throw err
  125. } else {
  126. return false
  127. }
  128. }
  129. return true
  130. }