EmailSender.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { callbackify } from 'node:util'
  2. import logger from '@overleaf/logger'
  3. import metrics from '@overleaf/metrics'
  4. import Settings from '@overleaf/settings'
  5. import nodemailer from 'nodemailer'
  6. import * as aws from '@aws-sdk/client-ses'
  7. import OError from '@overleaf/o-error'
  8. import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
  9. import _ from 'lodash'
  10. const EMAIL_SETTINGS = Settings.email || {}
  11. export default {
  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: options.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 (options.cc) {
  102. sendMailOptions.cc = options.cc
  103. }
  104. if (EMAIL_SETTINGS.textEncoding != null) {
  105. sendMailOptions.textEncoding = EMAIL_SETTINGS.textEncoding
  106. }
  107. if (options.category) {
  108. // category support for sendgrid
  109. sendMailOptions.headers = {
  110. 'X-SMTPAPI': JSON.stringify({ category: options.category }),
  111. }
  112. }
  113. await client.sendMail(sendMailOptions)
  114. } catch (err) {
  115. throw new OError('error sending message').withCause(err)
  116. }
  117. }
  118. async function checkCanSendEmail(options) {
  119. if (options.sendingUser_id == null) {
  120. // email not sent from user, not rate limited
  121. return true
  122. }
  123. try {
  124. await rateLimiter.consume(options.sendingUser_id, 1, { method: 'userId' })
  125. } catch (err) {
  126. if (err instanceof Error) {
  127. throw err
  128. } else {
  129. return false
  130. }
  131. }
  132. return true
  133. }