Эх сурвалжийг харах

Merge pull request #3492 from overleaf/jel-calc-notification-period

Flag emails in affiliation notification period

GitOrigin-RevId: d2d4868ba3a49e69b85a3bdca16d12276ac5c006
Jessica Lawshe 5 жил өмнө
parent
commit
b44aed56dd

+ 41 - 8
services/web/app/src/Features/User/UserGetter.js

@@ -2,6 +2,8 @@ const { callbackify } = require('util')
 const { db } = require('../../infrastructure/mongodb')
 const { db } = require('../../infrastructure/mongodb')
 const metrics = require('@overleaf/metrics')
 const metrics = require('@overleaf/metrics')
 const logger = require('logger-sharelatex')
 const logger = require('logger-sharelatex')
+const moment = require('moment')
+const settings = require('settings-sharelatex')
 const { promisifyAll } = require('../../util/promises')
 const { promisifyAll } = require('../../util/promises')
 const {
 const {
   promises: InstitutionsAPIPromises
   promises: InstitutionsAPIPromises
@@ -11,6 +13,35 @@ const Errors = require('../Errors/Errors')
 const Features = require('../../infrastructure/Features')
 const Features = require('../../infrastructure/Features')
 const { normalizeQuery, normalizeMultiQuery } = require('../Helpers/Mongo')
 const { normalizeQuery, normalizeMultiQuery } = require('../Helpers/Mongo')
 
 
+function _emailInReconfirmNotificationPeriod(emailData, institutionData) {
+  const globalReconfirmPeriod = settings.reconfirmNotificationDays
+  if (!globalReconfirmPeriod) return false
+
+  // only show notification for institutions with reconfirmation enabled
+  if (!institutionData || !institutionData.maxConfirmationMonths) return false
+
+  if (!emailData.confirmedAt) return false
+
+  if (institutionData.ssoEnabled && !emailData.samlProviderId) {
+    // For SSO, only show notification for linked email
+    return false
+  }
+
+  // reconfirmedAt will not always be set, use confirmedAt as fallback
+  const lastConfirmed = emailData.reconfirmedAt || emailData.confirmedAt
+
+  const lastDayToReconfirm = moment(lastConfirmed).add(
+    institutionData.maxConfirmationMonths,
+    'months'
+  )
+  const notificationStarts = moment(lastDayToReconfirm).subtract(
+    globalReconfirmPeriod,
+    'days'
+  )
+
+  return moment().isAfter(notificationStarts)
+}
+
 async function getUserFullEmails(userId) {
 async function getUserFullEmails(userId) {
   const user = await UserGetter.promises.getUser(userId, {
   const user = await UserGetter.promises.getUser(userId, {
     email: 1,
     email: 1,
@@ -155,8 +186,8 @@ var decorateFullEmails = (
   emailsData,
   emailsData,
   affiliationsData,
   affiliationsData,
   samlIdentifiers
   samlIdentifiers
-) =>
-  emailsData.map(function(emailData) {
+) => {
+  emailsData.forEach(function(emailData) {
     emailData.default = emailData.email === defaultEmail
     emailData.default = emailData.email === defaultEmail
 
 
     const affiliation = affiliationsData.find(
     const affiliation = affiliationsData.find(
@@ -164,31 +195,33 @@ var decorateFullEmails = (
     )
     )
     if (affiliation) {
     if (affiliation) {
       const { institution, inferred, role, department, licence } = affiliation
       const { institution, inferred, role, department, licence } = affiliation
+      const inReconfirmNotificationPeriod = _emailInReconfirmNotificationPeriod(
+        emailData,
+        institution
+      )
       emailData.affiliation = {
       emailData.affiliation = {
         institution,
         institution,
         inferred,
         inferred,
+        inReconfirmNotificationPeriod,
         role,
         role,
         department,
         department,
         licence
         licence
       }
       }
-    } else {
-      emailsData.affiliation = null
     }
     }
 
 
     if (emailData.samlProviderId) {
     if (emailData.samlProviderId) {
       emailData.samlIdentifier = samlIdentifiers.find(
       emailData.samlIdentifier = samlIdentifiers.find(
         samlIdentifier => samlIdentifier.providerId === emailData.samlProviderId
         samlIdentifier => samlIdentifier.providerId === emailData.samlProviderId
       )
       )
-    } else {
-      emailsData.samlIdentifier = null
     }
     }
 
 
     emailData.emailHasInstitutionLicence = InstitutionsHelper.emailHasLicence(
     emailData.emailHasInstitutionLicence = InstitutionsHelper.emailHasLicence(
       emailData
       emailData
     )
     )
-
-    return emailData
   })
   })
+
+  return emailsData
+}
 ;[
 ;[
   'getUser',
   'getUser',
   'getUserEmail',
   'getUserEmail',

+ 2 - 0
services/web/test/acceptance/config/settings.test.coffee

@@ -186,3 +186,5 @@ module.exports =
   # setting to true since many features are enabled/disabled after availability of this
   # setting to true since many features are enabled/disabled after availability of this
   # property (check Features.js)
   # property (check Features.js)
 	overleaf: true
 	overleaf: true
+
+	reconfirmNotificationDays: 14

+ 1 - 0
services/web/test/acceptance/src/SubscriptionDashboardTests.js

@@ -516,6 +516,7 @@ describe('Subscriptions', function() {
             department: 'Math',
             department: 'Math',
             role: 'Prof',
             role: 'Prof',
             inferred: false,
             inferred: false,
+            inReconfirmNotificationPeriod: false,
             institution: {
             institution: {
               name: 'Stanford',
               name: 'Stanford',
               confirmed: true
               confirmed: true

+ 160 - 30
services/web/test/acceptance/src/UserEmailsTests.js

@@ -1,39 +1,14 @@
 const { expect } = require('chai')
 const { expect } = require('chai')
 const async = require('async')
 const async = require('async')
+const moment = require('moment')
+const Features = require('../../../app/src/infrastructure/Features')
 const User = require('./helpers/User')
 const User = require('./helpers/User')
 const UserHelper = require('./helpers/UserHelper')
 const UserHelper = require('./helpers/UserHelper')
+const UserUpdater = require('../../../app/src/Features/User/UserUpdater')
 const { db, ObjectId } = require('../../../app/src/infrastructure/mongodb')
 const { db, ObjectId } = require('../../../app/src/infrastructure/mongodb')
 const MockV1Api = require('./helpers/MockV1Api')
 const MockV1Api = require('./helpers/MockV1Api')
 const expectErrorResponse = require('./helpers/expectErrorResponse')
 const expectErrorResponse = require('./helpers/expectErrorResponse')
 
 
-async function confirmEmail(userHelper, email) {
-  let response
-  // UserHelper.createUser does not create a confirmation token
-  response = await userHelper.request.post({
-    form: {
-      email
-    },
-    simple: false,
-    uri: '/user/emails/resend_confirmation'
-  })
-  expect(response.statusCode).to.equal(200)
-  const tokenData = await db.tokens
-    .find({
-      use: 'email_confirmation',
-      'data.user_id': userHelper.user._id.toString(),
-      usedAt: { $exists: false }
-    })
-    .next()
-  response = await userHelper.request.post({
-    form: {
-      token: tokenData.token
-    },
-    simple: false,
-    uri: '/user/emails/confirm'
-  })
-  expect(response.statusCode).to.equal(200)
-}
-
 describe('UserEmails', function() {
 describe('UserEmails', function() {
   beforeEach(function(done) {
   beforeEach(function(done) {
     this.timeout(20000)
     this.timeout(20000)
@@ -283,14 +258,14 @@ describe('UserEmails', function() {
         password: userHelper.getDefaultPassword()
         password: userHelper.getDefaultPassword()
       })
       })
       // original confirmation
       // original confirmation
-      await confirmEmail(userHelper, email)
+      await userHelper.confirmEmail(userHelper.user._id, email)
       const user = (await UserHelper.getUser({ email })).user
       const user = (await UserHelper.getUser({ email })).user
       confirmedAtDate = user.emails[0].confirmedAt
       confirmedAtDate = user.emails[0].confirmedAt
       expect(user.emails[0].confirmedAt).to.exist
       expect(user.emails[0].confirmedAt).to.exist
       expect(user.emails[0].reconfirmedAt).to.exist
       expect(user.emails[0].reconfirmedAt).to.exist
     })
     })
     it('should set reconfirmedAt and not reset confirmedAt', async function() {
     it('should set reconfirmedAt and not reset confirmedAt', async function() {
-      await confirmEmail(userHelper, email)
+      await userHelper.confirmEmail(userHelper.user._id, email)
       const user = (await UserHelper.getUser({ email })).user
       const user = (await UserHelper.getUser({ email })).user
       expect(user.emails[0].confirmedAt).to.exist
       expect(user.emails[0].confirmedAt).to.exist
       expect(user.emails[0].reconfirmedAt).to.exist
       expect(user.emails[0].reconfirmedAt).to.exist
@@ -1019,4 +994,159 @@ describe('UserEmails', function() {
       expect(user.auditLog[0].ip).to.equal(this.user.request.ip)
       expect(user.auditLog[0].ip).to.equal(this.user.request.ip)
     })
     })
   })
   })
+
+  describe('notification period', function() {
+    let defaultEmail, userHelper, email1, email2, email3
+    const maxConfirmationMonths = 12
+
+    beforeEach(async function() {
+      if (!Features.hasFeature('affiliations')) {
+        this.skip()
+      }
+      userHelper = new UserHelper()
+      defaultEmail = userHelper.getDefaultEmail()
+      userHelper = await UserHelper.createUser({ email: defaultEmail })
+      userHelper = await UserHelper.loginUser({
+        email: defaultEmail,
+        password: userHelper.getDefaultPassword()
+      })
+      const institutionId = MockV1Api.createInstitution({
+        ssoEnabled: false,
+        maxConfirmationMonths
+      })
+      const domain = 'example-affiliation.com'
+      MockV1Api.addInstitutionDomain(institutionId, domain)
+
+      email1 = `leonard@${domain}`
+      email2 = `mccoy@${domain}`
+      email3 = `bones@${domain}`
+    })
+
+    describe('non SSO affiliations', function() {
+      beforeEach(async function() {
+        // create a user with 3 affiliations at the institution.
+        // all are within in the notification period
+        const backdatedDays = maxConfirmationMonths * 2 * 30
+        const userId = userHelper.user._id
+        await userHelper.addEmailAndConfirm(userId, email1)
+        await userHelper.addEmailAndConfirm(userId, email2)
+        await userHelper.addEmailAndConfirm(userId, email3)
+        await userHelper.backdateConfirmation(userId, email1, backdatedDays)
+        await userHelper.backdateConfirmation(userId, email2, backdatedDays)
+        await userHelper.backdateConfirmation(userId, email3, backdatedDays)
+      })
+
+      it('should flag inReconfirmNotificationPeriod for all affiliations in period', async function() {
+        const response = await userHelper.request.get('/user/emails')
+        expect(response.statusCode).to.equal(200)
+        const fullEmails = JSON.parse(response.body)
+        expect(fullEmails.length).to.equal(4)
+        expect(fullEmails[0].affiliation).to.not.exist
+        expect(
+          fullEmails[1].affiliation.inReconfirmNotificationPeriod
+        ).to.equal(true)
+        expect(
+          fullEmails[2].affiliation.inReconfirmNotificationPeriod
+        ).to.equal(true)
+        expect(
+          fullEmails[3].affiliation.inReconfirmNotificationPeriod
+        ).to.equal(true)
+      })
+
+      describe('should flag emails before their confirmation expires, but within the notification period', function() {
+        beforeEach(async function() {
+          const dateInPeriodButNotExpired = moment()
+            .subtract(maxConfirmationMonths, 'months')
+            .add(14, 'days')
+            .toDate()
+          const backdatedDays = moment().diff(dateInPeriodButNotExpired, 'days')
+          await userHelper.backdateConfirmation(
+            userHelper.user._id,
+            email1,
+            backdatedDays
+          )
+          await userHelper.backdateConfirmation(
+            userHelper.user._id,
+            email2,
+            backdatedDays
+          )
+          await userHelper.backdateConfirmation(
+            userHelper.user._id,
+            email3,
+            backdatedDays
+          )
+        })
+
+        it('should flag the emails', async function() {
+          const response = await userHelper.request.get('/user/emails')
+          expect(response.statusCode).to.equal(200)
+          const fullEmails = JSON.parse(response.body)
+          expect(fullEmails.length).to.equal(4)
+          expect(fullEmails[0].affiliation).to.not.exist
+          expect(
+            fullEmails[1].affiliation.inReconfirmNotificationPeriod
+          ).to.equal(true)
+          expect(
+            fullEmails[2].affiliation.inReconfirmNotificationPeriod
+          ).to.equal(true)
+          expect(
+            fullEmails[3].affiliation.inReconfirmNotificationPeriod
+          ).to.equal(true)
+          // ensure dates are not past reconfirmation period
+          function _getLastDayToReconfirm(date) {
+            return moment(date).add(maxConfirmationMonths, 'months')
+          }
+
+          expect(
+            moment(fullEmails[1].reconfirmedAt).isAfter(
+              _getLastDayToReconfirm(fullEmails[1].reconfirmedAt)
+            )
+          ).to.equal(false)
+
+          expect(
+            moment(fullEmails[2].reconfirmedAt).isAfter(
+              _getLastDayToReconfirm(fullEmails[2].reconfirmedAt)
+            )
+          ).to.equal(false)
+          expect(
+            moment(fullEmails[3].reconfirmedAt).isAfter(
+              _getLastDayToReconfirm(fullEmails[3].reconfirmedAt)
+            )
+          ).to.equal(false)
+        })
+      })
+
+      describe('missing reconfirmedAt', function() {
+        beforeEach(async function() {
+          const userId = userHelper.user._id
+          const query = {
+            _id: userId,
+            'emails.email': email2
+          }
+          const update = {
+            $unset: { 'emails.$.reconfirmedAt': true }
+          }
+          await UserUpdater.promises.updateUser(query, update)
+        })
+
+        it('should fallback to confirmedAt for date check', async function() {
+          const response = await userHelper.request.get('/user/emails')
+          expect(response.statusCode).to.equal(200)
+          const fullEmails = JSON.parse(response.body)
+          expect(fullEmails.length).to.equal(4)
+          expect(fullEmails[0].affiliation).to.not.exist
+          expect(fullEmails[2].reconfirmedAt).to.not.exist
+          expect(
+            fullEmails[1].affiliation.inReconfirmNotificationPeriod
+          ).to.equal(true)
+          expect(
+            fullEmails[2].affiliation.inReconfirmNotificationPeriod
+          ).to.equal(true)
+          expect(
+            fullEmails[3].affiliation.inReconfirmNotificationPeriod
+          ).to.equal(true)
+        })
+      })
+    })
+  })
 })
 })

+ 72 - 0
services/web/test/acceptance/src/helpers/UserHelper.js

@@ -4,7 +4,9 @@ const Settings = require('settings-sharelatex')
 const UserCreator = require('../../../../app/src/Features/User/UserCreator')
 const UserCreator = require('../../../../app/src/Features/User/UserCreator')
 const UserGetter = require('../../../../app/src/Features/User/UserGetter')
 const UserGetter = require('../../../../app/src/Features/User/UserGetter')
 const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
 const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
+const moment = require('moment')
 const request = require('request-promise-native')
 const request = require('request-promise-native')
+const { db } = require('../../../../app/src/infrastructure/mongodb')
 const { ObjectId } = require('mongodb')
 const { ObjectId } = require('mongodb')
 
 
 let globalUserNum = 1
 let globalUserNum = 1
@@ -289,6 +291,76 @@ class UserHelper {
 
 
     return userHelper
     return userHelper
   }
   }
+
+  async addEmailAndConfirm(userId, email) {
+    let response = await this.request.post({
+      form: {
+        email
+      },
+      simple: false,
+      uri: '/user/emails'
+    })
+    expect(response.statusCode).to.equal(204)
+    const token = (
+      await db.tokens.findOne({
+        'data.user_id': userId.toString(),
+        'data.email': email
+      })
+    ).token
+    response = await this.request.post({
+      form: {
+        token
+      },
+      simple: false,
+      uri: '/user/emails/confirm'
+    })
+    expect(response.statusCode).to.equal(200)
+  }
+
+  async backdateConfirmation(userId, email, days) {
+    const confirmedDate = moment()
+      .subtract(days, 'days')
+      .toDate()
+    const query = {
+      _id: userId,
+      'emails.email': email
+    }
+    const update = {
+      $set: {
+        'emails.$.confirmedAt': confirmedDate,
+        'emails.$.reconfirmedAt': confirmedDate
+      }
+    }
+    await UserUpdater.promises.updateUser(query, update)
+  }
+
+  async confirmEmail(userId, email) {
+    let response
+    // UserHelper.createUser does not create a confirmation token
+    response = await this.request.post({
+      form: {
+        email
+      },
+      simple: false,
+      uri: '/user/emails/resend_confirmation'
+    })
+    expect(response.statusCode).to.equal(200)
+    const tokenData = await db.tokens
+      .find({
+        use: 'email_confirmation',
+        'data.user_id': userId.toString(),
+        usedAt: { $exists: false }
+      })
+      .next()
+    response = await this.request.post({
+      form: {
+        token: tokenData.token
+      },
+      simple: false,
+      uri: '/user/emails/confirm'
+    })
+    expect(response.statusCode).to.equal(200)
+  }
 }
 }
 
 
 module.exports = UserHelper
 module.exports = UserHelper

+ 528 - 3
services/web/test/unit/src/User/UserGetterTests.js

@@ -2,6 +2,7 @@ const { ObjectId } = require('mongodb')
 const should = require('chai').should()
 const should = require('chai').should()
 const SandboxedModule = require('sandboxed-module')
 const SandboxedModule = require('sandboxed-module')
 const assert = require('assert')
 const assert = require('assert')
+const moment = require('moment')
 const path = require('path')
 const path = require('path')
 const sinon = require('sinon')
 const sinon = require('sinon')
 const modulePath = path.join(
 const modulePath = path.join(
@@ -41,7 +42,6 @@ describe('UserGetter', function() {
       },
       },
       ObjectId
       ObjectId
     }
     }
-    const settings = { apis: { v1: { url: 'v1.url', user: '', pass: '' } } }
     this.getUserAffiliations = sinon.stub().resolves([])
     this.getUserAffiliations = sinon.stub().resolves([])
 
 
     this.UserGetter = SandboxedModule.require(modulePath, {
     this.UserGetter = SandboxedModule.require(modulePath, {
@@ -57,7 +57,9 @@ describe('UserGetter', function() {
         '@overleaf/metrics': {
         '@overleaf/metrics': {
           timeAsyncMethod: sinon.stub()
           timeAsyncMethod: sinon.stub()
         },
         },
-        'settings-sharelatex': settings,
+        'settings-sharelatex': (this.settings = {
+          reconfirmNotificationDays: 14
+        }),
         '../Institutions/InstitutionsAPI': {
         '../Institutions/InstitutionsAPI': {
           promises: {
           promises: {
             getUserAffiliations: this.getUserAffiliations
             getUserAffiliations: this.getUserAffiliations
@@ -192,7 +194,8 @@ describe('UserGetter', function() {
                 inferred: affiliationsData[0].inferred,
                 inferred: affiliationsData[0].inferred,
                 department: affiliationsData[0].department,
                 department: affiliationsData[0].department,
                 role: affiliationsData[0].role,
                 role: affiliationsData[0].role,
-                licence: affiliationsData[0].licence
+                licence: affiliationsData[0].licence,
+                inReconfirmNotificationPeriod: false
               }
               }
             },
             },
             {
             {
@@ -262,6 +265,528 @@ describe('UserGetter', function() {
         }
         }
       )
       )
     })
     })
+
+    describe('affiliation reconfirmation', function() {
+      const institutionNonSSO = {
+        id: 1,
+        name: 'University Name',
+        commonsAccount: true,
+        isUniversity: true,
+        confirmed: true,
+        ssoBeta: false,
+        ssoEnabled: false,
+        maxConfirmationMonths: 12
+      }
+      const institutionSSO = {
+        id: 2,
+        name: 'SSO University Name',
+        isUniversity: true,
+        confirmed: true,
+        ssoBeta: false,
+        ssoEnabled: true,
+        maxConfirmationMonths: 12
+      }
+      describe('non-SSO institutions', function() {
+        const email1 = 'leonard@example-affiliation.com'
+        const email2 = 'mccoy@example-affiliation.com'
+        const affiliationsData = [
+          {
+            email: email1,
+            role: 'Prof',
+            department: 'Medicine',
+            inferred: false,
+            licence: 'pro_plus',
+            institution: institutionNonSSO
+          },
+          {
+            email: email2,
+            role: 'Prof',
+            department: 'Medicine',
+            inferred: false,
+            licence: 'pro_plus',
+            institution: institutionNonSSO
+          }
+        ]
+        it('should flag inReconfirmNotificationPeriod for all affiliations in period', function(done) {
+          const user = {
+            _id: '12390i',
+            email: email1,
+            emails: [
+              {
+                email: email1,
+                reversedHostname: 'moc.noitailiffa-elpmaxe',
+                confirmedAt: moment()
+                  .subtract(
+                    institutionNonSSO.maxConfirmationMonths + 2,
+                    'months'
+                  )
+                  .toDate(),
+                default: true
+              },
+              {
+                email: email2,
+                reversedHostname: 'moc.noitailiffa-elpmaxe',
+                confirmedAt: moment()
+                  .subtract(
+                    institutionNonSSO.maxConfirmationMonths + 1,
+                    'months'
+                  )
+                  .toDate()
+              }
+            ]
+          }
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              expect(
+                fullEmails[1].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              done()
+            }
+          )
+        })
+        it('should not flag affiliations outside of notification period', function(done) {
+          const aboutToBeWithinPeriod = moment()
+            .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
+            .add(15, 'days')
+            .toDate() // expires in 15 days
+          const user = {
+            _id: '12390i',
+            email: email1,
+            emails: [
+              {
+                email: email1,
+                reversedHostname: 'moc.noitailiffa-elpmaxe',
+                confirmedAt: new Date(),
+                default: true
+              },
+              {
+                email: email2,
+                reversedHostname: 'moc.noitailiffa-elpmaxe',
+                confirmedAt: aboutToBeWithinPeriod
+              }
+            ]
+          }
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(false)
+              expect(
+                fullEmails[1].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(false)
+              done()
+            }
+          )
+        })
+      })
+
+      describe('SSO institutions', function() {
+        it('should flag only linked email, if in notification period', function(done) {
+          const email1 = 'email1@sso.bar'
+          const email2 = 'email2@sso.bar'
+          const email3 = 'email3@sso.bar'
+
+          const affiliationsData = [
+            {
+              email: email1,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionSSO
+            },
+            {
+              email: email2,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionSSO
+            },
+            {
+              email: email3,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionSSO
+            }
+          ]
+
+          const user = {
+            _id: '12390i',
+            email: email1,
+            emails: [
+              {
+                email: email1,
+                reversedHostname: 'rab.oss',
+                confirmedAt: new Date('2019-09-24'),
+                reconfirmedAt: new Date('2019-09-24'),
+                default: true
+              },
+              {
+                email: email2,
+                reversedHostname: 'rab.oss',
+                confirmedAt: new Date('2019-09-24'),
+                reconfirmedAt: new Date('2019-09-24'),
+                samlProviderId: institutionSSO.id
+              },
+              {
+                email: email3,
+                reversedHostname: 'rab.oss',
+                confirmedAt: new Date('2019-09-24'),
+                reconfirmedAt: new Date('2019-09-24')
+              }
+            ],
+            samlIdentifiers: [
+              {
+                providerId: institutionSSO.id,
+                externalUserId: 'abc123'
+              }
+            ]
+          }
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(false)
+              expect(
+                fullEmails[1].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              expect(
+                fullEmails[2].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(false)
+              done()
+            }
+          )
+        })
+      })
+
+      describe('multiple institution affiliations', function() {
+        it('should flag each institution', function(done) {
+          const email1 = 'email1@sso.bar'
+          const email2 = 'email2@sso.bar'
+          const email3 = 'email3@foo.bar'
+          const email4 = 'email4@foo.bar'
+
+          const affiliationsData = [
+            {
+              email: email1,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionSSO
+            },
+            {
+              email: email2,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionSSO
+            },
+            {
+              email: email3,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionNonSSO
+            },
+            {
+              email: email4,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionNonSSO
+            }
+          ]
+          const user = {
+            _id: '12390i',
+            email: email1,
+            emails: [
+              {
+                email: email1,
+                reversedHostname: 'rab.oss',
+                confirmedAt: '2019-09-24T20:25:08.503Z',
+                default: true
+              },
+              {
+                email: email2,
+                reversedHostname: 'rab.oss',
+                confirmedAt: new Date('2019-09-24T20:25:08.503Z'),
+                samlProviderId: institutionSSO.id
+              },
+              {
+                email: email3,
+                reversedHostname: 'rab.oof',
+                confirmedAt: new Date('2019-10-24T20:25:08.503Z')
+              },
+              {
+                email: email4,
+                reversedHostname: 'rab.oof',
+                confirmedAt: new Date('2019-09-24T20:25:08.503Z')
+              }
+            ],
+            samlIdentifiers: [
+              {
+                providerId: institutionSSO.id,
+                externalUserId: 'abc123'
+              }
+            ]
+          }
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.to.equal(false)
+              expect(
+                fullEmails[1].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              expect(
+                fullEmails[2].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              expect(
+                fullEmails[3].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              done()
+            }
+          )
+        })
+      })
+
+      describe('reconfirmedAt', function() {
+        it('only use confirmedAt when no reconfirmedAt', function(done) {
+          const email1 = 'email1@foo.bar'
+          const email2 = 'email2@foo.bar'
+          const email3 = 'email3@foo.bar'
+
+          const affiliationsData = [
+            {
+              email: email1,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionNonSSO
+            },
+            {
+              email: email2,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionNonSSO
+            },
+            {
+              email: email3,
+              role: 'Prof',
+              department: 'Maths',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionNonSSO
+            }
+          ]
+          const user = {
+            _id: '12390i',
+            email: email1,
+            emails: [
+              {
+                email: email1,
+                reversedHostname: 'rab.oof',
+                confirmedAt: moment().subtract(
+                  institutionSSO.maxConfirmationMonths * 2,
+                  'months'
+                ),
+                reconfirmedAt: moment().subtract(
+                  institutionSSO.maxConfirmationMonths * 3,
+                  'months'
+                ),
+                default: true
+              },
+              {
+                email: email2,
+                reversedHostname: 'rab.oof',
+                confirmedAt: moment().subtract(
+                  institutionSSO.maxConfirmationMonths * 3,
+                  'months'
+                ),
+                reconfirmedAt: moment().subtract(
+                  institutionSSO.maxConfirmationMonths * 2,
+                  'months'
+                )
+              },
+              {
+                email: email3,
+                reversedHostname: 'rab.oof',
+                confirmedAt: moment().subtract(
+                  institutionSSO.maxConfirmationMonths * 4,
+                  'months'
+                ),
+                reconfirmedAt: moment().subtract(
+                  institutionSSO.maxConfirmationMonths * 4,
+                  'months'
+                )
+              }
+            ]
+          }
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              expect(
+                fullEmails[1].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              expect(
+                fullEmails[2].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              done()
+            }
+          )
+        })
+      })
+
+      describe('before reconfirmation period expires and within reconfirmation notification period', function() {
+        const email = 'leonard@example-affiliation.com'
+        it('should flag the email', function(done) {
+          const confirmedAt = moment()
+            .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
+            .subtract(14, 'days')
+            .toDate() // expires in 14 days
+          const affiliationsData = [
+            {
+              email,
+              role: 'Prof',
+              department: 'Medicine',
+              inferred: false,
+              licence: 'pro_plus',
+              institution: institutionNonSSO
+            }
+          ]
+          const user = {
+            _id: '12390i',
+            email,
+            emails: [
+              {
+                email,
+                confirmedAt,
+                default: true
+              }
+            ]
+          }
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(true)
+              done()
+            }
+          )
+        })
+      })
+
+      describe('when no Settings.reconfirmNotificationDays', function() {
+        it('should always return inReconfirmNotificationPeriod:false', function(done) {
+          const email1 = 'email1@sso.bar'
+          const email2 = 'email2@foo.bar'
+          const email3 = 'email3@foo.bar'
+          const confirmedAtAboutToExpire = moment()
+            .subtract(institutionNonSSO.maxConfirmationMonths, 'months')
+            .subtract(14, 'days')
+            .toDate() // expires in 14 days
+
+          const affiliationsData = [
+            {
+              email: email1,
+              institution: institutionSSO
+            },
+            {
+              email: email2,
+              institution: institutionNonSSO
+            },
+            {
+              email: email3,
+              institution: institutionNonSSO
+            }
+          ]
+          const user = {
+            _id: '12390i',
+            email: email1,
+            emails: [
+              {
+                email: email1,
+                confirmedAt: confirmedAtAboutToExpire,
+                default: true,
+                samlProviderId: institutionSSO.id
+              },
+              {
+                email: email2,
+                confirmedAt: new Date('2019-09-24T20:25:08.503Z')
+              },
+              {
+                email: email3,
+                confirmedAt: new Date('2019-10-24T20:25:08.503Z')
+              }
+            ],
+            samlIdentifiers: [
+              {
+                providerId: institutionSSO.id,
+                externalUserId: 'abc123'
+              }
+            ]
+          }
+          this.settings.reconfirmNotificationDays = undefined
+          this.getUserAffiliations.resolves(affiliationsData)
+          this.UserGetter.promises.getUser = sinon.stub().resolves(user)
+          this.UserGetter.getUserFullEmails(
+            this.fakeUser._id,
+            (error, fullEmails) => {
+              expect(error).to.not.exist
+              expect(
+                fullEmails[0].affiliation.inReconfirmNotificationPeriod
+              ).to.to.equal(false)
+              expect(
+                fullEmails[1].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(false)
+              expect(
+                fullEmails[2].affiliation.inReconfirmNotificationPeriod
+              ).to.equal(false)
+              done()
+            }
+          )
+        })
+      })
+    })
   })
   })
 
 
   describe('getUserbyMainEmail', function() {
   describe('getUserbyMainEmail', function() {