From 81fef54fc566650493fbd2c9ddcbd1429a980bf0 Mon Sep 17 00:00:00 2001
From: Sebastian <sebastian.mihalache@thinslices.com>
Date: Fri, 23 Feb 2018 12:47:00 +0200
Subject: [PATCH] feat(component-invite): add HE invite functionality

---
 .../src/controllers/inviteCollectionRole.js   |  60 +++++
 .../src/controllers/inviteGlobalRole.js       |  39 +++
 packages/component-invite/src/routes/post.js  | 105 ++------
 .../src/tests/fixtures/users.js               |  16 ++
 .../component-invite/src/tests/post.test.js   |  99 ++++----
 packages/component-mail-service/src/Mail.js   |  39 ++-
 .../src/templates/assign-handling-editor.html | 232 ++++++++++++++++++
 .../src/templates/assign-handling-editor.txt  |   6 +
 packages/component-send-email/package.json    |   1 -
 9 files changed, 463 insertions(+), 134 deletions(-)
 create mode 100644 packages/component-invite/src/controllers/inviteCollectionRole.js
 create mode 100644 packages/component-invite/src/controllers/inviteGlobalRole.js
 create mode 100644 packages/component-mail-service/src/templates/assign-handling-editor.html
 create mode 100644 packages/component-mail-service/src/templates/assign-handling-editor.txt

diff --git a/packages/component-invite/src/controllers/inviteCollectionRole.js b/packages/component-invite/src/controllers/inviteCollectionRole.js
new file mode 100644
index 000000000..0c670970f
--- /dev/null
+++ b/packages/component-invite/src/controllers/inviteCollectionRole.js
@@ -0,0 +1,60 @@
+const logger = require('@pubsweet/logger')
+const config = require('config')
+const helpers = require('../helpers/helpers')
+const mailService = require('pubsweet-component-mail-service')
+
+const configRoles = config.get('roles')
+
+module.exports = async (
+  email,
+  role,
+  reqUser,
+  res,
+  collectionId,
+  models,
+  dashboardUrl,
+) => {
+  if (reqUser.admin) {
+    logger.error(`admin tried to invite a ${role} to a collection`)
+
+    return res.status(403).json({
+      error: `admin cannot invite an ${role} to a collection`,
+    })
+  }
+
+  if (!configRoles.collection.includes(role)) {
+    logger.error(`invitation has been attempted with invalid role: ${role}`)
+    return res
+      .status(403)
+      .json({ error: `Role ${role} cannot be set on collections` })
+  }
+
+  try {
+    await models.Collection.find(collectionId)
+  } catch (e) {
+    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+
+  try {
+    const user = await models.User.findByEmail(email)
+    user.roles.push(role)
+    await mailService.setupAssignEmail(
+      user.email,
+      'assign-handling-editor',
+      dashboardUrl,
+    )
+
+    // await helpers.createNewTeam(collection.id, user, models.Team)
+
+    // add team to collection.teams
+    return res.status(200).json(user)
+  } catch (e) {
+    const notFoundError = await helpers.handleNotFoundError(e, 'user')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-invite/src/controllers/inviteGlobalRole.js b/packages/component-invite/src/controllers/inviteGlobalRole.js
new file mode 100644
index 000000000..107da28af
--- /dev/null
+++ b/packages/component-invite/src/controllers/inviteGlobalRole.js
@@ -0,0 +1,39 @@
+const logger = require('@pubsweet/logger')
+const helpers = require('../helpers/helpers')
+const mailService = require('pubsweet-component-mail-service')
+
+module.exports = async (body, UserModel, res) => {
+  const { email, role, firstName, lastName, affiliation, title } = body
+
+  try {
+    const user = await UserModel.findByEmail(email)
+
+    if (user) {
+      logger.error('someone tried to invite existing user')
+      return res.status(400).json({ error: 'User already exists' })
+    }
+  } catch (e) {
+    if (e.name !== 'NotFoundError') {
+      logger.error(e)
+      return res.status(500).json({ error: e.details[0].message })
+    }
+
+    const newUser = await helpers.createNewUser(
+      email,
+      role,
+      firstName,
+      lastName,
+      affiliation,
+      title,
+      UserModel,
+    )
+
+    await mailService.setupInviteEmail(
+      newUser.email,
+      'invite',
+      newUser.passwordResetToken,
+    )
+
+    return res.status(200).json(newUser)
+  }
+}
diff --git a/packages/component-invite/src/routes/post.js b/packages/component-invite/src/routes/post.js
index 8284ef47e..e3c8b16c4 100644
--- a/packages/component-invite/src/routes/post.js
+++ b/packages/component-invite/src/routes/post.js
@@ -1,5 +1,4 @@
 const logger = require('@pubsweet/logger')
-const mailService = require('pubsweet-component-mail-service')
 const get = require('lodash/get')
 const config = require('config')
 const helpers = require('../helpers/helpers')
@@ -7,7 +6,7 @@ const helpers = require('../helpers/helpers')
 const configRoles = config.get('roles')
 
 module.exports = models => async (req, res) => {
-  const { email, role, firstName, lastName, affiliation, title } = req.body
+  const { email, role } = req.body
 
   if (!helpers.checkForUndefinedParams(email, role)) {
     res.status(400).json({ error: 'Email and role are required' })
@@ -15,94 +14,38 @@ module.exports = models => async (req, res) => {
     return
   }
 
-  const collectionId = get(req, 'params.collectionId')
   const reqUser = await models.User.find(req.user)
-  let collection
-
-  if (reqUser.admin && collectionId) {
-    res.status(403).json({
-      error: `admin cannot invite an ${role} to a collection`,
-    })
-    logger.error(`admin tried to invite a ${role} to a collection`)
-    return
-  } else if (reqUser.admin) {
-    reqUser.roles = reqUser.roles || ['admin']
-    const inviteRight = helpers.hasInviteRight(configRoles, reqUser.roles, role)
-    if (!inviteRight.success) {
-      res.status(inviteRight.status).json({
-        error: inviteRight.message,
-      })
-      logger.error(`incorrect role when inviting a ${role}`)
-      return
-    }
-  } else if (collectionId) {
-    if (!configRoles.collection.includes(role)) {
-      res
-        .status(403)
-        .json({ error: `Role ${role} cannot be set on collections` })
-      logger.error(`invitation has been attempted with invalid role: ${role}`)
-      return
-    }
-
-    const inviteRight = helpers.hasInviteRight(configRoles, reqUser.roles, role)
-    if (!inviteRight.success) {
-      res.status(inviteRight.status).json({
-        error: inviteRight.message,
-      })
-      return
-    }
-    try {
-      collection = await models.Collection.find(collectionId)
-    } catch (e) {
-      const notFoundError = helpers.handleNotFoundError(e, 'collection')
-      res.status(notFoundError.status).json({
-        error: notFoundError.message,
-      })
-      return
-    }
-  } else {
-    res.status(403).json({
-      error: `${reqUser.roles ||
-        'undefined roles'} cannot invite a ${role} without a collection`,
+  const collectionId = get(req, 'params.collectionId')
+  if (reqUser.admin) reqUser.roles = reqUser.roles || ['admin']
+  const inviteRight = helpers.hasInviteRight(configRoles, reqUser.roles, role)
+  if (!inviteRight.success) {
+    res.status(inviteRight.status).json({
+      error: inviteRight.message,
     })
-    logger.error(`cannot invite manuscript roles without a collection`)
+    logger.error(`incorrect role when inviting a ${role}`)
     return
   }
 
-  try {
-    const user = await models.User.findByEmail(email)
-
-    if (user) {
-      res.status(400).json({ error: 'User already exists' })
-      logger.error('someone tried to invite existing user')
-      return
-    }
-  } catch (e) {
-    if (e.name !== 'NotFoundError') {
-      res.status(500).json({ error: e.details[0].message })
-      logger.error(e)
-      return
-    }
-
-    const newUser = await helpers.createNewUser(
+  if (collectionId) {
+    return require('../controllers/inviteCollectionRole')(
       email,
       role,
-      firstName,
-      lastName,
-      affiliation,
-      title,
-      models.User,
+      reqUser,
+      res,
+      collectionId,
+      models,
+      `${req.protocol}://${req.get('host')}`,
     )
+  }
 
-    if (collection)
-      await helpers.createNewTeam(collection.id, newUser, models.Team)
-
-    await mailService.setupInviteEmail(
-      newUser.email,
-      'invite',
-      newUser.passwordResetToken,
+  if (reqUser.admin)
+    return require('../controllers/inviteGlobalRole')(
+      req.body,
+      models.User,
+      res,
     )
 
-    res.status(200).json(newUser)
-  }
+  res.status(403).json({
+    error: `${reqUser.roles} cannot invite a ${role} without a collection`,
+  })
 }
diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js
index 0e2075304..d592b48ae 100644
--- a/packages/component-invite/src/tests/fixtures/users.js
+++ b/packages/component-invite/src/tests/fixtures/users.js
@@ -32,6 +32,22 @@ const users = {
     id: 'handling123',
     roles: ['handlingEditor'],
   },
+  author: {
+    type: 'user',
+    username: 'author',
+    email: 'author@example.com',
+    password: 'test',
+    admin: false,
+    id: 'author123',
+    roles: ['author'],
+    passwordResetToken: 'token123',
+    firstName: 'leopold',
+    lastName: 'smith',
+    affiliation: 'MIT',
+    title: 'mr',
+    save: jest.fn(() => users.authorUser),
+    isConfirmed: false,
+  },
 }
 
 module.exports = users
diff --git a/packages/component-invite/src/tests/post.test.js b/packages/component-invite/src/tests/post.test.js
index a21c3effb..03db53ddd 100644
--- a/packages/component-invite/src/tests/post.test.js
+++ b/packages/component-invite/src/tests/post.test.js
@@ -10,6 +10,7 @@ const TeamMock = require('./mocks/Team')
 
 jest.mock('pubsweet-component-mail-service', () => ({
   setupInviteEmail: jest.fn(),
+  setupAssignEmail: jest.fn(),
 }))
 const chance = new Chance()
 const globalRoles = ['editorInChief', 'author', 'admin']
@@ -60,18 +61,16 @@ const notFoundError = new Error()
 notFoundError.name = 'NotFoundError'
 notFoundError.status = 404
 
-const adminUser = fixtures.users.admin
-const editorInChiefUser = fixtures.users.editorInChief
-const handlingEditorUser = fixtures.users.handlingEditor
-
+const { admin, editorInChief, handlingEditor, author } = fixtures.users
+const { standardCollection } = fixtures.collections
 describe('Post invite route handler', () => {
   it('should return success when the admin invites a global role', async () => {
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = adminUser
+    req.user = admin
     const res = httpMocks.createResponse()
-    const models = buildModels(notFoundError, adminUser, notFoundError)
+    const models = buildModels(notFoundError, admin, notFoundError)
     await require('../routes/post')(models)(req, res)
 
     expect(res.statusCode).toBe(200)
@@ -85,10 +84,10 @@ describe('Post invite route handler', () => {
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = adminUser
+    req.user = admin
     req.params.collectionId = '123'
     const res = httpMocks.createResponse()
-    const models = buildModels(notFoundError, adminUser)
+    const models = buildModels(notFoundError, admin)
     await require('../routes/post')(models)(req, res)
     expect(res.statusCode).toBe(403)
     const data = JSON.parse(res._getData())
@@ -102,9 +101,9 @@ describe('Post invite route handler', () => {
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = adminUser
+    req.user = admin
     const res = httpMocks.createResponse()
-    const models = buildModels(notFoundError, adminUser)
+    const models = buildModels(notFoundError, admin)
     await require('../routes/post')(models)(req, res)
     expect(res.statusCode).toBe(403)
     const data = JSON.parse(res._getData())
@@ -115,9 +114,9 @@ describe('Post invite route handler', () => {
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = adminUser
+    req.user = admin
     const res = httpMocks.createResponse()
-    const models = buildModels(notFoundError, adminUser)
+    const models = buildModels(notFoundError, admin)
     await require('../routes/post')(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
@@ -130,28 +129,46 @@ describe('Post invite route handler', () => {
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = editorInChiefUser
+    req.user = editorInChief
     req.params.collectionId = '123'
     const res = httpMocks.createResponse()
-    const models = buildModels(notFoundError, editorInChiefUser)
+    const models = buildModels(notFoundError, editorInChief)
     await require('../routes/post')(models)(req, res)
     expect(res.statusCode).toBe(403)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual(`Role ${body.role} cannot be set on collections`)
+    expect(data.error).toEqual(
+      `${req.user.roles[0]} cannot invite a ${body.role}`,
+    )
   })
-  it('should return an error when an non-admin invites without a collection', async () => {
-    body.role = manuscriptRoles[random(0, manuscriptRoles.length - 1)]
+  it('should return an error when an editorInChief invites a handlingEditor without a collection', async () => {
+    body.role = 'handlingEditor'
     body.admin = false
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = editorInChiefUser
+    req.user = editorInChief
     const res = httpMocks.createResponse()
 
-    const models = buildModels(notFoundError, editorInChiefUser)
+    const models = buildModels(notFoundError, editorInChief)
+    await require('../routes/post')(models)(req, res)
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `${req.user.roles} cannot invite a ${body.role} without a collection`,
+    )
+  })
+  it('should return an error when an handlingEditor invites a reviewer without a collection', async () => {
+    body.role = 'reviewer'
+    body.admin = false
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor
+    const res = httpMocks.createResponse()
+
+    const models = buildModels(notFoundError, handlingEditor)
     await require('../routes/post')(models)(req, res)
     expect(res.statusCode).toBe(403)
-    // console.log(res._getData())
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual(
       `${req.user.roles} cannot invite a ${body.role} without a collection`,
@@ -163,57 +180,51 @@ describe('Post invite route handler', () => {
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = adminUser
+    req.user = admin
     const res = httpMocks.createResponse()
-    const models = buildModels(notFoundError, adminUser, editorInChiefUser)
+    const models = buildModels(notFoundError, admin, editorInChief)
     await require('../routes/post')(models)(req, res)
 
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('User already exists')
   })
-  it('should return success when the editor in chief invites a handling Editor with a collection', async () => {
-    body.role = 'handlingEditor'
-    body.admin = false
+  it('should return success when the editor in chief invites a handlingEditor with a collection', async () => {
+    const body = {
+      email: author.email,
+      role: 'handlingEditor',
+    }
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = editorInChiefUser
+    req.user = editorInChief
     req.params.collectionId = '123'
     const res = httpMocks.createResponse()
-    const models = buildModels(
-      fixtures.collections.standardCollection,
-      editorInChiefUser,
-      notFoundError,
-    )
+    const models = buildModels(standardCollection, editorInChief, author)
     await require('../routes/post')(models)(req, res)
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
-    expect(data.roles[0]).toEqual(body.role)
-    expect(data.firstName).toEqual(body.firstName)
+    expect(data.roles).toContain(body.role)
     expect(data.email).toEqual(body.email)
   })
-  it('should return success when the handlintEditor invites a reviewer with a collection', async () => {
-    body.role = 'reviewer'
-    body.admin = false
+  it('should return success when the handlingEditor invites a reviewer with a collection', async () => {
+    const body = {
+      email: author.email,
+      role: 'reviewer',
+    }
     const req = httpMocks.createRequest({
       body,
     })
-    req.user = handlingEditorUser
+    req.user = handlingEditor
     req.params.collectionId = '123'
     const res = httpMocks.createResponse()
-    const models = buildModels(
-      fixtures.collections.standardCollection,
-      handlingEditorUser,
-      notFoundError,
-    )
+    const models = buildModels(standardCollection, handlingEditor, author)
     await require('../routes/post')(models)(req, res)
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
-    expect(data.roles[0]).toEqual(body.role)
-    expect(data.firstName).toEqual(body.firstName)
+    expect(data.roles).toContain(body.role)
     expect(data.email).toEqual(body.email)
   })
 })
diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js
index cbcc4b2db..246ef41f3 100644
--- a/packages/component-mail-service/src/Mail.js
+++ b/packages/component-mail-service/src/Mail.js
@@ -7,14 +7,9 @@ const config = require('config')
 const resetUrl = config.get('invite-reset-password.url')
 
 module.exports = {
-  setupInviteEmail: async (email, emailType, token, comment = '') => {
+  setupInviteEmail: async (email, emailType, token) => {
     let subject
-    const htmlFile = readFile(`${__dirname}/templates/${emailType}.html`)
-    const textFile = readFile(`${__dirname}/templates/${emailType}.txt`)
     let replacements = {}
-    const htmlTemplate = handlebars.compile(htmlFile)
-    const textTemplate = handlebars.compile(textFile)
-
     switch (emailType) {
       case 'invite':
         subject = 'Hindawi Invitation'
@@ -30,11 +25,39 @@ module.exports = {
         break
     }
 
-    const htmlBody = htmlTemplate(replacements)
-    const textBody = textTemplate(replacements)
+    const { htmlBody, textBody } = getEmailBody(emailType, replacements)
 
     Email.send(email, subject, textBody, htmlBody)
   },
+  setupAssignEmail: async (email, emailType, dashBoardUrl) => {
+    let subject
+    let replacements = {}
+    switch (emailType) {
+      case 'assign-handling-editor':
+        subject = 'Hindawi Handling Editor Invitation'
+        replacements = {
+          url: dashBoardUrl,
+        }
+        break
+      default:
+        subject = 'Welcome to Hindawi!'
+        break
+    }
+
+    const { htmlBody, textBody } = getEmailBody(emailType, replacements)
+
+    Email.send(email, subject, textBody, htmlBody)
+  },
+}
+
+const getEmailBody = (emailType, replacements) => {
+  const htmlFile = readFile(`${__dirname}/templates/${emailType}.html`)
+  const textFile = readFile(`${__dirname}/templates/${emailType}.txt`)
+  const htmlTemplate = handlebars.compile(htmlFile)
+  const textTemplate = handlebars.compile(textFile)
+  const htmlBody = htmlTemplate(replacements)
+  const textBody = textTemplate(replacements)
+  return { htmlBody, textBody }
 }
 
 const readFile = path =>
diff --git a/packages/component-mail-service/src/templates/assign-handling-editor.html b/packages/component-mail-service/src/templates/assign-handling-editor.html
new file mode 100644
index 000000000..72a0f88f6
--- /dev/null
+++ b/packages/component-mail-service/src/templates/assign-handling-editor.html
@@ -0,0 +1,232 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
+  <!--[if !mso]><!-->
+  <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+  <!--<![endif]-->
+  <!--[if (gte mso 9)|(IE)]>
+    <xml>
+    <o:OfficeDocumentSettings>
+    <o:AllowPNG/>
+    <o:PixelsPerInch>96</o:PixelsPerInch>
+    </o:OfficeDocumentSettings>
+    </xml>
+    <![endif]-->
+  <!--[if (gte mso 9)|(IE)]>
+    <style type="text/css">
+      body {width: 600px;margin: 0 auto;}
+      table {border-collapse: collapse;}
+      table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;}
+      img {-ms-interpolation-mode: bicubic;}
+    </style>
+    <![endif]-->
+
+  <style type="text/css">
+    body,
+    p,
+    div {
+      font-family: helvetica, arial, sans-serif;
+      font-size: 14px;
+    }
+
+    body {
+      color: #626262;
+    }
+
+    body a {
+      color: #0D78F2;
+      text-decoration: none;
+    }
+
+    p {
+      margin: 0;
+      padding: 0;
+    }
+
+    table.wrapper {
+      width: 100% !important;
+      table-layout: fixed;
+      -webkit-font-smoothing: antialiased;
+      -webkit-text-size-adjust: 100%;
+      -moz-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+    }
+
+    img.max-width {
+      max-width: 100% !important;
+    }
+
+    .column.of-2 {
+      width: 50%;
+    }
+
+    .column.of-3 {
+      width: 33.333%;
+    }
+
+    .column.of-4 {
+      width: 25%;
+    }
+
+    @media screen and (max-width:480px) {
+      .preheader .rightColumnContent,
+      .footer .rightColumnContent {
+        text-align: left !important;
+      }
+      .preheader .rightColumnContent div,
+      .preheader .rightColumnContent span,
+      .footer .rightColumnContent div,
+      .footer .rightColumnContent span {
+        text-align: left !important;
+      }
+      .preheader .rightColumnContent,
+      .preheader .leftColumnContent {
+        font-size: 80% !important;
+        padding: 5px 0;
+      }
+      table.wrapper-mobile {
+        width: 100% !important;
+        table-layout: fixed;
+      }
+      img.max-width {
+        height: auto !important;
+        max-width: 480px !important;
+      }
+      a.bulletproof-button {
+        display: block !important;
+        width: auto !important;
+        font-size: 80%;
+        padding-left: 0 !important;
+        padding-right: 0 !important;
+      }
+      .columns {
+        width: 100% !important;
+      }
+      .column {
+        display: block !important;
+        width: 100% !important;
+        padding-left: 0 !important;
+        padding-right: 0 !important;
+        margin-left: 0 !important;
+        margin-right: 0 !important;
+      }
+    }
+  </style>
+  <!--user entered Head Start-->
+
+  <!--End Head user entered-->
+</head>
+
+<body>
+  <center class="wrapper" data-link-color="#0D78F2" data-body-style="font-size: 14px; font-family: helvetica,arial,sans-serif; color: #626262; background-color: #F4F4F4;">
+    <div class="webkit">
+      <table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F4F4F4">
+        <tr>
+          <td valign="top" bgcolor="#F4F4F4" width="100%">
+            <table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0">
+              <tr>
+                <td width="100%">
+                  <table width="100%" cellpadding="0" cellspacing="0" border="0">
+                    <tr>
+                      <td>
+                        <!--[if mso]>
+                          <center>
+                          <table><tr><td width="600">
+                          <![endif]-->
+                        <table width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width:600px;" align="center">
+                          <tr>
+                            <td role="modules-container" style="padding: 0px 0px 0px 0px; color: #626262; text-align: left;" bgcolor="#F4F4F4" width="100%"
+                              align="left">
+
+                              <table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0"
+                                width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
+                                <tr>
+                                  <td role="module-content">
+                                    <p>handling editor</p>
+                                  </td>
+                                </tr>
+                              </table>
+
+                              <table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;">
+                                <tr>
+                                  <td style="font-size:6px;line-height:10px;padding:20px 0px 20px 0px;" valign="top" align="center">
+                                    <img class="max-width" border="0" style="display:block;color:#000000;text-decoration:none;font-family:Helvetica, arial, sans-serif;font-size:16px;max-width:10% !important;width:10%;height:auto !important;"
+                                      src="https://marketing-image-production.s3.amazonaws.com/uploads/bb39b20cf15e52c1c0933676e25f2b2402737c6560b8098c204ad6932b84eb2058804376dbc4db138c7a21dcaed9325bde36185648afac5bc97e3d73d4e12718.png"
+                                      alt="" width="60">
+                                  </td>
+                                </tr>
+                              </table>
+
+                              <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;">
+                                <tr>
+                                  <td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff">
+                                    <h1 style="text-align: center;">You have been assigned as a Handling Editor to a manuscript.</h1>
+
+                                    <div style="text-align: center;">Please click on the link below to access your dashboard.</div>
+
+                                    <div style="text-align: center;">&nbsp;</div>
+
+                                    <div style="text-align: center;">&nbsp;</div>
+
+                                  </td>
+                                </tr>
+                              </table>
+                              <table border="0" cellPadding="0" cellSpacing="0" class="module" data-role="module-button" data-type="button" role="module"
+                                style="table-layout:fixed" width="100%">
+                                <tbody>
+                                  <tr>
+                                    <td align="center" bgcolor="#FFFFFF" class="outer-td" style="padding:0px 0px 30px 0px;background-color:#FFFFFF">
+                                      <table border="0" cellPadding="0" cellSpacing="0" class="button-css__deep-table___2OZyb wrapper-mobile"
+                                        style="text-align:center">
+                                        <tbody>
+                                          <tr>
+                                            <td align="center" bgcolor="#0d78f2" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit">
+                                              <a href="{{ url }}" style="background-color:#0d78f2;border:1px solid #333333;border-color:#0d78f2;border-radius:0px;border-width:1px;color:#ffffff;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none"
+                                                target="_blank">VIEW DASHBOARD</a>
+                                            </td>
+                                          </tr>
+                                        </tbody>
+                                      </table>
+                                    </td>
+                                  </tr>
+                                </tbody>
+                              </table>
+                              <div data-role="module-unsubscribe" class="module unsubscribe-css__unsubscribe___2CDlR" role="module"
+                                data-type="unsubscribe" style="color:#444444;font-size:12px;line-height:20px;padding:16px 16px 16px 16px;text-align:center">
+                                <div class="Unsubscribe--addressLine">
+                                  <p class="Unsubscribe--senderName" style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px">Hindawi Publishing Corporation</p>
+                                  <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px">
+                                    <span class="Unsubscribe--senderAddress">315 Madison Ave, Third Floor, Suite 3070</span>,
+                                    <span class="Unsubscribe--senderCity">NEW YORK</span>,
+                                    <span class="Unsubscribe--senderState">NY</span>
+                                    <span class="Unsubscribe--senderZip">10017</span>
+                                  </p>
+                                </div>
+                                <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px">
+                                  <a class="Unsubscribe--unsubscribeLink" href="[Unsubscribe]">Unsubscribe</a>
+                                </p>
+                              </div>
+                            </td>
+                          </tr>
+                        </table>
+                        <!--[if mso]>
+                          </td></tr></table>
+                          </center>
+                          <![endif]-->
+                      </td>
+                    </tr>
+                  </table>
+                </td>
+              </tr>
+            </table>
+          </td>
+        </tr>
+      </table>
+    </div>
+  </center>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/packages/component-mail-service/src/templates/assign-handling-editor.txt b/packages/component-mail-service/src/templates/assign-handling-editor.txt
new file mode 100644
index 000000000..a1c6edf27
--- /dev/null
+++ b/packages/component-mail-service/src/templates/assign-handling-editor.txt
@@ -0,0 +1,6 @@
+You have been assigned as a Handling Editor to a manuscript.
+Please click on the link below to access your dashboard
+{{ url }} VIEW DASHBOARD
+Hindawi Publishing Corporation
+315 Madison Ave, Third Floor, Suite 307
+New York, NY 10017
\ No newline at end of file
diff --git a/packages/component-send-email/package.json b/packages/component-send-email/package.json
index abbf0d925..42a1c5d72 100644
--- a/packages/component-send-email/package.json
+++ b/packages/component-send-email/package.json
@@ -12,7 +12,6 @@
   },
   "dependencies": {
     "aws-sdk": "^2.185.0",
-    "body-parser": "^1.17.2",
     "nodemailer": "^4.4.2"
   },
   "peerDependencies": {
-- 
GitLab