diff --git a/packages/component-email/src/routes/emails/helpers.js b/packages/component-email/src/routes/emails/helpers.js
index 8f484b0a50207f09e5f8bbb4810ff23516bf40b2..35395e3da3bb1fac36c7d2006ca49122f955cb3e 100644
--- a/packages/component-email/src/routes/emails/helpers.js
+++ b/packages/component-email/src/routes/emails/helpers.js
@@ -4,19 +4,13 @@ const { services } = require('pubsweet-component-helper-service')
 const { getEmailCopy } = require('./emailCopy')
 
 const confirmSignUp = config.get('confirm-signup.url')
-const { name: journalName, staffEmail } = config.get('journal')
 
 module.exports = {
   sendNewUserEmail: ({ email, role }) => {
     email.content.subject = 'Confirm your account'
 
-    let emailType
-    if (role === 'Handling Editor') {
-      emailType = 'he-added-by-admin'
-      email.fromEmail = `${journalName} <${staffEmail}>`
-    } else {
-      emailType = 'user-added-by-admin'
-    }
+    const emailType =
+      role === 'Handling Editor' ? 'he-added-by-admin' : 'user-added-by-admin'
 
     const { html, text } = email.getNotificationBody({
       emailBodyProps: getEmailCopy({
diff --git a/packages/component-email/src/routes/emails/notifications.js b/packages/component-email/src/routes/emails/notifications.js
index 84a1b0a863d3e015707391a99062896268dc4dd7..5fe6fc1183bef9386e5b8f811a0ee6084e1387b3 100644
--- a/packages/component-email/src/routes/emails/notifications.js
+++ b/packages/component-email/src/routes/emails/notifications.js
@@ -2,7 +2,7 @@ const config = require('config')
 
 const unsubscribeSlug = config.get('unsubscribe.url')
 const resetPath = config.get('invite-reset-password.url')
-const staffEmail = config.get('journal.staffEmail')
+const { staffEmail, name: journalName } = config.get('journal')
 const Email = require('@pubsweet/component-email-templating')
 const { services } = require('pubsweet-component-helper-service')
 
@@ -12,7 +12,7 @@ module.exports = {
   async sendNotifications({ user, baseUrl, role }) {
     const email = new Email({
       type: 'user',
-      fromEmail: `Hindawi <${staffEmail}>`,
+      fromEmail: `${journalName} <${staffEmail}>`,
       toUser: {
         email: user.email,
         name: `${user.lastName}`,
diff --git a/packages/component-faraday-ui/src/PublonsTable.js b/packages/component-faraday-ui/src/PublonsTable.js
index 4b7a53511d9312f03b61f77d11b52a83e8f5b85d..ce9f549473a84b3e60bda962f46054e93cefd1ef 100644
--- a/packages/component-faraday-ui/src/PublonsTable.js
+++ b/packages/component-faraday-ui/src/PublonsTable.js
@@ -58,8 +58,8 @@ const TableView = ({
             <HiddenCell>
               <OpenModal
                 confirmText="Invite"
-                isFetching={isFetching}
                 onConfirm={modalProps => onInviteReviewer(reviewer, modalProps)}
+                publonsFetching={isFetching}
                 setFetching={setFetching}
                 title="Send invitation to review?"
               >
diff --git a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js
index 1bd5ca285bd9e16c2af76402abe6689c3bf2738a..105b76fc04c6a4046c794398157ee770135ad538 100644
--- a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js
+++ b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js
@@ -18,7 +18,8 @@ const SubmittedReportsNumberForAuthorReviews = ({ reports }) => (
 const AuthorReviews = ({ invitations, journal, reports, fragment }) =>
   reports.length > 0 && (
     <ContextualBox
-      label="Review Reports"
+      label="Reviewer Reports"
+      mb={2}
       rightChildren={
         <SubmittedReportsNumberForAuthorReviews reports={reports.length} />
       }
diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js
index 40affb63b76e5970b505ea5300633084b931d141..d4a4310b2e39b974037596a603de4ecfac7e7354 100644
--- a/packages/component-helper-service/src/services/Collection.js
+++ b/packages/component-helper-service/src/services/Collection.js
@@ -68,9 +68,10 @@ class Collection {
     handlingEditor.hasAnswer = true
     handlingEditor.isAccepted = isAccepted
     handlingEditor.respondedOn = Date.now()
-    let status
-    isAccepted ? (status = 'heAssigned') : (status = 'submitted')
-    await this.updateStatus({ newStatus: status })
+
+    const newStatus = isAccepted ? 'heAssigned' : 'submitted'
+
+    await this.updateStatus({ newStatus })
   }
 
   async updateStatusByNumberOfReviewers({ invitations }) {
diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js b/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js
index da8d7c94bb43f5ca550cb12e7f609468aed6c190..11a81820196eb1e7d4ac0048892def94fa91b031 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js
@@ -4,7 +4,6 @@ const { services } = require('pubsweet-component-helper-service')
 const { getEmailCopy } = require('./emailCopy')
 
 const unsubscribeSlug = config.get('unsubscribe.url')
-const { name: journalName, staffEmail } = config.get('journal')
 
 module.exports = {
   sendInvitedHEEmail: ({
@@ -24,7 +23,6 @@ module.exports = {
       ? `${customId}: Editor invitation cancelled`
       : `${customId}: Invitation to edit a manuscript`
 
-    email.fromEmail = `${journalName} <${staffEmail}>`
     email.content.unsubscribeLink = services.createUrl(
       baseUrl,
       unsubscribeSlug,
diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js
index 2cf01e436af71c6273d998a741e08205e40862c0..65e1375189adc5fc06576082ad8c8b206ae35dc0 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js
@@ -1,6 +1,9 @@
+const config = require('config')
 const { last } = require('lodash')
 const Email = require('@pubsweet/component-email-templating')
 
+const { name: journalName, staffEmail } = config.get('journal')
+
 const {
   User,
   services,
@@ -40,6 +43,7 @@ module.exports = {
 
     const email = new Email({
       type: 'user',
+      fromEmail: `${journalName} <${staffEmail}>`,
       content: {
         signatureName: eicName,
         ctaLink: services.createUrl(
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js b/packages/component-manuscript-manager/src/notifications/emailCopy.js
similarity index 90%
rename from packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js
rename to packages/component-manuscript-manager/src/notifications/emailCopy.js
index 8fadac625272f7f5a3d200d11da0af3e5a1d1270..3aa154fcdd356c251d191cf3d38b02778e1379cf 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js
+++ b/packages/component-manuscript-manager/src/notifications/emailCopy.js
@@ -1,4 +1,5 @@
 const config = require('config')
+const { upperFirst } = require('lodash')
 
 const staffEmail = config.get('journal.staffEmail')
 const journalName = config.get('journal.name')
@@ -42,6 +43,8 @@ const getEmailCopy = ({
         Thank you for handling this manuscript on behalf of ${journalName}.`
       break
     case 'he-manuscript-published':
+      hasIntro = false
+      hasSignature = false
       paragraph = `${targetUserName} has confirmed your decision to accept ${titleText}.<br/><br/>
         No further action is required at this time. To review this decision, please visit the manuscript details.<br/><br/>
         Thank you for handling this manuscript on behalf of ${journalName}.`
@@ -106,7 +109,8 @@ const getEmailCopy = ({
       hasLink = false
       hasIntro = false
       hasSignature = false
-      paragraph = `${titleText} has been accepted for publication by ${eicName}. <br/><br/>
+      paragraph = `${upperFirst(titleText)} has been accepted
+        for publication by ${eicName}.<br/><br/>
         Please complete QA screening so that manuscript can be sent to production.`
       break
     case 'authors-manuscript-rejected-before-review':
@@ -115,6 +119,18 @@ const getEmailCopy = ({
         Thank you for your submission, and please do consider submitting again in the future.`
       hasLink = false
       break
+    case 'eic-manuscript-accepted-by-eqs':
+      hasIntro = false
+      hasSignature = false
+      paragraph = `A new ${titleText} has been submitted to ${journalName}.<br/><br/>
+        To begin the review process, please visit the manuscript details page.`
+      break
+    case 'eic-manuscript-returned-by-eqa':
+      hasIntro = false
+      hasSignature = false
+      paragraph = `We regret to inform you that ${titleText} has been returned with comments. Please click the link below to access the manuscript.<br/><br/>
+        Comments: ${comments}<br/><br/>`
+      break
     default:
       throw new Error(`The ${emailType} email type is not defined.`)
   }
diff --git a/packages/component-manuscript-manager/src/notifications/helpers.js b/packages/component-manuscript-manager/src/notifications/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb1302a2fb093c93316475c47e9532093ae9cde0
--- /dev/null
+++ b/packages/component-manuscript-manager/src/notifications/helpers.js
@@ -0,0 +1,44 @@
+const { get } = require('lodash')
+
+module.exports = {
+  getPrivateNoteTextForAuthor: ({ newRecommendation }) => {
+    const authorNote = newRecommendation.comments.find(comm => comm.public)
+    const content = get(authorNote, 'content')
+
+    return content ? `Reason & Details: "${content}"` : ''
+  },
+  getHEComments: ({ heRecommendation }) => {
+    const heComments = get(heRecommendation, 'comments', [])
+
+    if (heComments.length === 0) return
+
+    const publicComment = heComments.find(comm => comm.public)
+
+    const content = get(publicComment, 'content')
+    if (!content) {
+      throw new Error('a public comment cannot be without content')
+    }
+
+    return `Please find our editorial comments below.<br/> "${content}"`
+  },
+  getEmailTypeByRecommendationForAuthors: ({
+    recommendation,
+    hasPeerReview,
+  }) => {
+    let emailType
+    switch (recommendation) {
+      case 'publish':
+        emailType = 'author-manuscript-published'
+        break
+      case 'reject':
+        emailType = hasPeerReview
+          ? 'author-manuscript-rejected'
+          : 'authors-manuscript-rejected-before-review'
+        break
+      default:
+        throw new Error(`Undefined recommendation: ${recommendation}`)
+    }
+
+    return emailType
+  },
+}
diff --git a/packages/component-manuscript-manager/src/notifications/notification.js b/packages/component-manuscript-manager/src/notifications/notification.js
new file mode 100644
index 0000000000000000000000000000000000000000..dfb831c3585207a0aacbb7b0f20f4e8f5a20e4eb
--- /dev/null
+++ b/packages/component-manuscript-manager/src/notifications/notification.js
@@ -0,0 +1,652 @@
+const config = require('config')
+const { get, isEmpty, chain } = require('lodash')
+const Email = require('@pubsweet/component-email-templating')
+const {
+  User,
+  services,
+  Fragment,
+  Collection,
+} = require('pubsweet-component-helper-service')
+
+const helpers = require('./helpers')
+const { getEmailCopy } = require('./emailCopy')
+
+const { name: journalName, staffEmail } = config.get('journal')
+const unsubscribeSlug = config.get('unsubscribe.url')
+
+class Notification {
+  constructor({
+    baseUrl = '',
+    fragment = {},
+    UserModel = {},
+    collection = {},
+    newRecommendation = {},
+  }) {
+    this.baseUrl = baseUrl
+    this.fragment = fragment
+    this.UserModel = UserModel
+    this.collection = collection
+    this.newRecommendation = newRecommendation
+  }
+
+  async notifyHEWhenReviewerSubmitsReport(reviewerLastName) {
+    const { eicName, titleText } = await this.getNotificationProperties()
+
+    const handlingEditorId = get(this.collection, 'handlingEditor.id')
+    const heUser = await this.UserModel.find(handlingEditorId)
+
+    const email = new Email({
+      type: 'user',
+      toUser: {
+        email: heUser.email,
+        name: heUser.lastName,
+      },
+      fromEmail: `${eicName} <${staffEmail}>`,
+      content: {
+        signatureName: eicName,
+        ctaText: 'MANUSCRIPT DETAILS',
+        subject: `${this.collection.customId}: A review has been submitted`,
+        unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+          id: heUser.id,
+          token: heUser.accessTokens.unsubscribe,
+        }),
+        ctaLink: services.createUrl(
+          this.baseUrl,
+          `/projects/${this.collection.id}/versions/${
+            this.fragment.id
+          }/details`,
+        ),
+      },
+    })
+
+    const emailBodyProps = getEmailCopy({
+      emailType: 'he-review-submitted',
+      titleText,
+      targetUserName: reviewerLastName,
+    })
+
+    const { html, text } = email.getNotificationBody({ emailBodyProps })
+    email.sendEmail({ html, text })
+  }
+
+  // EiC decided to either publish or reject the manuscript
+  async notifyHEWhenEiCMakesDecision() {
+    const {
+      eicName,
+      titleText,
+      recommendation,
+    } = await this.getNotificationProperties()
+
+    const handlingEditorId = get(this.collection, 'handlingEditor.id')
+    const heUser = await this.UserModel.find(handlingEditorId)
+
+    const email = new Email({
+      type: 'user',
+      toUser: {
+        email: heUser.email,
+      },
+      fromEmail: `${journalName} <${staffEmail}>`,
+      content: {
+        ctaText: 'MANUSCRIPT DETAILS',
+        subject: `${this.collection.customId}: Editorial decision confirmed`,
+        unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+          id: heUser.id,
+          token: heUser.accessTokens.unsubscribe,
+        }),
+        ctaLink: services.createUrl(
+          this.baseUrl,
+          `/projects/${this.collection.id}/versions/${
+            this.fragment.id
+          }/details`,
+        ),
+      },
+    })
+
+    const emailBodyProps = getEmailCopy({
+      titleText,
+      targetUserName: eicName,
+      emailType:
+        recommendation === 'publish'
+          ? 'he-manuscript-published'
+          : 'he-manuscript-rejected',
+    })
+
+    const { html, text } = email.getNotificationBody({ emailBodyProps })
+    email.sendEmail({ html, text })
+  }
+
+  async notifyHEWhenEiCReturnsToHE() {
+    const { eicName, titleText } = await this.getNotificationProperties()
+
+    const handlingEditorId = get(this.collection, 'handlingEditor.id')
+    const heUser = await this.UserModel.find(handlingEditorId)
+
+    const email = new Email({
+      type: 'user',
+      toUser: {
+        email: heUser.email,
+        name: heUser.lastName,
+      },
+      fromEmail: `${eicName} <${staffEmail}>`,
+      content: {
+        subject: `${
+          this.collection.customId
+        }: Editorial decision returned with comments`,
+        signatureName: eicName,
+        ctaText: 'MANUSCRIPT DETAILS',
+        unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+          id: heUser.id,
+          token: heUser.accessTokens.unsubscribe,
+        }),
+        ctaLink: services.createUrl(
+          this.baseUrl,
+          `/projects/${this.collection.id}/versions/${
+            this.fragment.id
+          }/details`,
+        ),
+      },
+    })
+
+    const eicComments = chain(this.newRecommendation)
+      .get('comments')
+      .find(comm => !comm.public)
+      .get('content')
+      .value()
+
+    const emailBodyProps = getEmailCopy({
+      titleText,
+      comments: eicComments,
+      targetUserName: eicName,
+      emailType: 'he-manuscript-return-with-comments',
+    })
+
+    const { html, text } = email.getNotificationBody({ emailBodyProps })
+    email.sendEmail({ html, text })
+  }
+
+  async notifyEAWhenEiCMakesFinalDecision() {
+    const { eicName, titleText } = await this.getNotificationProperties()
+    const subjectBaseText = `${this.collection.customId}: Manuscript`
+
+    const email = new Email({
+      type: 'system',
+      toUser: {
+        email: staffEmail,
+      },
+      fromEmail: `${journalName} <${staffEmail}>`,
+      content: {
+        subject: `${subjectBaseText} decision finalized`,
+        unsubscribeLink: this.baseUrl,
+      },
+    })
+
+    const emailBodyProps = getEmailCopy({
+      eicName,
+      titleText,
+      emailType: 'eqa-manuscript-published',
+    })
+    const { html, text } = email.getNotificationBody({ emailBodyProps })
+    email.sendEmail({ html, text })
+  }
+
+  async notifyEAWhenEiCRequestsEQAApproval() {
+    const { eicName } = await this.getNotificationProperties()
+    const subjectBaseText = `${this.collection.customId}: Manuscript`
+
+    const email = new Email({
+      type: 'system',
+      toUser: {
+        email: staffEmail,
+      },
+      fromEmail: `${journalName} <${staffEmail}>`,
+      content: {
+        subject: `${subjectBaseText} Request for EQA approval`,
+        unsubscribeLink: this.baseUrl,
+        ctaText: 'MAKE DECISION',
+        ctaLink: services.createUrl(
+          this.baseUrl,
+          config.get('eqa-decision.url'),
+          {
+            collectionId: this.collection.id,
+            customId: this.collection.customId,
+            token: this.collection.technicalChecks.token,
+          },
+        ),
+      },
+    })
+
+    const emailBodyProps = getEmailCopy({
+      eicName,
+      customId: this.collection.customId,
+      emailType: 'eqa-manuscript-request-for-approval',
+    })
+
+    const { html, text } = email.getNotificationBody({ emailBodyProps })
+    email.sendEmail({ html, text })
+  }
+
+  async notifyAuthorsWhenEiCMakesDecision() {
+    const {
+      eicName,
+      titleText,
+      activeAuthors,
+      recommendation,
+      parsedFragment,
+    } = await this.getNotificationProperties()
+
+    const subjectOpts = {
+      publish: `${this.collection.customId}: Manuscript accepted`,
+      reject: `${this.collection.customId}: Manuscript rejected`,
+    }
+    const subject = subjectOpts[recommendation]
+
+    if (isEmpty(subject)) {
+      throw new Error(`Undefined recommendation: ${recommendation}`)
+    }
+
+    const hasPeerReview = !isEmpty(this.collection.handlingEditor)
+    const emailType = helpers.getEmailTypeByRecommendationForAuthors({
+      recommendation,
+      hasPeerReview,
+    })
+    const comments = hasPeerReview
+      ? helpers.getHEComments({
+          heRecommendation: parsedFragment.heRecommendation,
+        })
+      : this.newRecommendation.comments[0].content
+
+    activeAuthors.forEach(async author => {
+      const email = new Email({
+        type: 'user',
+        toUser: {
+          email: author.email,
+          name: author.lastName,
+        },
+        fromEmail: `${eicName} <${staffEmail}>`,
+        content: {
+          signatureName: eicName,
+          ctaText: 'MANUSCRIPT DETAILS',
+          subject,
+          unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+            id: author.id,
+            token: author.accessTokens.unsubscribe,
+          }),
+          ctaLink: services.createUrl(
+            this.baseUrl,
+            `/projects/${this.collection.id}/versions/${
+              this.fragment.id
+            }/details`,
+          ),
+          signatureJournal: journalName,
+        },
+      })
+
+      const emailBodyProps = getEmailCopy({
+        titleText,
+        emailType,
+        comments,
+      })
+
+      const { html, text } = email.getNotificationBody({ emailBodyProps })
+      email.sendEmail({ html, text })
+    })
+  }
+
+  async notifyReviewersWhenEiCMakesDecision() {
+    const {
+      eicName,
+      titleText,
+      recommendation,
+      fragmentHelper,
+    } = await this.getNotificationProperties()
+
+    const emailType =
+      recommendation === 'publish'
+        ? 'submitted-reviewers-after-publish'
+        : 'submitted-reviewers-after-reject'
+
+    const subject =
+      recommendation === 'publish'
+        ? 'A manuscript you reviewed has been accepted'
+        : 'A manuscript you reviewed has been rejected'
+
+    const reviewers = await fragmentHelper.getReviewers({
+      UserModel: this.UserModel,
+      type: 'submitted',
+    })
+
+    const emailBodyProps = getEmailCopy({
+      emailType,
+      titleText,
+    })
+
+    reviewers.forEach(reviewer => {
+      const email = new Email({
+        type: 'user',
+        toUser: {
+          email: reviewer.email,
+          name: reviewer.lastName,
+        },
+        fromEmail: `${eicName} <${staffEmail}>`,
+        content: {
+          signatureName: eicName,
+          ctaText: 'MANUSCRIPT DETAILS',
+          subject: `${this.collection.customId}: ${subject}`,
+          unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+            id: reviewer.id,
+            token: reviewer.accessTokens.unsubscribe,
+          }),
+          ctaLink: services.createUrl(
+            this.baseUrl,
+            `/projects/${this.collection.id}/versions/${
+              this.fragment.id
+            }/details`,
+          ),
+          signatureJournal: journalName,
+        },
+      })
+
+      const { html, text } = email.getNotificationBody({ emailBodyProps })
+      email.sendEmail({ html, text })
+    })
+  }
+
+  async notifySAWhenHERequestsRevision() {
+    const {
+      eicName,
+      submittingAuthor,
+      parsedFragment: { title },
+    } = await this.getNotificationProperties()
+
+    const authorNoteText = helpers.getPrivateNoteTextForAuthor({
+      newRecommendation: this.newRecommendation,
+    })
+
+    const signatureName = get(this.collection, 'handlingEditor.name', eicName)
+
+    const email = new Email({
+      type: 'user',
+      toUser: {
+        email: submittingAuthor.email,
+        name: submittingAuthor.lastName,
+      },
+      fromEmail: `${signatureName} <${staffEmail}>`,
+      content: {
+        signatureName,
+        ctaText: 'MANUSCRIPT DETAILS',
+        signatureJournal: journalName,
+        subject: `${this.collection.customId}: Revision requested`,
+        unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+          id: submittingAuthor.id,
+          token: submittingAuthor.accessTokens.unsubscribe,
+        }),
+        ctaLink: services.createUrl(
+          this.baseUrl,
+          `/projects/${this.collection.id}/versions/${
+            this.fragment.id
+          }/details`,
+        ),
+      },
+    })
+
+    const emailBodyProps = getEmailCopy({
+      emailType: 'author-request-to-revision',
+      titleText: `your submission "${title}" to ${journalName}`,
+      comments: authorNoteText,
+    })
+
+    const { html, text } = email.getNotificationBody({ emailBodyProps })
+    email.sendEmail({ html, text })
+  }
+
+  async notifyReviewersWhenHEMakesRecommendation() {
+    const {
+      eicName,
+      titleText,
+      fragmentHelper,
+    } = await this.getNotificationProperties()
+
+    const signatureName = get(this.collection, 'handlingEditor.name', eicName)
+
+    const acceptedReviewers = await fragmentHelper.getReviewers({
+      UserModel: this.UserModel,
+      type: 'accepted',
+    })
+    const acceptedReviewersEmailBody = getEmailCopy({
+      emailType: 'accepted-reviewers-after-recommendation',
+      titleText,
+    })
+
+    const pendingReviewers = await fragmentHelper.getReviewers({
+      UserModel: this.UserModel,
+      type: 'pending',
+    })
+    const pendingReviewersEmailBody = getEmailCopy({
+      emailType: 'pending-reviewers-after-recommendation',
+      titleText,
+    })
+
+    const buildSendEmailFunction = emailBodyProps => reviewer => {
+      const email = new Email({
+        type: 'user',
+        toUser: {
+          email: reviewer.email,
+          name: reviewer.lastName,
+        },
+        fromEmail: `${signatureName} <${staffEmail}>`,
+        content: {
+          signatureName,
+          ctaText: 'MANUSCRIPT DETAILS',
+          subject: `${this.collection.customId}: Review no longer required`,
+          unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, {
+            id: reviewer.id,
+            token: reviewer.accessTokens.unsubscribe,
+          }),
+          ctaLink: services.createUrl(
+            this.baseUrl,
+            `/projects/${this.collection.id}/versions/${
+              this.fragment.id
+            }/details`,
+          ),
+          signatureJournal: journalName,
+        },
+      })
+
+      const { html, text } = email.getNotificationBody({ emailBodyProps })
+      email.sendEmail({ html, text })
+    }
+
+    acceptedReviewers.forEach(
+      buildSendEmailFunction(acceptedReviewersEmailBody),
+    )
+    pendingReviewers.forEach(buildSendEmailFunction(pendingReviewersEmailBody))
+  }
+
+  async notifyEiCWhenHEMakesRecommendation() {
+    const {
+      eicName,
+      titleText,
+      recommendation,
+    } = await this.getNotificationProperties()
+
+    let emailType, subject
+    switch (recommendation) {
+      case 'minor':
+      case 'major':
+        emailType = 'eic-request-revision-from-he'
+        subject = `${this.collection.customId}: Revision requested`
+        break
+      case 'publish':
+        emailType = 'eic-recommend-to-publish-from-he'
+        subject = `${this.collection.customId}: Recommendation to publish`
+        break
+      case 'reject':
+        emailType = 'eic-recommend-to-reject-from-he'
+        subject = `${this.collection.customId}: Recommendation to reject`
+        break
+      default:
+        throw new Error(`undefined recommendation: ${recommendation} `)
+    }
+
+    const privateNote = this.newRecommendation.comments.find(
+      comm => !comm.public,
+    )
+    const content = get(privateNote, 'content')
+    const comments = content
+      ? `The editor provided the following comments: "${content}"`
+      : ''
+
+    const collHelper = new Collection({ collection: this.collection })
+    const targetUserName = collHelper.getHELastName()
+
+    const userHelper = new User({ UserModel: this.UserModel })
+    const editors = await userHelper.getEditorsInChief()
+
+    const emailBodyProps = getEmailCopy({
+      comments,
+      emailType,
+      titleText,
+      targetUserName,
+    })
+
+    editors.forEach(eic => {
+      const email = new Email({
+        type: 'user',
+        toUser: {
+          email: eic.email,
+          name: `${eic.firstName} ${eic.lastName}`,
+        },
+        fromEmail: `${journalName} <${staffEmail}>`,
+        content: {
+          signatureName: eicName,
+          subject,
+          ctaText: 'MANUSCRIPT DETAILS',
+          unsubscribeLink: this.baseUrl,
+          ctaLink: services.createUrl(
+            this.baseUrl,
+            `/projects/${this.collection.id}/versions/${
+              this.fragment.id
+            }/details`,
+          ),
+        },
+      })
+
+      const { html, text } = email.getNotificationBody({ emailBodyProps })
+      email.sendEmail({ html, text })
+    })
+  }
+
+  async getNotificationProperties() {
+    const fragmentHelper = new Fragment({ fragment: this.fragment })
+    const parsedFragment = await fragmentHelper.getFragmentData({
+      handlingEditor: this.collection.handlingEditor,
+    })
+    const {
+      submittingAuthor,
+      activeAuthors,
+    } = await fragmentHelper.getAuthorData({
+      UserModel: this.UserModel,
+    })
+
+    const userHelper = new User({ UserModel: this.UserModel })
+    const eicName = await userHelper.getEiCName()
+
+    const titleText = `the manuscript titled "${parsedFragment.title}" by ${
+      submittingAuthor.firstName
+    } ${submittingAuthor.lastName}`
+
+    const { recommendation, recommendationType } = this.newRecommendation
+
+    return {
+      recommendation,
+      recommendationType,
+      eicName,
+      titleText,
+      submittingAuthor,
+      activeAuthors,
+      parsedFragment,
+      fragmentHelper,
+    }
+  }
+
+  async notifyEiCWhenEQSAcceptsManuscript() {
+    const { submittingAuthor } = await this.getNotificationProperties()
+
+    const userHelper = new User({ UserModel: this.UserModel })
+    const editors = await userHelper.getEditorsInChief()
+    const fragmentHelper = new Fragment({ fragment: this.fragment })
+    const parsedFragment = await fragmentHelper.getFragmentData({})
+
+    const emailBodyProps = getEmailCopy({
+      emailType: 'eic-manuscript-accepted-by-eqs',
+      titleText: `manuscript titled "${parsedFragment.title}" by ${
+        submittingAuthor.firstName
+      } ${submittingAuthor.lastName}`,
+    })
+
+    editors.forEach(eic => {
+      const email = new Email({
+        type: 'system',
+        toUser: {
+          email: eic.email,
+        },
+        fromEmail: `${journalName} <${staffEmail}>`,
+        content: {
+          subject: `${this.collection.customId}: New manuscript submitted`,
+          ctaText: 'MANUSCRIPT DETAILS',
+          unsubscribeLink: this.baseUrl,
+          ctaLink: services.createUrl(
+            this.baseUrl,
+            `/projects/${this.collection.id}/versions/${
+              this.fragment.id
+            }/details`,
+          ),
+        },
+      })
+
+      const { html, text } = email.getNotificationBody({ emailBodyProps })
+      email.sendEmail({ html, text })
+    })
+  }
+
+  async notifyEiCWhenEQARejectsManuscript(comments) {
+    const { titleText } = await this.getNotificationProperties()
+
+    const userHelper = new User({ UserModel: this.UserModel })
+    const editors = await userHelper.getEditorsInChief()
+
+    const emailBodyProps = getEmailCopy({
+      comments,
+      titleText,
+      emailType: 'eic-manuscript-returned-by-eqa',
+    })
+
+    editors.forEach(eic => {
+      const email = new Email({
+        type: 'system',
+        toUser: {
+          email: eic.email,
+        },
+        fromEmail: `${journalName} <${staffEmail}>`,
+        content: {
+          subject: `${
+            this.collection.customId
+          }: Manuscript returned with comments`,
+          ctaText: 'MANUSCRIPT DETAILS',
+          unsubscribeLink: this.baseUrl,
+          ctaLink: services.createUrl(
+            this.baseUrl,
+            `/projects/${this.collection.id}/versions/${
+              this.fragment.id
+            }/details`,
+          ),
+        },
+      })
+
+      const { html, text } = email.getNotificationBody({ emailBodyProps })
+      email.sendEmail({ html, text })
+    })
+  }
+}
+
+module.exports = Notification
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/helpers.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/helpers.js
deleted file mode 100644
index c03dedb12bd798664f0d1855fcd4af9cb2f345fb..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/helpers.js
+++ /dev/null
@@ -1,401 +0,0 @@
-const config = require('config')
-const { get, chain } = require('lodash')
-
-const { services } = require('pubsweet-component-helper-service')
-const { getEmailCopy } = require('./emailCopy')
-
-const unsubscribeSlug = config.get('unsubscribe.url')
-const journalName = config.get('journal.name')
-
-module.exports = {
-  sendReviewersEmail: async ({ email, baseUrl, reviewers }) => {
-    reviewers.forEach(reviewer => {
-      email.toUser = {
-        email: reviewer.email,
-        name: `${reviewer.lastName}`,
-      }
-      email.content.unsubscribeLink = services.createUrl(
-        baseUrl,
-        unsubscribeSlug,
-        {
-          id: reviewer.id,
-          token: reviewer.accessTokens.unsubscribe,
-        },
-      )
-      const { html, text } = email.getNotificationBody({
-        emailBodyProps: {
-          paragraph: reviewer.paragraph,
-          hasLink: reviewer.hasLink,
-          hasIntro: reviewer.hasIntro,
-          hasSignature: reviewer.hasSignature,
-        },
-      })
-      email.sendEmail({ html, text })
-    })
-  },
-  getSubmittedReviewers: async ({
-    UserModel,
-    titleText,
-    fragmentHelper,
-    recommendation,
-  }) => {
-    const emailType =
-      recommendation === 'publish'
-        ? 'submitted-reviewers-after-publish'
-        : 'submitted-reviewers-after-reject'
-
-    const reviewers = (await fragmentHelper.getReviewers({
-      UserModel,
-      type: 'submitted',
-    })).map(rev => ({
-      ...rev,
-      emailType,
-      ...getEmailCopy({
-        emailType,
-        titleText,
-      }),
-    }))
-
-    return reviewers
-  },
-  getNoResponseReviewers: async ({ fragmentHelper, UserModel, titleText }) => {
-    const acceptedReviewers = (await fragmentHelper.getReviewers({
-      UserModel,
-      type: 'accepted',
-    })).map(rev => ({
-      ...rev,
-      ...getEmailCopy({
-        emailType: 'accepted-reviewers-after-recommendation',
-        titleText,
-      }),
-    }))
-
-    const pendingReviewers = (await fragmentHelper.getReviewers({
-      UserModel,
-      type: 'pending',
-    })).map(rev => ({
-      ...rev,
-      ...getEmailCopy({
-        emailType: 'pending-reviewers-after-recommendation',
-        titleText,
-      }),
-    }))
-
-    const reviewers = [...acceptedReviewers, ...pendingReviewers]
-
-    return reviewers
-  },
-  updateEmailContentForReviewers: ({
-    email,
-    customId,
-    signatureName,
-    recommendation,
-  }) => {
-    const subject =
-      recommendation === 'publish'
-        ? 'A manuscript you reviewed has been accepted'
-        : 'A manuscript you reviewed has been rejected'
-
-    email.content.subject = `${customId}: ${subject}`
-    email.content.signatureName = signatureName
-    email.content.signatureJournal = journalName
-    return email
-  },
-  sendHandlingEditorEmail: ({
-    email,
-    comments,
-    emailType,
-    titleText,
-    targetUserName,
-  }) => {
-    const { html, text } = email.getNotificationBody({
-      emailBodyProps: getEmailCopy({
-        emailType,
-        titleText,
-        comments,
-        targetUserName,
-      }),
-    })
-    email.sendEmail({ html, text })
-  },
-  updateEmailContentForHE: ({
-    email,
-    baseUrl,
-    eicName,
-    customId,
-    heLastName,
-    handlingEditor,
-    recommendation,
-    recommendationType,
-  }) => {
-    if (recommendationType === 'review') {
-      email.content.subject = `${customId}: A review has been submitted`
-    } else {
-      switch (recommendation) {
-        case 'return-to-handling-editor':
-          email.content.subject = `${customId}: Editorial decision returned with comments`
-          break
-        case 'publish':
-        case 'reject':
-          email.content.subject = `${customId}: Editorial decision confirmed`
-          break
-        default:
-          throw new Error(`Undefined recommendation: ${recommendation}`)
-      }
-    }
-
-    email.toUser = {
-      email: handlingEditor.email,
-      name: heLastName,
-    }
-    email.content.unsubscribeLink = services.createUrl(
-      baseUrl,
-      unsubscribeSlug,
-      {
-        id: handlingEditor.id,
-        token: handlingEditor.accessTokens.unsubscribe,
-      },
-    )
-
-    email.content.signatureName = eicName
-    delete email.content.signatureJournal
-
-    return email
-  },
-  getEmailTypeByRecommendationForHE: ({
-    recommendation,
-    recommendationType,
-  }) => {
-    let emailType
-    if (recommendationType === 'review') {
-      emailType = 'he-review-submitted'
-    } else {
-      switch (recommendation) {
-        case 'return-to-handling-editor':
-          emailType = 'he-manuscript-return-with-comments'
-          break
-        case 'publish':
-          emailType = 'he-manuscript-published'
-          break
-        case 'reject':
-          emailType = 'he-manuscript-rejected'
-          break
-        default:
-          throw new Error(`undefined recommendation: ${recommendation} `)
-      }
-    }
-
-    return emailType
-  },
-  getEiCCommentsForHE: ({ newRecommendation }) => {
-    const eicComments = chain(newRecommendation)
-      .get('comments')
-      .find(comm => !comm.public)
-      .get('content')
-      .value()
-
-    return eicComments
-  },
-  sendEiCsEmail: async ({
-    email,
-    customId,
-    titleText,
-    userHelper,
-    targetUserName,
-    recommendation: { recommendation, comments: recComments = [] },
-  }) => {
-    let emailType
-
-    switch (recommendation) {
-      case 'minor':
-      case 'major':
-        emailType = 'eic-request-revision-from-he'
-        email.content.subject = `${customId}: Revision requested`
-        break
-      case 'publish':
-        emailType = 'eic-recommend-to-publish-from-he'
-        email.content.subject = `${customId}: Recommendation to publish`
-        break
-      case 'reject':
-        emailType = 'eic-recommend-to-reject-from-he'
-        email.content.subject = `${customId}: Recommendation to reject`
-        break
-      default:
-        throw new Error(`undefined recommendation: ${recommendation} `)
-    }
-    delete email.content.signatureJournal
-
-    const privateNote = recComments.find(comm => !comm.public)
-    const content = get(privateNote, 'content')
-    const comments = content
-      ? `The editor provided the following comments: "${content}"`
-      : ''
-
-    const editors = (await userHelper.getEditorsInChief()).map(eic => ({
-      ...eic,
-      ...getEmailCopy({
-        comments,
-        emailType,
-        titleText,
-        targetUserName,
-      }),
-    }))
-
-    editors.forEach(eic => {
-      email.toUser = {
-        email: eic.email,
-        name: `${eic.firstName} ${eic.lastName}`,
-      }
-      const { html, text } = email.getNotificationBody({
-        emailBodyProps: {
-          paragraph: eic.paragraph,
-          hasLink: eic.hasLink,
-          hasIntro: eic.hasIntro,
-          hasSignature: eic.hasSignature,
-        },
-      })
-      email.sendEmail({ html, text })
-    })
-  },
-  sendAuthorsEmail: async ({ email, authors, baseUrl }) => {
-    authors.forEach(author => {
-      email.toUser = {
-        email: author.email,
-        name: `${author.lastName}`,
-      }
-      email.content.unsubscribeLink = services.createUrl(
-        baseUrl,
-        unsubscribeSlug,
-        {
-          id: author.id,
-          token: author.accessTokens.unsubscribe,
-        },
-      )
-      const { html, text } = email.getNotificationBody({
-        emailBodyProps: {
-          paragraph: author.paragraph,
-          hasLink: author.hasLink,
-          hasIntro: author.hasIntro,
-          hasSignature: author.hasSignature,
-        },
-      })
-      email.sendEmail({ html, text })
-    })
-  },
-  getSubmittingAuthor: ({
-    title,
-    journalName,
-    authorNoteText,
-    submittingAuthor,
-  }) => {
-    const author = {
-      ...submittingAuthor,
-      ...getEmailCopy({
-        emailType: 'author-request-to-revision',
-        titleText: `your submission "${title}" to ${journalName}`,
-        comments: authorNoteText,
-      }),
-    }
-
-    return author
-  },
-  getPrivateNoteTextForAuthor: ({ newRecommendation }) => {
-    const authorNote = newRecommendation.comments.find(comm => comm.public)
-    const content = get(authorNote, 'content')
-    const authorNoteText = content ? `Reason & Details: "${content}"` : ''
-
-    return authorNoteText
-  },
-  updateEmailContentForSA: ({ email, customId, signatureName }) => {
-    email.content.subject = `${customId}: Revision requested`
-    email.content.signatureName = signatureName
-    email.content.signatureJournal = journalName
-
-    return email
-  },
-  getHEComments: ({ heRecommendation }) => {
-    const heComments = get(heRecommendation, 'comments', [])
-
-    if (heComments.length === 0) return
-
-    const publicComment = heComments.find(comm => comm.public)
-
-    const content = get(publicComment, 'content')
-    if (!content) {
-      throw new Error('a public comment cannot be without content')
-    }
-    const comments = `Please find our editorial comments below<br/>: "${content}"`
-
-    return comments
-  },
-  getAllAuthors: ({ title, comments, emailType, fragmentAuthors }) => {
-    const authors = fragmentAuthors.map(author => ({
-      ...author,
-      ...getEmailCopy({
-        comments,
-        emailType,
-        titleText: `your manuscript titled "${title}"`,
-      }),
-    }))
-
-    return authors
-  },
-  sendSubmittingAuthorEmail: ({ email, author, baseUrl }) => {
-    email.toUser = {
-      email: author.email,
-      name: `${author.lastName}`,
-    }
-    email.content.unsubscribeLink = services.createUrl(
-      baseUrl,
-      unsubscribeSlug,
-      {
-        id: author.id,
-        token: author.accessTokens.unsubscribe,
-      },
-    )
-    const { html, text } = email.getNotificationBody({
-      emailBodyProps: {
-        paragraph: author.paragraph,
-        hasLink: author.hasLink,
-        hasIntro: author.hasIntro,
-        hasSignature: author.hasSignature,
-      },
-    })
-    email.sendEmail({ html, text })
-  },
-  updateEmailContentForAllAuthors: ({ email, recommendation, customId }) => {
-    switch (recommendation) {
-      case 'publish':
-        email.content.subject = `${customId}: Manuscript accepted`
-        break
-      case 'reject':
-        email.content.subject = `${customId}: Manuscript rejected`
-        break
-      default:
-        throw new Error(`Undefined recommendation: ${recommendation}`)
-    }
-    email.content.signatureJournal = journalName
-
-    return email
-  },
-  getEmailTypeByRecommendationForAuthors: ({
-    recommendation,
-    hasPeerReview,
-  }) => {
-    let emailType
-    switch (recommendation) {
-      case 'publish':
-        emailType = 'author-manuscript-published'
-        break
-      case 'reject':
-        emailType = hasPeerReview
-          ? 'author-manuscript-rejected'
-          : 'authors-manuscript-rejected-before-review'
-        break
-      default:
-        throw new Error(`Undefined recommendation: ${recommendation}`)
-    }
-
-    return emailType
-  },
-}
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js
deleted file mode 100644
index 9bb9c3a6cbd7d7b66222cb4b472e108c08304a06..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js
+++ /dev/null
@@ -1,276 +0,0 @@
-const config = require('config')
-const { get, isEmpty } = require('lodash')
-const Email = require('@pubsweet/component-email-templating')
-
-const {
-  User,
-  services,
-  Fragment,
-  Collection,
-} = require('pubsweet-component-helper-service')
-
-const { getEmailCopy } = require('./emailCopy')
-const helpers = require('./helpers')
-
-const editorialAssistantEmail = config.get('journal.staffEmail')
-const journalName = config.get('journal.name')
-module.exports = {
-  async sendNotifications({
-    hasEQA,
-    baseUrl,
-    fragment,
-    UserModel,
-    collection,
-    targetUserName,
-    isEditorInChief,
-    newRecommendation,
-  }) {
-    const fragmentHelper = new Fragment({ fragment })
-    const parsedFragment = await fragmentHelper.getFragmentData({
-      handlingEditor: collection.handlingEditor,
-    })
-    const {
-      activeAuthors,
-      submittingAuthor,
-    } = await fragmentHelper.getAuthorData({ UserModel })
-
-    const subjectBaseText = `${collection.customId}: Manuscript`
-    const titleText = `The manuscript titled "${parsedFragment.title}" by ${
-      submittingAuthor.firstName
-    } ${submittingAuthor.lastName}`
-
-    const userHelper = new User({ UserModel })
-    const eicName = await userHelper.getEiCName()
-
-    let email = new Email({
-      type: 'user',
-      content: {
-        signatureName: eicName,
-        unsubscribeLink: baseUrl,
-        ctaText: 'MANUSCRIPT DETAILS',
-        signatureJournal: journalName,
-        ctaLink: services.createUrl(
-          baseUrl,
-          `/projects/${collection.id}/versions/${fragment.id}/details`,
-        ),
-      },
-    })
-
-    const { recommendation, recommendationType } = newRecommendation
-
-    // the EiC recommends to publish so an email to the EQA needs to be sent,
-    // one requesting approval or one informing them that the manuscript has been published
-    if (
-      isEditorInChief &&
-      recommendation === 'publish' &&
-      collection.technicalChecks.token
-    ) {
-      sendEQAEmail({
-        email,
-        eicName,
-        baseUrl,
-        titleText,
-        collection,
-        subjectBaseText,
-      })
-    }
-
-    const hasPeerReview = !isEmpty(collection.handlingEditor)
-    const { customId } = collection
-    const collHelper = new Collection({ collection })
-
-    // send HE emails when a review is submitted
-    // or when the EiC makes a recommendation after peer review
-    if (
-      recommendationType === 'review' ||
-      (isEditorInChief &&
-        hasPeerReview &&
-        (recommendation !== 'publish' || hasEQA))
-    ) {
-      const handlingEditor = get(collection, 'handlingEditor', {})
-      const heUser = await UserModel.find(handlingEditor.id)
-      email = helpers.updateEmailContentForHE({
-        email,
-        baseUrl,
-        eicName,
-        customId,
-        recommendation,
-        recommendationType,
-        heLastName: collHelper.getHELastName(),
-        handlingEditor: heUser,
-      })
-      const emailType = helpers.getEmailTypeByRecommendationForHE({
-        recommendation,
-        recommendationType,
-      })
-      const comments = helpers.getEiCCommentsForHE({ newRecommendation })
-      helpers.sendHandlingEditorEmail({
-        email,
-        comments,
-        emailType,
-        titleText,
-        targetUserName,
-      })
-    }
-
-    if (
-      recommendationType === 'review' ||
-      recommendation === 'return-to-handling-editor'
-    ) {
-      return
-    }
-
-    // when publishing, only send emails to authors if the manuscript has passed EQA
-    if (isEditorInChief && (recommendation !== 'publish' || hasEQA)) {
-      // send all authors email
-      const emailType = helpers.getEmailTypeByRecommendationForAuthors({
-        recommendation,
-        hasPeerReview,
-      })
-
-      let comments
-      if (hasPeerReview) {
-        comments = helpers.getHEComments({
-          heRecommendation: parsedFragment.heRecommendation,
-        })
-      } else {
-        comments = newRecommendation.comments[0].content
-      }
-
-      const authors = helpers.getAllAuthors({
-        comments,
-        emailType,
-        title: parsedFragment.title,
-        fragmentAuthors: activeAuthors,
-      })
-      email = helpers.updateEmailContentForAllAuthors({
-        email,
-        customId,
-        recommendation,
-      })
-      helpers.sendAuthorsEmail({ email, authors, baseUrl })
-    }
-
-    // send email to SA when the HE requests a revision
-    if (collection.status === 'revisionRequested') {
-      const authorNoteText = helpers.getPrivateNoteTextForAuthor({
-        newRecommendation,
-      })
-      const author = helpers.getSubmittingAuthor({
-        journalName,
-        authorNoteText,
-        submittingAuthor,
-        title: parsedFragment.title,
-      })
-      email = helpers.updateEmailContentForSA({
-        email,
-        customId,
-        signatureName: get(collection, 'handlingEditor.name', eicName),
-      })
-
-      helpers.sendSubmittingAuthorEmail({ email, author, baseUrl })
-    }
-
-    if (!hasPeerReview) {
-      return
-    }
-
-    let reviewers = []
-    if (isEditorInChief) {
-      if (recommendation !== 'publish' || hasEQA) {
-        email = helpers.updateEmailContentForReviewers({
-          email,
-          customId,
-          recommendation,
-          signatureName: eicName,
-        })
-        reviewers = await helpers.getSubmittedReviewers({
-          UserModel,
-          titleText,
-          fragmentHelper,
-          recommendation,
-        })
-      }
-    } else {
-      email = helpers.updateEmailContentForReviewers({
-        email,
-        customId,
-        subject: `Review no longer required`,
-        signatureName: get(collection, 'handlingEditor.name', eicName),
-      })
-
-      reviewers = await helpers.getNoResponseReviewers({
-        UserModel,
-        titleText,
-        fragmentHelper,
-      })
-
-      helpers.sendEiCsEmail({
-        email,
-        baseUrl,
-        customId,
-        userHelper,
-        recommendation: newRecommendation,
-        targetUserName: collHelper.getHELastName(),
-        titleText: `the submission "${parsedFragment.title}" by ${
-          submittingAuthor.firstName
-        } ${submittingAuthor.lastName}`,
-      })
-    }
-
-    helpers.sendReviewersEmail({ email, baseUrl, reviewers })
-  },
-}
-
-const sendEQAEmail = ({
-  email,
-  eicName,
-  baseUrl,
-  titleText,
-  collection,
-  subjectBaseText,
-}) => {
-  let emailType
-  switch (collection.status) {
-    case 'accepted':
-      emailType = 'eqa-manuscript-published'
-      email.content.subject = `${subjectBaseText} decision finalized`
-      break
-    case 'inQA':
-      emailType = 'eqa-manuscript-request-for-approval'
-      email.content.subject = `${subjectBaseText} Request for EQA Approval`
-      email.content.ctaLink = services.createUrl(
-        baseUrl,
-        config.get('eqa-decision.url'),
-        {
-          collectionId: collection.id,
-          customId: collection.customId,
-          token: collection.technicalChecks.token,
-        },
-      )
-      email.content.ctaText = 'MAKE DECISION'
-      break
-    default:
-      throw new Error(
-        `Cannot send EQA email when collection status is ${collection.status} `,
-      )
-  }
-
-  email.toUser = {
-    email: editorialAssistantEmail,
-    name: 'Editorial Assistant',
-  }
-
-  email.content.unsubscribeLink = baseUrl
-  email.content.signatureName = eicName
-
-  const { html, text } = email.getNotificationBody({
-    emailBodyProps: getEmailCopy({
-      eicName,
-      titleText,
-      emailType,
-      customId: collection.customId,
-    }),
-  })
-  email.sendEmail({ html, text })
-}
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
index 43ea6d60498e6c90dceadec514060fe4d55bf3a9..7846722ed2f83db255a59ff80f95c142ccf2d3ec 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
@@ -4,7 +4,7 @@ const {
   Collection,
 } = require('pubsweet-component-helper-service')
 
-const notifications = require('./notifications/notifications')
+const Notification = require('../../notifications/notification')
 
 module.exports = models => async (req, res) => {
   const { collectionId, fragmentId, recommendationId } = req.params
@@ -42,22 +42,22 @@ module.exports = models => async (req, res) => {
       })
 
     const UserModel = models.User
-    const user = await UserModel.find(req.user)
+    const reviewer = await UserModel.find(req.user)
 
     Object.assign(recommendation, req.body)
     recommendation.updatedOn = Date.now()
 
     if (req.body.submittedOn) {
-      notifications.sendNotifications({
+      const notification = new Notification({
         fragment,
+        UserModel,
         collection,
-        isEditorInChief: false,
-        UserModel: models.User,
         baseUrl: services.getBaseUrl(req),
         newRecommendation: recommendation,
-        targetUserName: `${user.lastName}`,
       })
 
+      notification.notifyHEWhenReviewerSubmitsReport(reviewer.lastName)
+
       if (['underReview'].includes(collection.status)) {
         const collectionHelper = new Collection({ collection })
         collectionHelper.updateStatus({ newStatus: 'reviewCompleted' })
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
index b16803dbdb91e845d3a6e4b9b24fe30ca6776606..55d54aa4274c9035da2fe92342ccab04929b6220 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
@@ -1,5 +1,5 @@
 const uuid = require('uuid')
-const { pick, get, set, has } = require('lodash')
+const { pick, get, set, has, isEmpty } = require('lodash')
 const config = require('config')
 const { v4 } = require('uuid')
 
@@ -30,7 +30,7 @@ const sendMTSPackage = async (collection, fragment, isEQA = false) => {
   await MTS.sendPackage({ fragment: packageFragment, isEQA })
 }
 
-const notifications = require('./notifications/notifications')
+const Notification = require('../../notifications/notification')
 
 module.exports = models => async (req, res) => {
   const { recommendation, comments, recommendationType } = req.body
@@ -134,16 +134,50 @@ module.exports = models => async (req, res) => {
       await collection.save()
     }
 
-    notifications.sendNotifications({
-      hasEQA,
+    const notification = new Notification({
       fragment,
       collection,
-      isEditorInChief,
       newRecommendation,
       UserModel: models.User,
-      targetUserName: reqUser.lastName,
       baseUrl: services.getBaseUrl(req),
     })
+
+    const hasPeerReview = !isEmpty(collection.handlingEditor)
+
+    if (isEditorInChief) {
+      if (recommendation === 'publish' && collection.status === 'inQA') {
+        notification.notifyEAWhenEiCRequestsEQAApproval()
+      }
+
+      if (recommendation === 'publish' && collection.status === 'accepted') {
+        notification.notifyEAWhenEiCMakesFinalDecision()
+      }
+
+      if (hasPeerReview && (recommendation !== 'publish' || hasEQA)) {
+        if (recommendation === 'return-to-handling-editor') {
+          notification.notifyHEWhenEiCReturnsToHE()
+        } else {
+          notification.notifyHEWhenEiCMakesDecision()
+          notification.notifyReviewersWhenEiCMakesDecision()
+        }
+      }
+
+      if (
+        recommendation !== 'return-to-handling-editor' &&
+        (recommendation !== 'publish' || hasEQA)
+      ) {
+        notification.notifyAuthorsWhenEiCMakesDecision()
+      }
+    } else {
+      if (collection.status === 'revisionRequested') {
+        notification.notifySAWhenHERequestsRevision()
+      }
+
+      if (hasPeerReview) {
+        notification.notifyReviewersWhenHEMakesRecommendation()
+        notification.notifyEiCWhenHEMakesRecommendation()
+      }
+    }
   }
 
   fragment.recommendations.push(newRecommendation)
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
deleted file mode 100644
index d454047c92f9d937a234ee0eb60baf89e0ed50b1..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
+++ /dev/null
@@ -1,46 +0,0 @@
-const config = require('config')
-
-// const staffEmail = config.get('journal.staffEmail')
-const journalName = config.get('journal.name')
-
-const getEmailCopy = ({ emailType, titleText, comments }) => {
-  let paragraph
-  let hasLink = true
-  let hasIntro = true
-  let hasSignature = true
-  switch (emailType) {
-    case 'eqs-manuscript-accepted':
-      hasIntro = false
-      hasSignature = false
-      paragraph = `A new ${titleText} has been submitted to ${journalName}.<br/><br/>
-        To begin the review process, please visit the manuscript details page.`
-      break
-    case 'he-manuscript-published':
-      hasLink = false
-      paragraph = `Thank you for your recommendation to publish ${titleText} based on the reviews you received.<br/><br/>
-      I can confirm this article will now go through to publication.`
-      break
-    case 'author-manuscript-published':
-      paragraph = `I am delighted to inform you that ${titleText} has passed through the review process and will be published in Hindawi.<br/><br/>
-        Thanks again for choosing to publish with us.`
-      hasLink = false
-      break
-    case 'submitted-reviewers-after-publish':
-      hasLink = false
-      paragraph = `Thank you for your review on ${titleText}. After taking into account the reviews and the recommendation of the Handling Editor, I can confirm this article will now be published.<br/><br/>
-      If you have any queries about this decision, then please email them to Hindawi as soon as possible.`
-      break
-    case 'eqa-manuscript-returned-to-eic':
-      paragraph = `We regret to inform you that ${titleText} has been returned with comments. Please click the link below to access the manuscript.<br/><br/>
-        Comments: ${comments}<br/><br/>`
-      break
-    default:
-      throw new Error(`The ${emailType} email type is not defined.`)
-  }
-
-  return { paragraph, hasLink, hasIntro, hasSignature }
-}
-
-module.exports = {
-  getEmailCopy,
-}
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
deleted file mode 100644
index 8380da030638ce5c7d66293fd7531ac01841a237..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
+++ /dev/null
@@ -1,105 +0,0 @@
-const config = require('config')
-const Email = require('@pubsweet/component-email-templating')
-
-const {
-  User,
-  services,
-  Fragment,
-} = require('pubsweet-component-helper-service')
-const { getEmailCopy } = require('./emailCopy')
-
-const { name: journalName, staffEmail } = config.get('journal')
-
-module.exports = {
-  async sendNotifications({
-    agree,
-    baseUrl,
-    collection,
-    comments = '',
-    User: UserModel,
-    Fragment: FragmentModel,
-  }) {
-    const fragment = await FragmentModel.find(collection.fragments[0])
-    const fragmentHelper = new Fragment({ fragment })
-    const parsedFragment = await fragmentHelper.getFragmentData({
-      handlingEditor: collection.handlingEditor,
-    })
-
-    const { submittingAuthor } = await fragmentHelper.getAuthorData({
-      UserModel,
-    })
-
-    const titleText = `manuscript titled "${parsedFragment.title}" by ${
-      submittingAuthor.firstName
-    } ${submittingAuthor.lastName}`
-    const subjectBaseText = `${collection.customId}: Manuscript`
-    const { customId } = collection
-    const userHelper = new User({ UserModel })
-    const subject = `${subjectBaseText} ${
-      agree ? '' : 'Not '
-    }Passed Technical Checks`
-
-    const email = new Email({
-      type: 'user',
-      fromEmail: `${journalName} <${staffEmail}>`,
-      content: {
-        subject,
-        signatureName: 'EQA Team',
-        ctaLink: services.createUrl(
-          baseUrl,
-          `/projects/${collection.id}/versions/${fragment.id}/details`,
-        ),
-        ctaText: 'MANUSCRIPT DETAILS',
-        unsubscribeLink: baseUrl,
-      },
-    })
-
-    sendEditorsEmail({
-      email,
-      agree,
-      customId,
-      comments,
-      titleText,
-      userHelper,
-    })
-  },
-}
-
-const sendEditorsEmail = async ({
-  email,
-  agree,
-  titleText,
-  customId,
-  userHelper,
-  comments = '',
-}) => {
-  email.content.subject = `${customId}: New manuscript submitted`
-  const emailType = agree
-    ? 'eqs-manuscript-accepted'
-    : 'eqa-manuscript-returned-to-eic'
-
-  const editors = (await userHelper.getEditorsInChief()).map(eic => ({
-    ...eic,
-    ...getEmailCopy({
-      emailType,
-      titleText,
-      comments,
-    }),
-  }))
-
-  editors.forEach(eic => {
-    email.toUser = {
-      email: eic.email,
-      name: `${eic.firstName} ${eic.lastName}`,
-    }
-    const { html, text } = email.getNotificationBody({
-      emailBodyProps: {
-        paragraph: eic.paragraph,
-        hasLink: eic.hasLink,
-        hasIntro: eic.hasIntro,
-        hasSignature: eic.hasSignature,
-      },
-    })
-    email.sendEmail({ html, text })
-  })
-}
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
index fcb8a59daf70b1bc7129fa23b1fe664d8de5750e..c73ab4ad4f676df47d77fc7fd7ee281e06dd31a9 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
@@ -1,13 +1,7 @@
 const { get, find, isEmpty, last } = require('lodash')
 const { services } = require('pubsweet-component-helper-service')
 
-const {
-  sendNotifications: sendEQSNotifications,
-} = require('./notifications/notifications')
-
-const {
-  sendNotifications: sendEQANotifications,
-} = require('../fragmentsRecommendations/notifications/notifications')
+const Notification = require('../../notifications/notification')
 
 const TECHNICAL_STEPS = {
   EQS: 'eqs',
@@ -57,7 +51,6 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => {
       })
     }
 
-    delete collection.technicalChecks.token
     if (agree) {
       if (step === TECHNICAL_STEPS.EQA) {
         collection.technicalChecks.eqa = true
@@ -67,35 +60,37 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => {
       }
     }
 
+    delete collection.technicalChecks.token
     collection.status = setNewStatus(step, agree)
     await collection.save()
 
-    const isEQA = get(collection, 'technicalChecks.eqa', false)
-    const baseUrl = services.getBaseUrl(req)
-    if (isEQA) {
-      const fragment = await Fragment.find(last(collection.fragments))
+    const fragment = await Fragment.find(last(collection.fragments))
+    const notification = new Notification({
+      fragment,
+      collection,
+      UserModel: User,
+      baseUrl: services.getBaseUrl(req),
+      newRecommendation: {
+        recommendation: 'publish',
+        recommendationType: 'editorRecommendation',
+      },
+    })
 
-      sendEQANotifications({
-        baseUrl,
-        fragment,
-        collection,
-        hasEQA: true,
-        UserModel: User,
-        isEditorInChief: true,
-        newRecommendation: {
-          recommendation: 'publish',
-          recommendationType: 'editorRecommendation',
-        },
-      })
-    } else {
-      sendEQSNotifications({
-        User,
-        agree,
-        comments,
-        Fragment,
-        collection,
-        baseUrl,
-      })
+    if (step === TECHNICAL_STEPS.EQA) {
+      const hasPassedEQA = get(collection, 'technicalChecks.eqa')
+      if (hasPassedEQA) {
+        // the EA accepted the manuscript so we need to notify users of the final publication decision
+        notification.notifyAuthorsWhenEiCMakesDecision()
+        const hasPeerReview = !isEmpty(collection.handlingEditor)
+        if (hasPeerReview) {
+          notification.notifyHEWhenEiCMakesDecision()
+          notification.notifyReviewersWhenEiCMakesDecision()
+        }
+      } else {
+        notification.notifyEiCWhenEQARejectsManuscript(comments)
+      }
+    } else if (agree) {
+      notification.notifyEiCWhenEQSAcceptsManuscript()
     }
 
     return res.status(200).json(collection)
diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
index e86097c9abb9defbad77862f1826360d5edef8fa..b090a9cb6fc1ae464488560f7032b0ff97388dea 100644
--- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
+++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
@@ -306,8 +306,6 @@ describe('Post fragments recommendations route handler', () => {
     delete fragment.recommendations
     delete fragment.revision
     delete fragment.invitations
-    delete collection.invitations
-    delete collection.handlingEditor
     collection.technicalChecks.eqa = false
 
     const res = await requests.sendRequest({
diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js
index c3e8683307b2b599e99a166cef78f693f2b78ddb..ab97d296049c9240213c740f6987eb0cabd8f8ad 100644
--- a/packages/component-manuscript/src/components/ManuscriptLayout.js
+++ b/packages/component-manuscript/src/components/ManuscriptLayout.js
@@ -39,6 +39,8 @@ const ManuscriptLayout = ({
   fragment = {},
   changeForm,
   isFetching,
+  isFetchingData,
+  publonsFetching,
   fetchingError,
   formValues,
   heExpanded,
@@ -84,7 +86,7 @@ const ManuscriptLayout = ({
           fragment={fragment}
           handlingEditors={handlingEditors}
           inviteHE={toggleAssignHE}
-          isFetching={isFetching.editorsFetching}
+          isFetching={isFetchingData.editorsFetching}
           journal={journal}
           resendInvitation={inviteHandlingEditor.assignHE}
           revokeInvitation={inviteHandlingEditor.revokeHE}
@@ -107,8 +109,12 @@ const ManuscriptLayout = ({
           />
         )}
 
-        {get(currentUser, 'permissions.canSubmitRevision', false) && (
-          <SubmitRevision {...submitRevision} />
+        {get(currentUser, 'permissions.authorCanViewReportsDetails', false) && (
+          <AuthorReviews
+            currentUser={currentUser}
+            journal={journal}
+            reports={reviewerReports}
+          />
         )}
 
         {submittedOwnRecommendation && (
@@ -161,7 +167,7 @@ const ManuscriptLayout = ({
           currentUser={currentUser}
           expanded={heExpanded}
           handlingEditors={handlingEditors}
-          isFetching={isFetching.editorsFetching}
+          isFetching={isFetchingData.editorsFetching}
           toggle={toggleAssignHE}
         />
 
@@ -179,7 +185,7 @@ const ManuscriptLayout = ({
               )
             }
             invitations={invitationsWithReviewers}
-            isFetching={isFetching.publonsFetching}
+            isFetching={isFetchingData.publonsFetching}
             journal={journal}
             mb={2}
             publonReviewers={publonReviewers}
@@ -189,12 +195,9 @@ const ManuscriptLayout = ({
             {...inviteReviewer}
           />
         )}
-        {get(currentUser, 'permissions.authorCanViewReportsDetails', false) && (
-          <AuthorReviews
-            currentUser={currentUser}
-            journal={journal}
-            reports={reviewerReports}
-          />
+
+        {get(currentUser, 'permissions.canSubmitRevision', false) && (
+          <SubmitRevision {...submitRevision} />
         )}
 
         {get(currentUser, 'permissions.canMakeHERecommendation', false) &&
diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js
index 0b77b64cdfeaecaa7e71b9089627d0aade3a2519..ab96f5c6b3bb159bf4e1d9f15ea20a341029e5a3 100644
--- a/packages/component-manuscript/src/components/ManuscriptPage.js
+++ b/packages/component-manuscript/src/components/ManuscriptPage.js
@@ -191,7 +191,7 @@ export default compose(
           ),
         },
       },
-      isFetching: {
+      isFetchingData: {
         editorsFetching: selectFetching(state),
         publonsFetching: isFetching,
       },