diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js
index ad099a9522..2ad2ac6ac3 100644
--- a/services/web/app/src/Features/Email/EmailBuilder.js
+++ b/services/web/app/src/Features/Email/EmailBuilder.js
@@ -563,6 +563,46 @@ manage ${projectName} sharing settings.
}
})
+templates.userOnboardingEmail = NoCTAEmailTemplate({
+ subject(opts) {
+ return `Getting more out of ${settings.appName}`
+ },
+ greeting(opts) {
+ return ''
+ },
+ title(opts) {
+ return `Getting more out of ${settings.appName}`
+ },
+ message(opts) {
+ return `\
+Thanks for signing up for ${
+ settings.appName
+ } recently. We hope you've been finding it useful!
+Here are some key features to help you get the most out of the service:
+
+Learn LaTeX in 30 minutes: 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!
+
+Find a beautiful template: 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.
+
+Work with your collaborators: 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.
+
+If you have any questions, please let us know, and thanks again for using Overleaf.
+
+John
+
+Dr John Hammersley
+Co-founder & CEO
+www.overleaf.com
+
+ `
+ },
+ secondaryMessage() {
+ return `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 here .`
+ }
+})
+
function _formatUserNameAndEmail(user, placeholder) {
if (user.first_name && user.last_name) {
const fullName = `${user.first_name} ${user.last_name}`
diff --git a/services/web/app/src/Features/User/UserOnboardingController.js b/services/web/app/src/Features/User/UserOnboardingController.js
new file mode 100644
index 0000000000..4fd1245a1f
--- /dev/null
+++ b/services/web/app/src/Features/User/UserOnboardingController.js
@@ -0,0 +1,56 @@
+const { db, ObjectId } = require('../../infrastructure/mongojs')
+const UserUpdater = require('./UserUpdater')
+const EmailHandler = require('../Email/EmailHandler')
+const logger = require('logger-sharelatex')
+const async = require('async')
+const _ = require('underscore')
+
+module.exports = {
+ sendRecentSignupOnboardingEmails(req, res, next) {
+ // find all the users with no onboardingEmailSentAt and
+ // have signed up in the last 7 days
+ db.users.find(
+ {
+ onboardingEmailSentAt: null,
+ _id: {
+ $gt: ObjectId.createFromTime(Date.now() / 1000 - 7 * 24 * 60 * 60)
+ }
+ },
+ { email: 1 },
+ function(error, users) {
+ if (error) {
+ return next(error)
+ }
+ const ids = _.map(users, function(user) {
+ return user._id
+ })
+ logger.log('SENDING USER ONBOARDING EMAILS TO: ', ids)
+ async.mapLimit(users, 10, sendOne, function(error) {
+ if (error) {
+ return next(error)
+ }
+ logger.log('DONE SENDING ONBOARDING EMAILS')
+ res.send(ids)
+ })
+ }
+ )
+ }
+}
+
+function sendOne(user, callback) {
+ var opts = {
+ to: user.email
+ }
+ EmailHandler.sendEmail('userOnboardingEmail', opts, function(error) {
+ if (error) {
+ return callback(error)
+ }
+ UserUpdater.updateUser(
+ user._id,
+ { $set: { onboardingEmailSentAt: new Date() } },
+ function() {
+ callback()
+ }
+ )
+ })
+}
diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js
index 834eaaacb2..0de0cc8621 100644
--- a/services/web/app/src/models/User.js
+++ b/services/web/app/src/models/User.js
@@ -149,7 +149,8 @@ const UserSchema = new Schema({
createdAt: { type: Date },
enrolledAt: { type: Date },
secret: { type: String }
- }
+ },
+ onboardingEmailSentAt: { type: Date }
})
exports.User = mongoose.model('User', UserSchema)
diff --git a/services/web/app/src/router.js b/services/web/app/src/router.js
index 41020d748d..49e9b44943 100644
--- a/services/web/app/src/router.js
+++ b/services/web/app/src/router.js
@@ -18,6 +18,7 @@ const UserInfoController = require('./Features/User/UserInfoController')
const UserController = require('./Features/User/UserController')
const UserEmailsController = require('./Features/User/UserEmailsController')
const UserPagesController = require('./Features/User/UserPagesController')
+const UserOnboardingController = require('./Features/User/UserOnboardingController')
const DocumentController = require('./Features/Documents/DocumentController')
const CompileManager = require('./Features/Compile/CompileManager')
const CompileController = require('./Features/Compile/CompileController')
@@ -240,6 +241,12 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
UserInfoController.getPersonalInfo
)
+ privateApiRouter.post(
+ '/user/onboarding_emails',
+ AuthenticationController.httpAuth,
+ UserOnboardingController.sendRecentSignupOnboardingEmails
+ )
+
webRouter.get(
'/user/reconfirm',
UserPagesController.renderReconfirmAccountPage
diff --git a/services/web/test/acceptance/src/UserOnboardingTests.js b/services/web/test/acceptance/src/UserOnboardingTests.js
new file mode 100644
index 0000000000..40218ab7a7
--- /dev/null
+++ b/services/web/test/acceptance/src/UserOnboardingTests.js
@@ -0,0 +1,66 @@
+const { expect } = require('chai')
+const async = require('async')
+const User = require('./helpers/User')
+const request = require('./helpers/request')
+const { db, ObjectId } = require('../../../app/src/infrastructure/mongojs')
+const _ = require('underscore')
+
+describe('UserOnboardingTests', function() {
+ beforeEach(function(done) {
+ // 2 new users
+ this.user1 = new User()
+ this.user2 = new User()
+ // 1 older
+ this.user3 = new User()
+ this.user3._id = ObjectId('5d15fca20000000000000000')
+ async.series(
+ [
+ cb => db.users.insert(this.user3, cb),
+ this.user1.ensureUserExists.bind(this.user1),
+ this.user2.ensureUserExists.bind(this.user2)
+ ],
+ done
+ )
+ })
+
+ it('should send emails to the new users only', function(done) {
+ request(
+ {
+ method: 'POST',
+ url: '/user/onboarding_emails',
+ auth: {
+ username: 'sharelatex',
+ password: 'password',
+ sendImmediately: true
+ }
+ },
+ (error, response, body) => {
+ if (error != null) {
+ throw error
+ }
+ // should have sent two emails to new users
+ expect(response.statusCode).to.equal(200)
+ expect(response.body).to.include(this.user1._id)
+ expect(response.body).to.include(this.user2._id)
+ expect(response.body).to.not.include(this.user3._id)
+
+ // user 3 should still not have had an email sent
+ const user3 = this.user3
+ db.users.find(
+ {
+ onboardingEmailSentAt: null
+ },
+ (error, users) => {
+ if (error != null) {
+ throw error
+ }
+ const ids = _.map(users, user => user._id.toString())
+ expect(ids.length).to.equal(1)
+ expect(ids).to.include(user3._id.toString())
+ done()
+ }
+ )
+ }
+ )
+ })
+})
diff --git a/services/web/test/unit/src/User/UserOnboardingControllerTests.js b/services/web/test/unit/src/User/UserOnboardingControllerTests.js
new file mode 100644
index 0000000000..89ad150694
--- /dev/null
+++ b/services/web/test/unit/src/User/UserOnboardingControllerTests.js
@@ -0,0 +1,76 @@
+const SandboxedModule = require('sandboxed-module')
+const modulePath =
+ '../../../../app/src/Features/User/UserOnboardingController.js'
+const { ObjectId } = require('mongojs')
+const sinon = require('sinon')
+
+describe('UserOnboardingController', function() {
+ beforeEach(function() {
+ this.date = new Date().getTime()
+ sinon.useFakeTimers(this.date)
+
+ this.users = [
+ {
+ _id: ObjectId('00000001f037be01a0e3a541')
+ },
+ {
+ _id: ObjectId('00000001f037be01a0e3a542')
+ },
+ {
+ _id: ObjectId('00000001f037be01a0e3a543')
+ }
+ ]
+
+ this.mongojs = {
+ db: { users: { find: sinon.stub().callsArgWith(2, null, this.users) } },
+ ObjectId: ObjectId
+ }
+
+ this.logger = {
+ log() {}
+ }
+
+ this.UserUpdater = {
+ updateUser: sinon.stub().callsArgWith(2, null)
+ }
+
+ this.EmailHandler = {
+ sendEmail: sinon.stub().callsArgWith(2)
+ }
+
+ this.UserOnboardingController = SandboxedModule.require(modulePath, {
+ requires: {
+ '../../infrastructure/mongojs': this.mongojs,
+ './UserUpdater': this.UserUpdater,
+ '../Email/EmailHandler': this.EmailHandler,
+ 'logger-sharelatex': this.logger
+ }
+ })
+ this.req = {}
+ this.res = {}
+ })
+
+ it('sends onboarding emails', function(done) {
+ this.res.send = ids => {
+ ids.length.should.equal(3)
+ this.mongojs.db.users.find.calledOnce.should.equal(true)
+ this.EmailHandler.sendEmail.calledThrice.should.equal(true)
+ this.UserUpdater.updateUser.calledThrice.should.equal(true)
+ for (var i = 0; i < 3; i++) {
+ this.UserUpdater.updateUser
+ .calledWith(
+ this.users[0]._id,
+ sinon.match({
+ $set: { onboardingEmailSentAt: new Date(this.date) }
+ })
+ )
+ .should.equal(true)
+ }
+ done()
+ }
+ this.UserOnboardingController.sendRecentSignupOnboardingEmails(
+ this.req,
+ this.res
+ )
+ })
+})