From 317acbab8ce806bc8580d624e2de7253830ef8c6 Mon Sep 17 00:00:00 2001
From: Sebastian <sebastian.mihalache@thinslices.com>
Date: Tue, 24 Apr 2018 19:48:28 +0300
Subject: [PATCH] feat(component-invite): resend reviewer invite

---
 .../src/helpers/Collection.js                 | 167 +++++----
 .../src/helpers/Invitation.js                 |  69 ++--
 .../src/routes/collectionsInvitations/post.js | 102 +++---
 .../tests/collectionsInvitations/post.test.js |   6 +-
 packages/component-mail-service/src/Mail.js   |  61 ++--
 .../src/templates/invite-reviewer.html        |   2 +-
 .../src/templates/invite-reviewer.txt         |   2 +-
 .../src/templates/resend-reviewer.html        | 335 ++++++++++++++++++
 .../src/templates/resend-reviewer.txt         |  20 ++
 9 files changed, 579 insertions(+), 185 deletions(-)
 create mode 100644 packages/component-mail-service/src/templates/resend-reviewer.html
 create mode 100644 packages/component-mail-service/src/templates/resend-reviewer.txt

diff --git a/packages/component-invite/src/helpers/Collection.js b/packages/component-invite/src/helpers/Collection.js
index a673d629b..c1a4c3a33 100644
--- a/packages/component-invite/src/helpers/Collection.js
+++ b/packages/component-invite/src/helpers/Collection.js
@@ -5,85 +5,98 @@ const config = require('config')
 const last = require('lodash/last')
 
 const statuses = config.get('statuses')
-module.exports = {
-  addInvitation: async (collection, userId, role) => {
-    collection.invitations = collection.invitations || []
-    let matchingInvitation = collection.invitations.find(
-      invitation =>
-        invitation.userId === userId &&
-        invitation.role === role &&
-        invitation.hasAnswer === false,
+
+const addInvitation = async (collection, userId, role) => {
+  collection.invitations = collection.invitations || []
+  let matchingInvitation = collection.invitations.find(
+    invitation =>
+      invitation.userId === userId &&
+      invitation.role === role &&
+      invitation.hasAnswer === false,
+  )
+  if (matchingInvitation === undefined) {
+    matchingInvitation = await invitationHelper.setupInvitation(
+      userId,
+      role,
+      collection,
     )
-    if (matchingInvitation === undefined) {
-      matchingInvitation = await invitationHelper.setupInvitation(
-        userId,
-        role,
-        collection,
-      )
-    }
+  } else {
     matchingInvitation.timestamp = Date.now()
-    await collection.save()
-    return matchingInvitation
-  },
-  addAuthor: async (collection, user, res, url) => {
-    if (collection.owners.includes(user.id)) {
-      return res.status(200).json(user)
-    }
-    try {
-      await mailService.setupAssignEmail(user.email, 'assign-author', url)
+  }
 
-      return res.status(200).json(user)
-    } catch (e) {
-      logger.error(e)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
-  },
-  addHandlingEditor: async (collection, user, invitation) => {
-    collection.handlingEditor = {
-      id: user.id,
-      name: `${user.firstName} ${user.lastName}`,
-      timestamp: invitation.timestamp,
-      email: user.email,
-      hasAnswer: invitation.hasAnswer,
-      isAccepted: invitation.isAccepted,
-    }
-    collection.status = 'heInvited'
-    collection.visibleStatus = statuses[collection.status].private
-    await collection.save()
-  },
-  updateHandlingEditor: async (collection, isAccepted) => {
-    collection.handlingEditor.hasAnswer = true
-    collection.handlingEditor.isAccepted = isAccepted
-    collection.handlingEditor.timestamp = Date.now()
-    if (isAccepted) {
-      collection.status = 'heAssigned'
-    } else {
-      collection.status = 'submitted'
-    }
-    collection.visibleStatus = statuses[collection.status].private
-    await collection.save()
-  },
-  getFragmentAndAuthorData: async (models, collection) => {
-    const fragment = await models.Fragment.find(last(collection.fragments))
-    let { title } = fragment.metadata
-    title = title.replace(/<(.|\n)*?>/g, '')
+  await collection.save()
+  return matchingInvitation
+}
 
-    const submittingAuthorData = collection.authors.find(
-      author => author.isSubmitting === true,
-    )
-    const author = await models.User.find(submittingAuthorData.userId)
-    const authorName = `${author.firstName} ${author.lastName}`
-    const { id } = fragment
-    return { title, authorName, id }
-  },
-  updateReviewerCollectionStatus: async collection => {
-    const reviewerInvitations = collection.invitations.filter(
-      inv => inv.role === 'reviewer',
-    )
-    if (reviewerInvitations.length === 0) {
-      collection.status = 'heAssigned'
-      collection.visibleStatus = statuses[collection.status].private
-    }
-    await collection.save()
-  },
+const addAuthor = async (collection, user, res, url) => {
+  if (collection.owners.includes(user.id)) {
+    return res.status(200).json(user)
+  }
+  try {
+    await mailService.setupAssignEmail(user.email, 'assign-author', url)
+
+    return res.status(200).json(user)
+  } catch (e) {
+    logger.error(e)
+    return res.status(500).json({ error: 'Email could not be sent.' })
+  }
+}
+
+const addHandlingEditor = async (collection, user, invitation) => {
+  collection.handlingEditor = {
+    id: user.id,
+    name: `${user.firstName} ${user.lastName}`,
+    timestamp: invitation.timestamp,
+    email: user.email,
+    hasAnswer: invitation.hasAnswer,
+    isAccepted: invitation.isAccepted,
+  }
+  await updateStatus(collection, 'heInvited')
+}
+
+const updateHandlingEditor = async (collection, isAccepted) => {
+  collection.handlingEditor.hasAnswer = true
+  collection.handlingEditor.isAccepted = isAccepted
+  collection.handlingEditor.timestamp = Date.now()
+  let status
+  isAccepted ? (status = 'heAssigned') : (status = 'submitted')
+  await updateStatus(collection, status)
+}
+
+const getFragmentAndAuthorData = async (models, collection) => {
+  const fragment = await models.Fragment.find(last(collection.fragments))
+  let { title } = fragment.metadata
+  title = title.replace(/<(.|\n)*?>/g, '')
+
+  const submittingAuthorData = collection.authors.find(
+    author => author.isSubmitting === true,
+  )
+  const author = await models.User.find(submittingAuthorData.userId)
+  const authorName = `${author.firstName} ${author.lastName}`
+  const { id } = fragment
+  return { title, authorName, id }
+}
+
+const updateReviewerCollectionStatus = async collection => {
+  const reviewerInvitations = collection.invitations.filter(
+    inv => inv.role === 'reviewer',
+  )
+  if (reviewerInvitations.length === 0) return
+  await updateStatus(collection, 'heAssigned')
+}
+
+const updateStatus = async (collection, newStatus) => {
+  collection.status = newStatus
+  collection.visibleStatus = statuses[collection.status].private
+  await collection.save()
+}
+
+module.exports = {
+  addInvitation,
+  addAuthor,
+  addHandlingEditor,
+  updateHandlingEditor,
+  getFragmentAndAuthorData,
+  updateReviewerCollectionStatus,
+  updateStatus,
 }
diff --git a/packages/component-invite/src/helpers/Invitation.js b/packages/component-invite/src/helpers/Invitation.js
index 855b265cf..29a252e71 100644
--- a/packages/component-invite/src/helpers/Invitation.js
+++ b/packages/component-invite/src/helpers/Invitation.js
@@ -30,53 +30,66 @@ const setupInvitation = async (userId, role, collection) => {
   return invitation
 }
 
-const setupReviewerInvitation = async (
-  req,
-  models,
+const setupReviewerInvitation = async ({
+  baseUrl,
+  FragmentModel,
+  UserModel,
   collection,
   user,
   mailService,
   invitationId,
-  invitationTimestamp,
-) => {
-  const baseUrl = `${req.protocol}://${req.get('host')}`
-  const fragment = await models.Fragment.find(collection.fragments[0])
+  timestamp,
+  resend = false,
+}) => {
+  const fragment = await FragmentModel.find(collection.fragments[0])
   const submittingAuthorData = collection.authors.find(
     author => author.isSubmitting === true,
   )
-  const submittingAuthor = await models.User.find(submittingAuthorData.userId)
+  const submittingAuthor = await UserModel.find(submittingAuthorData.userId)
   const authorsPromises = collection.authors.map(async author => {
-    const user = await models.User.find(author.userId)
+    const user = await UserModel.find(author.userId)
     return `${user.firstName} ${user.lastName}`
   })
   const authors = await Promise.all(authorsPromises)
   let { abstract, title } = fragment.metadata
   title = title.replace(/<(.|\n)*?>/g, '')
   abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : ''
-  const date = new Date(invitationTimestamp)
-  let expectedDate = date.getDate() + 14
-  date.setDate(expectedDate)
-  expectedDate = date.toLocaleDateString('en-US', {
-    day: 'numeric',
-    month: 'long',
-    year: 'numeric',
-  })
-  await mailService.setupReviewerInvitationEmail(
-    user,
+
+  const params = {
+    invitedUser: user,
     baseUrl,
-    collection,
-    abstract,
-    title,
-    submittingAuthor,
-    authors,
-    fragment.id,
-    invitationId,
-    expectedDate,
-  )
+    collection: {
+      id: collection.id,
+      authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
+      handlingEditor: collection.handlingEditor,
+    },
+    subject: `${collection.customId}: Review Requested`,
+    fragment: {
+      id: fragment.id,
+      title,
+      abstract,
+      authors,
+    },
+    invitation: {
+      id: invitationId,
+      timestamp,
+    },
+    resend,
+  }
+  await mailService.setupReviewerInvitationEmail(params)
 }
 
+const getInvitation = (invitations = [], userId, role) =>
+  invitations.find(
+    invitation =>
+      invitation.userId === userId &&
+      invitation.role === role &&
+      invitation.hasAnswer === false,
+  )
+
 module.exports = {
   getInvitationData,
   setupInvitation,
   setupReviewerInvitation,
+  getInvitation,
 }
diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js
index abffa83d3..09987c365 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/post.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/post.js
@@ -9,7 +9,7 @@ const userHelper = require('../../helpers/User')
 const invitationHelper = require('../../helpers/Invitation')
 
 const configRoles = config.get('roles')
-const statuses = config.get('statuses')
+
 module.exports = models => async (req, res) => {
   const { email, role } = req.body
 
@@ -40,51 +40,64 @@ module.exports = models => async (req, res) => {
       error: notFoundError.message,
     })
   }
-  const url = `${req.protocol}://${req.get('host')}`
+  const baseUrl = `${req.protocol}://${req.get('host')}`
+  const params = {
+    baseUrl,
+    FragmentModel: models.Fragment,
+    UserModel: models.User,
+    collection,
+    mailService,
+    resend: false,
+  }
 
   try {
     const user = await models.User.findByEmail(email)
 
     await teamHelper.setupManuscriptTeam(models, user, collectionId, role)
-
-    // find if there already is a matching invitation in the collection
-    const invitation = await collectionHelper.addInvitation(
-      collection,
+    let invitation = invitationHelper.getInvitation(
+      collection.invitations,
       user.id,
       role,
     )
-    if (role === 'handlingEditor') {
-      await collectionHelper.addHandlingEditor(collection, user, invitation)
-    }
-    if (role === 'reviewer' && collection.status === 'heAssigned') {
-      collection.status = 'reviewersInvited'
-      collection.visibleStatus = statuses[collection.status].private
-      collection = await collection.save()
+
+    let resend = false
+    if (invitation !== undefined) {
+      invitation.timestamp = Date.now()
+      await collection.save()
+      resend = true
+    } else {
+      invitation = await invitationHelper.setupInvitation(
+        user.id,
+        role,
+        collection,
+      )
     }
+
     try {
+      if (role === 'reviewer') {
+        if (collection.status === 'heAssigned')
+          await collectionHelper.updateStatus(collection, 'reviewersInvited')
+
+        await invitationHelper.setupReviewerInvitation({
+          ...params,
+          user,
+          invitationId: invitation.id,
+          timestamp: invitation.timestamp,
+          resend,
+        })
+      }
+
       if (role === 'handlingEditor') {
+        invitation.timestamp = Date.now()
+        await collection.save()
+        await collectionHelper.addHandlingEditor(collection, user, invitation)
         await mailService.setupAssignEmail(
           user.email,
           'assign-handling-editor',
-          url,
-        )
-      } else if (role === 'reviewer') {
-        await invitationHelper.setupReviewerInvitation(
-          req,
-          models,
-          collection,
-          user,
-          mailService,
-          invitation.id,
-          invitation.timestamp,
+          baseUrl,
         )
-      } else {
-        return res.status(500).json({
-          error: 'Something went wrong',
-        })
       }
-
-      return res.status(200).json(user)
+      return res.status(200).json(invitation)
     } catch (e) {
       logger.error(e)
       return res.status(500).json({ error: 'Email could not be sent.' })
@@ -93,7 +106,7 @@ module.exports = models => async (req, res) => {
     if (role === 'reviewer') {
       const newUser = await userHelper.setupNewUser(
         req.body,
-        url,
+        baseUrl,
         res,
         email,
         role,
@@ -105,27 +118,22 @@ module.exports = models => async (req, res) => {
           error: newUser.message,
         })
       }
-      if (collection.status === 'heAssigned') {
-        collection.status = 'reviewersInvited'
-        collection.visibleStatus = statuses[collection.status].private
-        collection = await collection.save()
-      }
+      if (collection.status === 'heAssigned')
+        await collectionHelper.updateStatus(collection, 'reviewersInvited')
       await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role)
-      const invitation = await collectionHelper.addInvitation(
-        collection,
+      const invitation = await invitationHelper.setupInvitation(
         newUser.id,
         role,
-      )
-      await invitationHelper.setupReviewerInvitation(
-        req,
-        models,
         collection,
-        newUser,
-        mailService,
-        invitation.id,
-        invitation.timestamp,
       )
-      return res.status(200).json(newUser)
+
+      await invitationHelper.setupReviewerInvitation({
+        ...params,
+        newUser,
+        invitationId: invitation.id,
+        timestamp: invitation.timestamp,
+      })
+      return res.status(200).json(invitation)
     }
     const notFoundError = await helpers.handleNotFoundError(e, 'user')
     return res.status(notFoundError.status).json({
diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js
index 5378ad0e6..d72bd114d 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js
@@ -69,7 +69,7 @@ describe('Post collections invitations route handler', () => {
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
-    expect(data.email).toEqual(body.email)
+    expect(data.role).toEqual(body.role)
   })
   it('should return success when the a reviewer is invited', async () => {
     const { user, editorInChief } = testFixtures.users
@@ -88,7 +88,7 @@ describe('Post collections invitations route handler', () => {
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
-    expect(data.email).toEqual(body.email)
+    expect(data.role).toEqual(body.role)
   })
   it('should return an error when inviting his self', async () => {
     const { editorInChief } = testFixtures.users
@@ -134,6 +134,6 @@ describe('Post collections invitations route handler', () => {
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
-    expect(data.email).toEqual(body.email)
+    expect(data.role).toEqual(body.role)
   })
 })
diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js
index 17bcee132..dc5282a44 100644
--- a/packages/component-mail-service/src/Mail.js
+++ b/packages/component-mail-service/src/Mail.js
@@ -137,27 +137,23 @@ module.exports = {
 
     return Email.send(mailData)
   },
-  setupReviewerInvitationEmail: async (
+  setupReviewerInvitationEmail: async ({
     invitedUser,
     baseUrl,
-    collection,
-    abstract,
-    title,
-    submittingAuthor,
-    authors,
-    fragmentId,
-    invitationId,
-    expectedDate,
-  ) => {
-    const subject = `${collection.customId}: Review Requested`
-    let agreeUrl = `${baseUrl}/projects/${
-      collection.id
-    }/versions/${fragmentId}/details?${querystring.encode({
-      invitationId,
-      agree: true,
-    })}`
+    collection: { id: collectionId, authorName, handlingEditor },
+    fragment: { title, id: fragmentId, abstract, authors },
+    invitation: { id: invitationId, timestamp },
+    resend,
+    subject,
+  }) => {
+    let agreeUrl = `${baseUrl}/projects/${collectionId}/versions/${fragmentId}/details?${querystring.encode(
+      {
+        invitationId,
+        agree: true,
+      },
+    )}`
     const declineUrl = `${baseUrl}${resetPasswordPath}?${querystring.encode({
-      collectionId: collection.id,
+      collectionId,
       agree: false,
       invitationId,
       invitationToken: invitedUser.invitationToken,
@@ -166,30 +162,39 @@ module.exports = {
       agreeUrl = `${baseUrl}${resetPasswordPath}?${querystring.encode({
         email: invitedUser.email,
         token: invitedUser.passwordResetToken,
-        collectionId: collection.id,
+        collectionId,
         fragmentId,
         agree: true,
         invitationId,
       })}`
     }
 
+    const daysExpected = resend ? 0 : 14
+
+    const date = new Date(timestamp)
+    let expectedDate = date.getDate() + daysExpected
+    date.setDate(expectedDate)
+    expectedDate = date.toLocaleDateString('en-US', {
+      day: 'numeric',
+      month: 'long',
+      year: 'numeric',
+    })
+
     const replacements = {
-      name: `${invitedUser.firstName} ${invitedUser.lastName}`,
+      reviewerName: `${invitedUser.firstName} ${invitedUser.lastName}`,
       agreeUrl,
       declineUrl,
       baseUrl,
       title,
+      editorName: handlingEditor.name,
+      submittingAuthorName: authorName,
+      expectedDate,
       abstract,
-      editorName: collection.handlingEditor.name,
-      editorEmail: collection.handlingEditor.email,
-      submittingAuthorName: `${submittingAuthor.firstName} ${
-        submittingAuthor.lastName
-      }`,
+      editorEmail: handlingEditor.email,
       authors,
-      expectedDate,
     }
-
-    const { htmlBody, textBody } = getEmailBody('invite-reviewer', replacements)
+    const emailTemplate = resend ? 'resend-reviewer' : 'invite-reviewer'
+    const { htmlBody, textBody } = getEmailBody(emailTemplate, replacements)
     const mailData = {
       from: config.get('mailer.from'),
       to: invitedUser.email,
diff --git a/packages/component-mail-service/src/templates/invite-reviewer.html b/packages/component-mail-service/src/templates/invite-reviewer.html
index d286cb63c..cfe6ed5b4 100644
--- a/packages/component-mail-service/src/templates/invite-reviewer.html
+++ b/packages/component-mail-service/src/templates/invite-reviewer.html
@@ -164,7 +164,7 @@
                                 <tr>
                                   <td style="padding:30px 23px 20px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff">
                                     <div>
-                                      <p data-pm-slice="1 1 []">Dear {{name}},</p>
+                                      <p data-pm-slice="1 1 []">Dear {{reviewerName}},</p>
 
                                       <p>&nbsp;</p>
 
diff --git a/packages/component-mail-service/src/templates/invite-reviewer.txt b/packages/component-mail-service/src/templates/invite-reviewer.txt
index b12453439..946571853 100644
--- a/packages/component-mail-service/src/templates/invite-reviewer.txt
+++ b/packages/component-mail-service/src/templates/invite-reviewer.txt
@@ -1,5 +1,5 @@
 
-Dear Dr. {{name}},
+Dear Dr. {{reviewerName}},
 
 A manuscript titled {{title}}" by {{submittingAuthorName}}, has been submitted
 for possible publication in Hindawi. As the Academic Editor handling the manuscript,
diff --git a/packages/component-mail-service/src/templates/resend-reviewer.html b/packages/component-mail-service/src/templates/resend-reviewer.html
new file mode 100644
index 000000000..8840a4579
--- /dev/null
+++ b/packages/component-mail-service/src/templates/resend-reviewer.html
@@ -0,0 +1,335 @@
+<!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>review request</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 20px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff">
+                                    <div>
+                                      <p data-pm-slice="1 1 []">Dear {{reviewerName}},</p>
+
+                                      <p>&nbsp;</p>
+                                      <p>On {{expectedDate}}, I sent you a request to review the manuscript titled &quot;{{title}}&quot;
+                                        by {{submittingAuthorName}}, submitted for possible publication in Hindawi.
+                                      </p>
+                                      <p>I would be grateful if you would agree to review it and let me know whether you feel
+                                        it is suitable for publication. If you are unable to review this manuscript then
+                                        please decline to review. More details are available by clicking the link.</p>
+                                    </div>
+
+                                  </td>
+                                </tr>
+                              </table>
+
+                              <table border="0" cellpadding="0" cellspacing="0" align="center" width="100%" role="module" data-type="columns" data-version="2"
+                                style="padding:20px 0px 20px 0px;background-color:#ffffff;box-sizing:border-box;" bgcolor="#ffffff">
+                                <tr role='module-content'>
+                                  <td height="100%" valign="top">
+                                    <!--[if (gte mso 9)|(IE)]>
+              <center>
+                <table cellpadding="0" cellspacing="0" border="0" width="100%" style="border-spacing:0;border-collapse:collapse;table-layout: fixed;" >
+                  <tr>
+            <![endif]-->
+
+                                    <!--[if (gte mso 9)|(IE)]>
+      <td width="300.000px" valign="top" style="padding: 0px 0px 0px 0px;border-collapse: collapse;" >
+    <![endif]-->
+
+                                    <table width="300.000" style="width:300.000px;border-spacing:0;border-collapse:collapse;margin:0px 0px 0px 0px;" cellpadding="0"
+                                      cellspacing="0" align="left" border="0" bgcolor="#ffffff" class="column column-0 of-2
+                  empty">
+                                      <tr>
+                                        <td style="padding:0px;margin:0px;border-spacing:0;">
+                                          <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" class="outer-td" style="padding:0px 0px 0px 0px">
+                                                  <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 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"
+                                                            href="{{agreeUrl}}" target="_blank">AGREE</a>
+                                                        </td>
+                                                      </tr>
+                                                    </tbody>
+                                                  </table>
+                                                </td>
+                                              </tr>
+                                            </tbody>
+                                          </table>
+                                        </td>
+                                      </tr>
+                                    </table>
+
+                                    <!--[if (gte mso 9)|(IE)]>
+      </td>
+    <![endif]-->
+                                    <!--[if (gte mso 9)|(IE)]>
+      <td width="300.000px" valign="top" style="padding: 0px 0px 0px 0px;border-collapse: collapse;" >
+    <![endif]-->
+
+                                    <table width="300.000" style="width:300.000px;border-spacing:0;border-collapse:collapse;margin:0px 0px 0px 0px;" cellpadding="0"
+                                      cellspacing="0" align="left" border="0" bgcolor="#ffffff" class="column column-1 of-2
+                  empty">
+                                      <tr>
+                                        <td style="padding:0px;margin:0px;border-spacing:0;">
+                                          <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" class="outer-td" style="padding:0px 0px 0px 0px">
+                                                  <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="#e4dfdf" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit">
+                                                          <a style="background-color:#e4dfdf;border:1px solid #333333;border-color:#E4DFDF;border-radius:0px;border-width:1px;color:#302e2e;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"
+                                                            href="{{declineUrl}}" target="_blank">DECLINE</a>
+                                                        </td>
+                                                      </tr>
+                                                    </tbody>
+                                                  </table>
+                                                </td>
+                                              </tr>
+                                            </tbody>
+                                          </table>
+                                        </td>
+                                      </tr>
+                                    </table>
+
+                                    <!--[if (gte mso 9)|(IE)]>
+      </td>
+    <![endif]-->
+                                    <!--[if (gte mso 9)|(IE)]>
+                  <tr>
+                </table>
+              </center>
+            <![endif]-->
+                                  </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">
+                                    <p data-pm-slice="1 1 []">
+                                      <a href="{{baseUrl}}">See more information</a>
+                                    </p>
+
+                                    <p data-pm-slice="1 1 []">&nbsp;</p>
+
+                                    <p data-pm-slice="1 1 []">I would like to thank you in advance for your help with the evaluation of this manuscript,
+                                      since it would not be possible for us to run the journal without the help of our reviewers.</p>
+
+                                    <p>&nbsp;</p>
+
+                                    <p>I am looking forward to hearing from you.</p>
+
+                                    <p>&nbsp;</p>
+
+
+
+                                    <p>Best regards,
+                                      <br /> {{editorName}}
+                                    </p>
+
+                                    <div style="text-align: center;">&nbsp;</div>
+
+                                  </td>
+                                </tr>
+                              </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/resend-reviewer.txt b/packages/component-mail-service/src/templates/resend-reviewer.txt
new file mode 100644
index 000000000..b9d171afb
--- /dev/null
+++ b/packages/component-mail-service/src/templates/resend-reviewer.txt
@@ -0,0 +1,20 @@
+Dear {{reviewerName}},
+
+On {{expectedDate}}, I sent you a request to review the manuscript titled "{{title}}"
+by {{submittingAuthorName}}, submitted for possible publication in Hindawi.
+
+I would be grateful if you would agree to review it and let me know whether you feel
+it is suitable for publication. If you are unable to review this manuscript then
+please decline to review. More details are available by clicking the link.
+
+{{agreeUrl}} AGREE
+{{declineUrl}} DECLINE
+{{baseUrl}} See more information
+
+I would like to thank you in advance for your help with the evaluation of this manuscript,
+since it would not be possible for us to run the journal without the help of our reviewers.
+
+I am looking forward to hearing from you.
+
+Best regards,
+{{editorName}}
\ No newline at end of file
-- 
GitLab