diff --git a/services/web/app/src/Features/Email/EmailHandler.js b/services/web/app/src/Features/Email/EmailHandler.js index 6c17752f8b..8d90dfacc3 100644 --- a/services/web/app/src/Features/Email/EmailHandler.js +++ b/services/web/app/src/Features/Email/EmailHandler.js @@ -2,11 +2,13 @@ const { callbackify } = require('util') const Settings = require('@overleaf/settings') const EmailBuilder = require('./EmailBuilder') const EmailSender = require('./EmailSender') +const Queues = require('../../infrastructure/Queues') const EMAIL_SETTINGS = Settings.email || {} module.exports = { sendEmail: callbackify(sendEmail), + sendDeferredEmail, promises: { sendEmail, }, @@ -22,3 +24,11 @@ async function sendEmail(emailType, opts) { opts.subject = email.subject await EmailSender.promises.sendEmail(opts) } + +function sendDeferredEmail(emailType, opts, delay) { + Queues.createScheduledJob( + 'deferred-emails', + { data: { emailType, opts } }, + delay + ) +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionHandler.js b/services/web/app/src/Features/Subscription/SubscriptionHandler.js index 98f256672f..28df01df8d 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionHandler.js @@ -199,20 +199,9 @@ function cancelSubscription(user, callback) { first_name: user.first_name, } const ONE_HOUR_IN_MS = 1000 * 60 * 60 - setTimeout( - () => - EmailHandler.sendEmail( - 'canceledSubscription', - emailOpts, - err => { - if (err) { - logger.warn( - { err }, - 'failed to send confirmation email for subscription cancellation' - ) - } - } - ), + EmailHandler.sendDeferredEmail( + 'canceledSubscription', + emailOpts, ONE_HOUR_IN_MS ) callback() diff --git a/services/web/app/src/infrastructure/QueueWorkers.js b/services/web/app/src/infrastructure/QueueWorkers.js index cc99a7a7e7..814e90f4e9 100644 --- a/services/web/app/src/infrastructure/QueueWorkers.js +++ b/services/web/app/src/infrastructure/QueueWorkers.js @@ -7,6 +7,9 @@ const { addOptionalCleanupHandlerBeforeStoppingTraffic, addRequiredCleanupHandlerBeforeDrainingConnections, } = require('./GracefulShutdown') +const EmailHandler = require('../Features/Email/EmailHandler') +const logger = require('@overleaf/logger') +const OError = require('@overleaf/o-error') function start() { if (!Features.hasFeature('saas')) { @@ -47,6 +50,19 @@ function start() { await FeaturesUpdater.promises.refreshFeatures(userId, reason) }) registerCleanup(refreshFeaturesQueue) + + const deferredEmailsQueue = Queues.getQueue('deferred-emails') + deferredEmailsQueue.process(async job => { + const { emailType, opts } = job.data + try { + await EmailHandler.promises.sendEmail(emailType, opts) + } catch (e) { + const error = OError.tag(e, 'failed to send deferred email') + logger.warn(error) + throw error + } + }) + registerCleanup(deferredEmailsQueue) } function registerCleanup(queue) { diff --git a/services/web/test/unit/src/Email/EmailHandlerTests.js b/services/web/test/unit/src/Email/EmailHandlerTests.js index 9317bd55f7..b52ac65d1e 100644 --- a/services/web/test/unit/src/Email/EmailHandlerTests.js +++ b/services/web/test/unit/src/Email/EmailHandlerTests.js @@ -20,11 +20,15 @@ describe('EmailHandler', function () { sendEmail: sinon.stub().resolves(), }, } + this.Queues = { + createScheduledJob: sinon.stub(), + } this.EmailHandler = SandboxedModule.require(MODULE_PATH, { requires: { './EmailBuilder': this.EmailBuilder, './EmailSender': this.EmailSender, '@overleaf/settings': this.Settings, + '../../infrastructure/Queues': this.Queues, }, }) }) @@ -92,4 +96,27 @@ describe('EmailHandler', function () { }) }) }) + + describe('send deferred email', function () { + beforeEach(function () { + this.opts = { + to: 'bob@bob.com', + first_name: 'hello bob', + } + this.emailType = 'canceledSubscription' + this.ONE_HOUR_IN_MS = 1000 * 60 * 60 + this.EmailHandler.sendDeferredEmail( + this.emailType, + this.opts, + this.ONE_HOUR_IN_MS + ) + }) + it('should add a email job to the queue', function () { + expect(this.Queues.createScheduledJob).to.have.been.calledWith( + 'deferred-emails', + { data: { emailType: this.emailType, opts: this.opts } }, + this.ONE_HOUR_IN_MS + ) + }) + }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js index e5281969ec..a8b1512429 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js @@ -110,7 +110,10 @@ describe('SubscriptionHandler', function () { this.LimitationsManager = { userHasV2Subscription: sinon.stub() } - this.EmailHandler = { sendEmail: sinon.stub() } + this.EmailHandler = { + sendEmail: sinon.stub(), + sendDeferredEmail: sinon.stub(), + } this.AnalyticsManager = { recordEventForUser: sinon.stub() } @@ -405,6 +408,15 @@ describe('SubscriptionHandler', function () { .calledWith(this.subscription.recurlySubscription_id) .should.equal(true) }) + + it('should send the email after 1 hour', function () { + const ONE_HOUR_IN_MS = 1000 * 60 * 60 + expect(this.EmailHandler.sendDeferredEmail).to.have.been.calledWith( + 'canceledSubscription', + { to: this.user.email, first_name: this.user.first_name }, + ONE_HOUR_IN_MS + ) + }) }) })