Browse Source

Add E2E client tests for signup approval

Chocobozzz 1 year ago
parent
commit
5bdfa604f1

+ 1 - 0
client/.gitignore

@@ -11,5 +11,6 @@
 /src/locale/target/server_*.xml
 /e2e/local.log
 /e2e/browserstack.err
+/e2e/screenshots
 /src/standalone/player/build
 /src/standalone/player/dist

+ 24 - 3
client/e2e/src/po/admin-config.po.ts

@@ -1,4 +1,4 @@
-import { getCheckbox, go } from '../utils'
+import { browserSleep, getCheckbox, go, isCheckboxSelected } from '../utils'
 
 export class AdminConfigPage {
 
@@ -8,7 +8,6 @@ export class AdminConfigPage {
       'basic-configuration': 'APPEARANCE',
       'instance-information': 'INSTANCE'
     }
-
     await go('/admin/config/edit-custom#' + tab)
 
     await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed()
@@ -28,17 +27,39 @@ export class AdminConfigPage {
     return $('#instanceCustomHomepageContent').setValue(newValue)
   }
 
-  async toggleSignup () {
+  async toggleSignup (enabled: boolean) {
+    if (await isCheckboxSelected('signupEnabled') === enabled) return
+
     const checkbox = await getCheckbox('signupEnabled')
 
     await checkbox.waitForClickable()
     await checkbox.click()
   }
 
+  async toggleSignupApproval (required: boolean) {
+    if (await isCheckboxSelected('signupRequiresApproval') === required) return
+
+    const checkbox = await getCheckbox('signupRequiresApproval')
+
+    await checkbox.waitForClickable()
+    await checkbox.click()
+  }
+
+  async toggleSignupEmailVerification (required: boolean) {
+    if (await isCheckboxSelected('signupRequiresEmailVerification') === required) return
+
+    const checkbox = await getCheckbox('signupRequiresEmailVerification')
+
+    await checkbox.waitForClickable()
+    await checkbox.click()
+  }
+
   async save () {
     const button = $('input[type=submit]')
 
     await button.waitForClickable()
     await button.click()
+
+    await browserSleep(1000)
   }
 }

+ 35 - 0
client/e2e/src/po/admin-registration.po.ts

@@ -0,0 +1,35 @@
+import { browserSleep, findParentElement, go } from '../utils'
+
+export class AdminRegistrationPage {
+
+  async navigateToRegistratonsList () {
+    await go('/admin/moderation/registrations/list')
+
+    await $('my-registration-list').waitForDisplayed()
+  }
+
+  async accept (username: string, moderationResponse: string) {
+    const usernameEl = await $('*=' + username)
+    await usernameEl.waitForDisplayed()
+
+    const tr = await findParentElement(usernameEl, async el => await el.getTagName() === 'tr')
+
+    await tr.$('.action-cell .dropdown-root').click()
+
+    const accept = await $('span*=Accept this registration')
+    await accept.waitForClickable()
+    await accept.click()
+
+    const moderationResponseTextarea = await $('#moderationResponse')
+    await moderationResponseTextarea.waitForDisplayed()
+
+    await moderationResponseTextarea.setValue(moderationResponse)
+
+    const submitButton = $('.modal-footer input[type=submit]')
+    await submitButton.waitForClickable()
+    await submitButton.click()
+
+    await browserSleep(1000)
+  }
+
+}

+ 28 - 8
client/e2e/src/po/login.po.ts

@@ -6,7 +6,14 @@ export class LoginPage {
 
   }
 
-  async login (username: string, password: string, url = '/login') {
+  async login (options: {
+    username: string
+    password: string
+    displayName?: string
+    url?: string
+  }) {
+    const { username, password, url = '/login', displayName = username } = options
+
     await go(url)
 
     await browser.execute(`window.localStorage.setItem('no_account_setup_warning_modal', 'true')`)
@@ -27,27 +34,40 @@ export class LoginPage {
 
       await menuToggle.click()
 
-      await this.ensureIsLoggedInAs(username)
+      await this.ensureIsLoggedInAs(displayName)
 
       await menuToggle.click()
     } else {
-      await this.ensureIsLoggedInAs(username)
+      await this.ensureIsLoggedInAs(displayName)
     }
   }
 
+  async getLoginError (username: string, password: string) {
+    await go('/login')
+
+    await $('input#username').setValue(username)
+    await $('input#password').setValue(password)
+
+    await browser.pause(1000)
+
+    await $('form input[type=submit]').click()
+
+    return $('.alert-danger').getText()
+  }
+
   async loginAsRootUser () {
-    return this.login('root', 'test' + this.getSuffix())
+    return this.login({ username: 'root', password: 'test' + this.getSuffix() })
   }
 
   loginOnPeerTube2 () {
-    return this.login('e2e', process.env.PEERTUBE2_E2E_PASSWORD, 'https://peertube2.cpy.re/login')
+    return this.login({ username: 'e2e', password: process.env.PEERTUBE2_E2E_PASSWORD, url: 'https://peertube2.cpy.re/login' })
   }
 
   async logout () {
-    const loggedInMore = $('.logged-in-more')
+    const loggedInDropdown = $('.logged-in-more .logged-in-info')
 
-    await loggedInMore.waitForClickable()
-    await loggedInMore.click()
+    await loggedInDropdown.waitForClickable()
+    await loggedInDropdown.click()
 
     const logout = $('.dropdown-item*=Log out')
 

+ 24 - 27
client/e2e/src/po/signup.po.ts

@@ -27,42 +27,39 @@ export class SignupPage {
     return terms.click()
   }
 
+  async getEndMessage () {
+    const alert = $('.pt-alert-primary')
+    await alert.waitForDisplayed()
+
+    return alert.getText()
+  }
+
+  async fillRegistrationReason (reason: string) {
+    await $('#registrationReason').setValue(reason)
+  }
+
   async fillAccountStep (options: {
-    displayName: string
     username: string
-    email: string
-    password: string
+    password?: string
+    displayName?: string
+    email?: string
   }) {
-    if (options.displayName) {
-      await $('#displayName').setValue(options.displayName)
-    }
-
-    if (options.username) {
-      await $('#username').setValue(options.username)
-    }
+    await $('#displayName').setValue(options.displayName || `${options.username} display name`)
 
-    if (options.email) {
-      // Fix weird bug on firefox that "cannot scroll into view" when using just `setValue`
-      await $('#email').scrollIntoView(false)
-      await $('#email').waitForClickable()
-      await $('#email').setValue(options.email)
-    }
+    await $('#username').setValue(options.username)
+    await $('#password').setValue(options.password || 'password')
 
-    if (options.password) {
-      await $('#password').setValue(options.password)
-    }
+    // Fix weird bug on firefox that "cannot scroll into view" when using just `setValue`
+    await $('#email').scrollIntoView(false)
+    await $('#email').waitForClickable()
+    await $('#email').setValue(options.email || `${options.username}@example.com`)
   }
 
   async fillChannelStep (options: {
-    displayName: string
     name: string
+    displayName?: string
   }) {
-    if (options.displayName) {
-      await $('#displayName').setValue(options.displayName)
-    }
-
-    if (options.name) {
-      await $('#name').setValue(options.name)
-    }
+    await $('#displayName').setValue(options.displayName || `${options.name} channel display name`)
+    await $('#name').setValue(options.name)
   }
 }

+ 359 - 44
client/e2e/src/suites-local/signup.e2e-spec.ts

@@ -1,12 +1,89 @@
 import { AdminConfigPage } from '../po/admin-config.po'
+import { AdminRegistrationPage } from '../po/admin-registration.po'
 import { LoginPage } from '../po/login.po'
 import { SignupPage } from '../po/signup.po'
-import { isMobileDevice, waitServerUp } from '../utils'
+import { browserSleep, getVerificationLink, go, findEmailTo, isMobileDevice, MockSMTPServer, waitServerUp } from '../utils'
+
+function checkEndMessage (options: {
+  message: string
+  requiresEmailVerification: boolean
+  requiresApproval: boolean
+  afterEmailVerification: boolean
+}) {
+  const { message, requiresApproval, requiresEmailVerification, afterEmailVerification } = options
+
+  {
+    const created = 'account has been created'
+    const request = 'account request has been sent'
+
+    if (requiresApproval) {
+      expect(message).toContain(request)
+      expect(message).not.toContain(created)
+    } else {
+      expect(message).not.toContain(request)
+      expect(message).toContain(created)
+    }
+  }
+
+  {
+    const checkEmail = 'Check your emails'
+
+    if (requiresEmailVerification) {
+      expect(message).toContain(checkEmail)
+    } else {
+      expect(message).not.toContain(checkEmail)
+
+      const moderatorsApproval = 'moderator will check your registration request'
+      if (requiresApproval) {
+        expect(message).toContain(moderatorsApproval)
+      } else {
+        expect(message).not.toContain(moderatorsApproval)
+      }
+    }
+  }
+
+  {
+    const emailVerified = 'email has been verified'
+
+    if (afterEmailVerification) {
+      expect(message).toContain(emailVerified)
+    } else {
+      expect(message).not.toContain(emailVerified)
+    }
+  }
+}
 
 describe('Signup', () => {
   let loginPage: LoginPage
   let adminConfigPage: AdminConfigPage
   let signupPage: SignupPage
+  let adminRegistrationPage: AdminRegistrationPage
+
+  async function prepareSignup (options: {
+    enabled: boolean
+    requiresApproval?: boolean
+    requiresEmailVerification?: boolean
+  }) {
+    await loginPage.loginAsRootUser()
+
+    await adminConfigPage.navigateTo('basic-configuration')
+    await adminConfigPage.toggleSignup(options.enabled)
+
+    if (options.enabled) {
+      if (options.requiresApproval !== undefined) {
+        await adminConfigPage.toggleSignupApproval(options.requiresApproval)
+      }
+
+      if (options.requiresEmailVerification !== undefined) {
+        await adminConfigPage.toggleSignupEmailVerification(options.requiresEmailVerification)
+      }
+    }
+
+    await adminConfigPage.save()
+
+    await loginPage.logout()
+    await browser.refresh()
+  }
 
   before(async () => {
     await waitServerUp()
@@ -16,72 +93,310 @@ describe('Signup', () => {
     loginPage = new LoginPage(isMobileDevice())
     adminConfigPage = new AdminConfigPage()
     signupPage = new SignupPage()
+    adminRegistrationPage = new AdminRegistrationPage()
 
     await browser.maximizeWindow()
   })
 
-  it('Should disable signup', async () => {
-    await loginPage.loginAsRootUser()
+  describe('Signup disabled', function () {
+    it('Should disable signup', async () => {
+      await prepareSignup({ enabled: false })
 
-    await adminConfigPage.navigateTo('basic-configuration')
-    await adminConfigPage.toggleSignup()
+      await expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed()
+    })
+  })
 
-    await adminConfigPage.save()
+  describe('Email verification disabled', function () {
 
-    await loginPage.logout()
-    await browser.refresh()
+    describe('Direct registration', function () {
 
-    expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed()
-  })
+      it('Should enable signup without approval', async () => {
+        await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: false })
 
-  it('Should enable signup', async () => {
-    await loginPage.loginAsRootUser()
+        await signupPage.getRegisterMenuButton().waitForDisplayed()
+      })
 
-    await adminConfigPage.navigateTo('basic-configuration')
-    await adminConfigPage.toggleSignup()
+      it('Should go on signup page', async function () {
+        await signupPage.clickOnRegisterInMenu()
+      })
 
-    await adminConfigPage.save()
+      it('Should validate the first step (about page)', async function () {
+        await signupPage.validateStep()
+      })
 
-    await loginPage.logout()
-    await browser.refresh()
+      it('Should validate the second step (terms)', async function () {
+        await signupPage.checkTerms()
+        await signupPage.validateStep()
+      })
 
-    expect(signupPage.getRegisterMenuButton()).toBeDisplayed()
-  })
+      it('Should validate the third step (account)', async function () {
+        await signupPage.fillAccountStep({ username: 'user_1', displayName: 'user_1_dn' })
 
-  it('Should go on signup page', async function () {
-    await signupPage.clickOnRegisterInMenu()
-  })
+        await signupPage.validateStep()
+      })
 
-  it('Should validate the first step (about page)', async function () {
-    await signupPage.validateStep()
-  })
+      it('Should validate the third step (channel)', async function () {
+        await signupPage.fillChannelStep({ name: 'user_1_channel' })
 
-  it('Should validate the second step (terms)', async function () {
-    await signupPage.checkTerms()
-    await signupPage.validateStep()
-  })
+        await signupPage.validateStep()
+      })
+
+      it('Should be logged in', async function () {
+        await loginPage.ensureIsLoggedInAs('user_1_dn')
+      })
+
+      it('Should have a valid end message', async function () {
+        const message = await signupPage.getEndMessage()
+
+        checkEndMessage({
+          message,
+          requiresEmailVerification: false,
+          requiresApproval: false,
+          afterEmailVerification: false
+        })
 
-  it('Should validate the third step (account)', async function () {
-    await signupPage.fillAccountStep({
-      displayName: 'user 1',
-      username: 'user_1',
-      email: 'user_1@example.com',
-      password: 'my_super_password'
+        await browser.saveScreenshot('./screenshots/direct-without-email.png')
+
+        await loginPage.logout()
+      })
     })
 
-    await signupPage.validateStep()
+    describe('Registration with approval', function () {
+
+      it('Should enable signup with approval', async () => {
+        await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: false })
+
+        await signupPage.getRegisterMenuButton().waitForDisplayed()
+      })
+
+      it('Should go on signup page', async function () {
+        await signupPage.clickOnRegisterInMenu()
+      })
+
+      it('Should validate the first step (about page)', async function () {
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the second step (terms)', async function () {
+        await signupPage.checkTerms()
+        await signupPage.fillRegistrationReason('my super reason')
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the third step (account)', async function () {
+        await signupPage.fillAccountStep({ username: 'user_2', displayName: 'user_2 display name', password: 'password' })
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the third step (channel)', async function () {
+        await signupPage.fillChannelStep({ name: 'user_2_channel' })
+        await signupPage.validateStep()
+      })
+
+      it('Should have a valid end message', async function () {
+        const message = await signupPage.getEndMessage()
+
+        checkEndMessage({
+          message,
+          requiresEmailVerification: false,
+          requiresApproval: true,
+          afterEmailVerification: false
+        })
+
+        await browser.saveScreenshot('./screenshots/request-without-email.png')
+      })
+
+      it('Should display a message when trying to login with this account', async function () {
+        const error = await loginPage.getLoginError('user_2', 'password')
+
+        expect(error).toContain('awaiting approval')
+      })
+
+      it('Should accept the registration', async function () {
+        await loginPage.loginAsRootUser()
+
+        await adminRegistrationPage.navigateToRegistratonsList()
+        await adminRegistrationPage.accept('user_2', 'moderation response')
+
+        await loginPage.logout()
+      })
+
+      it('Should be able to login with this new account', async function () {
+        await loginPage.login({ username: 'user_2', password: 'password', displayName: 'user_2 display name' })
+
+        await loginPage.logout()
+      })
+    })
   })
 
-  it('Should validate the third step (channel)', async function () {
-    await signupPage.fillChannelStep({
-      displayName: 'user 1 channel',
-      name: 'user_1_channel'
+  describe('Email verification enabled', function () {
+    const emails: any[] = []
+    let emailPort: number
+
+    before(async () => {
+      // FIXME: typings are wrong, get returns a promise
+      emailPort = await browser.sharedStore.get('emailPort') as unknown as number
+
+      MockSMTPServer.Instance.collectEmails(emailPort, emails)
     })
 
-    await signupPage.validateStep()
-  })
+    describe('Direct registration', function () {
+
+      it('Should enable signup without approval', async () => {
+        await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: true })
+
+        await signupPage.getRegisterMenuButton().waitForDisplayed()
+      })
+
+      it('Should go on signup page', async function () {
+        await signupPage.clickOnRegisterInMenu()
+      })
+
+      it('Should validate the first step (about page)', async function () {
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the second step (terms)', async function () {
+        await signupPage.checkTerms()
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the third step (account)', async function () {
+        await signupPage.fillAccountStep({ username: 'user_3', displayName: 'user_3 display name', email: 'user_3@example.com' })
+
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the third step (channel)', async function () {
+        await signupPage.fillChannelStep({ name: 'user_3_channel' })
+
+        await signupPage.validateStep()
+      })
+
+      it('Should have a valid end message', async function () {
+        const message = await signupPage.getEndMessage()
+
+        checkEndMessage({
+          message,
+          requiresEmailVerification: true,
+          requiresApproval: false,
+          afterEmailVerification: false
+        })
+
+        await browser.saveScreenshot('./screenshots/direct-with-email.png')
+      })
+
+      it('Should validate the email', async function () {
+        let email: { text: string }
+
+        while (!(email = findEmailTo(emails, 'user_3@example.com'))) {
+          await browserSleep(100)
+        }
+
+        await go(getVerificationLink(email))
+
+        const message = await signupPage.getEndMessage()
+
+        checkEndMessage({
+          message,
+          requiresEmailVerification: false,
+          requiresApproval: false,
+          afterEmailVerification: true
+        })
 
-  it('Should be logged in', async function () {
-    await loginPage.ensureIsLoggedInAs('user 1')
+        await browser.saveScreenshot('./screenshots/direct-after-email.png')
+      })
+    })
+
+    describe('Registration with approval', function () {
+
+      it('Should enable signup without approval', async () => {
+        await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: true })
+
+        await signupPage.getRegisterMenuButton().waitForDisplayed()
+      })
+
+      it('Should go on signup page', async function () {
+        await signupPage.clickOnRegisterInMenu()
+      })
+
+      it('Should validate the first step (about page)', async function () {
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the second step (terms)', async function () {
+        await signupPage.checkTerms()
+        await signupPage.fillRegistrationReason('my super reason 2')
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the third step (account)', async function () {
+        await signupPage.fillAccountStep({
+          username: 'user_4',
+          displayName: 'user_4 display name',
+          email: 'user_4@example.com',
+          password: 'password'
+        })
+        await signupPage.validateStep()
+      })
+
+      it('Should validate the third step (channel)', async function () {
+        await signupPage.fillChannelStep({ name: 'user_4_channel' })
+        await signupPage.validateStep()
+      })
+
+      it('Should have a valid end message', async function () {
+        const message = await signupPage.getEndMessage()
+
+        checkEndMessage({
+          message,
+          requiresEmailVerification: true,
+          requiresApproval: true,
+          afterEmailVerification: false
+        })
+
+        await browser.saveScreenshot('./screenshots/request-with-email.png')
+      })
+
+      it('Should display a message when trying to login with this account', async function () {
+        const error = await loginPage.getLoginError('user_4', 'password')
+
+        expect(error).toContain('awaiting approval')
+      })
+
+      it('Should accept the registration', async function () {
+        await loginPage.loginAsRootUser()
+
+        await adminRegistrationPage.navigateToRegistratonsList()
+        await adminRegistrationPage.accept('user_4', 'moderation response 2')
+
+        await loginPage.logout()
+      })
+
+      it('Should validate the email', async function () {
+        let email: { text: string }
+
+        while (!(email = findEmailTo(emails, 'user_4@example.com'))) {
+          await browserSleep(100)
+        }
+
+        await go(getVerificationLink(email))
+
+        const message = await signupPage.getEndMessage()
+
+        checkEndMessage({
+          message,
+          requiresEmailVerification: false,
+          requiresApproval: true,
+          afterEmailVerification: true
+        })
+
+        await browser.saveScreenshot('./screenshots/request-after-email.png')
+      })
+    })
+
+    before(() => {
+      MockSMTPServer.Instance.kill()
+    })
   })
 })

+ 16 - 1
client/e2e/src/utils/elements.ts

@@ -5,6 +5,10 @@ async function getCheckbox (name: string) {
   return input.parentElement()
 }
 
+function isCheckboxSelected (name: string) {
+  return $(`input[id=${name}]`).isSelected()
+}
+
 async function selectCustomSelect (id: string, valueLabel: string) {
   const wrapper = $(`[formcontrolname=${id}] .ng-arrow-wrapper`)
 
@@ -22,7 +26,18 @@ async function selectCustomSelect (id: string, valueLabel: string) {
   return option.click()
 }
 
+async function findParentElement (
+  el: WebdriverIO.Element,
+  finder: (el: WebdriverIO.Element) => Promise<boolean>
+) {
+  if (await finder(el) === true) return el
+
+  return findParentElement(await el.parentElement(), finder)
+}
+
 export {
   getCheckbox,
-  selectCustomSelect
+  isCheckboxSelected,
+  selectCustomSelect,
+  findParentElement
 }

+ 31 - 0
client/e2e/src/utils/email.ts

@@ -0,0 +1,31 @@
+function getVerificationLink (email: { text: string }) {
+  const { text } = email
+
+  const regexp = /\[(?<link>http:\/\/[^\]]+)\]/g
+  const matched = text.matchAll(regexp)
+
+  if (!matched) throw new Error('Could not find verification link in email')
+
+  for (const match of matched) {
+    const link = match.groups.link
+
+    if (link.includes('/verify-account/')) return link
+  }
+
+  throw new Error('Could not find /verify-account/ link')
+}
+
+function findEmailTo (emails: { text: string, to: { address: string }[] }[], to: string) {
+  for (const email of emails) {
+    for (const { address } of email.to) {
+      if (address === to) return email
+    }
+  }
+
+  return undefined
+}
+
+export {
+  getVerificationLink,
+  findEmailTo
+}

+ 19 - 5
client/e2e/src/utils/hooks.ts

@@ -1,10 +1,13 @@
 import { ChildProcessWithoutNullStreams } from 'child_process'
 import { basename } from 'path'
 import { runCommand, runServer } from './server'
+import { setValue } from '@wdio/shared-store-service'
 
-let appInstance: string
+let appInstance: number
 let app: ChildProcessWithoutNullStreams
 
+let emailPort: number
+
 async function beforeLocalSuite (suite: any) {
   const config = buildConfig(suite.file)
 
@@ -17,13 +20,20 @@ function afterLocalSuite () {
   app = undefined
 }
 
-function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) {
-  appInstance = capabilities['browserName'] === 'chrome' ? '1' : '2'
+async function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) {
+  appInstance = capabilities['browserName'] === 'chrome'
+    ? 1
+    : 2
+
+  emailPort = 1025 + appInstance
+
   config.baseUrl = 'http://localhost:900' + appInstance
+
+  await setValue('emailPort', emailPort)
 }
 
 async function onBrowserStackPrepare () {
-  const appInstance = '1'
+  const appInstance = 1
 
   await runCommand('npm run clean:server:test -- ' + appInstance)
   app = runServer(appInstance)
@@ -71,7 +81,11 @@ function buildConfig (suiteFile: string = undefined) {
   if (filename === 'signup.e2e-spec.ts') {
     return {
       signup: {
-        enabled: true
+        limit: -1
+      },
+      smtp: {
+        hostname: '127.0.0.1',
+        port: emailPort
       }
     }
   }

+ 2 - 0
client/e2e/src/utils/index.ts

@@ -1,5 +1,7 @@
 export * from './common'
 export * from './elements'
+export * from './email'
 export * from './hooks'
+export * from './mock-smtp'
 export * from './server'
 export * from './urls'

+ 58 - 0
client/e2e/src/utils/mock-smtp.ts

@@ -0,0 +1,58 @@
+import { ChildProcess } from 'child_process'
+import MailDev from '@peertube/maildev'
+
+class MockSMTPServer {
+
+  private static instance: MockSMTPServer
+  private started = false
+  private emailChildProcess: ChildProcess
+  private emails: object[]
+
+  collectEmails (port: number, emailsCollection: object[]) {
+    return new Promise<number>((res, rej) => {
+      this.emails = emailsCollection
+
+      if (this.started) {
+        return res(undefined)
+      }
+
+      const maildev = new MailDev({
+        ip: '127.0.0.1',
+        smtp: port,
+        disableWeb: true,
+        silent: true
+      })
+
+      maildev.on('new', email => {
+        this.emails.push(email)
+      })
+
+      maildev.listen(err => {
+        if (err) return rej(err)
+
+        this.started = true
+
+        return res(port)
+      })
+    })
+  }
+
+  kill () {
+    if (!this.emailChildProcess) return
+
+    process.kill(this.emailChildProcess.pid)
+
+    this.emailChildProcess = null
+    MockSMTPServer.instance = null
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  MockSMTPServer
+}

+ 2 - 2
client/e2e/src/utils/server.ts

@@ -1,10 +1,10 @@
 import { exec, spawn } from 'child_process'
 import { join, resolve } from 'path'
 
-function runServer (appInstance: string, config: any = {}) {
+function runServer (appInstance: number, config: any = {}) {
   const env = Object.create(process.env)
   env['NODE_ENV'] = 'test'
-  env['NODE_APP_INSTANCE'] = appInstance
+  env['NODE_APP_INSTANCE'] = appInstance + ''
 
   env['NODE_CONFIG'] = JSON.stringify({
     rates_limit: {

+ 1 - 1
client/e2e/wdio.local-test.conf.ts

@@ -37,7 +37,7 @@ module.exports = {
       // }
     ],
 
-    services: [ 'chromedriver', 'geckodriver' ],
+    services: [ 'chromedriver', 'geckodriver', 'shared-store' ],
 
     beforeSession: beforeLocalSession,
     beforeSuite: beforeLocalSuite,

+ 1 - 1
client/e2e/wdio.local.conf.ts

@@ -33,7 +33,7 @@ module.exports = {
       }
     ],
 
-    services: [ 'chromedriver', 'geckodriver' ],
+    services: [ 'chromedriver', 'geckodriver', 'shared-store' ],
 
     beforeSession: beforeLocalSession,
     beforeSuite: beforeLocalSuite,

+ 2 - 0
client/package.json

@@ -52,6 +52,7 @@
     "@ngx-loading-bar/core": "^6.0.0",
     "@ngx-loading-bar/http-client": "^6.0.0",
     "@ngx-loading-bar/router": "^6.0.0",
+    "@peertube/maildev": "^1.2.0",
     "@peertube/p2p-media-loader-core": "^1.0.14",
     "@peertube/p2p-media-loader-hlsjs": "^1.0.14",
     "@peertube/videojs-contextmenu": "^5.5.0",
@@ -75,6 +76,7 @@
     "@wdio/cli": "^7.25.2",
     "@wdio/local-runner": "^7.25.2",
     "@wdio/mocha-framework": "^7.25.2",
+    "@wdio/shared-store-service": "^7.25.2",
     "@wdio/spec-reporter": "^7.25.1",
     "angular2-hotkeys": "^13.1.0",
     "angularx-qrcode": "14.0.0",

+ 339 - 106
client/yarn.lock

@@ -302,6 +302,11 @@
   dependencies:
     tslib "^2.3.0"
 
+"@arr/every@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@arr/every/-/every-1.0.1.tgz#22fe1f8e6355beca6c7c7bde965eb15cf994387b"
+  integrity sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==
+
 "@assemblyscript/loader@^0.10.1":
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06"
@@ -1789,6 +1794,18 @@
     read-package-json-fast "^2.0.3"
     which "^2.0.2"
 
+"@peertube/maildev@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@peertube/maildev/-/maildev-1.2.0.tgz#f25ee9fa6a45c0a6bc99c5392f63139eaa8eb088"
+  integrity sha512-VGog0A2gk0P8UnP0ZjCoYQumELiqqQY5i+gt18avTC7NJNJLUxMRMI045NAVSDFVbqt2EJJPsbZf3LFjUWRtmw==
+  dependencies:
+    async "^3.1.0"
+    commander "^8.3.0"
+    mailparser-mit "^1.0.0"
+    rimraf "^3.0.2"
+    smtp-server "^3.9.0"
+    wildstring "1.0.9"
+
 "@peertube/p2p-media-loader-core@^1.0.14":
   version "1.0.14"
   resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz#b4442dd343d6b30a51502e1240275eb98ef2c788"
@@ -1830,6 +1847,16 @@
     tokenizr "^1.6.4"
     xmldom "^0.6.0"
 
+"@polka/parse@^1.0.0-next.0":
+  version "1.0.0-next.0"
+  resolved "https://registry.yarnpkg.com/@polka/parse/-/parse-1.0.0-next.0.tgz#3551d792acdf4ad0b053072e57498cbe32e45a94"
+  integrity sha512-zcPNrc3PNrRLSCQ7ca8XR7h18VxdPIXhn+yvrYMdUFCHM7mhXGSPw5xBdbcf/dQ1cI4uE8pDfmm5uU+HX+WfFg==
+
+"@polka/url@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31"
+  integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==
+
 "@polka/url@^1.0.0-next.20":
   version "1.0.0-next.21"
   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
@@ -2021,6 +2048,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/gitconfiglocal@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@types/gitconfiglocal/-/gitconfiglocal-2.0.1.tgz#c134f9fb03d71917afa35c14f3b82085520509a6"
+  integrity sha512-AYC38la5dRwIfbrZhPNIvlGHlIbH+kdl2j8A37twoCQyhKPPoRPfVmoBZKajpLIfV7SMboU6MZ6w/RmZLH68IQ==
+
 "@types/html-minifier-terser@^6.0.0":
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
@@ -2127,9 +2159,9 @@
     "@types/lodash" "*"
 
 "@types/lodash@*":
-  version "4.14.189"
-  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2"
-  integrity sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==
+  version "4.14.191"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
+  integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
 
 "@types/magnet-uri@*":
   version "5.1.3"
@@ -2162,9 +2194,9 @@
   integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
 
 "@types/mocha@^10.0.0":
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.0.tgz#3d9018c575f0e3f7386c1de80ee66cc21fbb7a52"
-  integrity sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b"
+  integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==
 
 "@types/mousetrap@^1.6.9":
   version "1.6.11"
@@ -2177,9 +2209,9 @@
   integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
 
 "@types/node@*", "@types/node@^18.0.0":
-  version "18.11.9"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
-  integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
+  version "18.11.18"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
+  integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==
 
 "@types/node@^17.0.42":
   version "17.0.45"
@@ -2380,9 +2412,9 @@
   integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
 
 "@types/yargs@^17.0.8":
-  version "17.0.13"
-  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76"
-  integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==
+  version "17.0.19"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae"
+  integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ==
   dependencies:
     "@types/yargs-parser" "*"
 
@@ -2509,22 +2541,26 @@
     is-function "^1.0.1"
 
 "@wdio/browserstack-service@^7.25.2":
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-7.26.0.tgz#d303c5998e565734bd7f5c23fc9b291a588b7c21"
-  integrity sha512-hRKmg4u/DRNZm1EJGaYESAH6GsCPCtBm15fP9ngm/HFUG084thFfrD8Tt09hO+KSNoK4tXl4k1ZHZ4akrOq9KA==
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-7.29.1.tgz#46282aa07b7c11a51ebac0bff1f12f1badd6e264"
+  integrity sha512-1+MoqlIXIjbh1oEOZcvtemij+Yz/CB6orZjeT3WCoA9oY8Ul8EeIHhfF7GxmE6u0OVofjmC+wfO5NlHYCKgL1w==
   dependencies:
-    "@types/node" "^18.0.0"
+    "@types/gitconfiglocal" "^2.0.1"
     "@wdio/logger" "7.26.0"
+    "@wdio/reporter" "7.25.4"
     "@wdio/types" "7.26.0"
     browserstack-local "^1.4.5"
     form-data "^4.0.0"
+    git-repo-info "^2.1.1"
+    gitconfiglocal "^2.1.0"
     got "^11.0.2"
-    webdriverio "7.26.0"
+    uuid "^8.3.2"
+    webdriverio "7.29.1"
 
 "@wdio/cli@^7.25.2":
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.26.0.tgz#20c690a5ede4a35cb2f84da9041c250a6013bc54"
-  integrity sha512-xG+ZIzPqzz/Tvhfrogd8oNvTXzzdE+cbkmTHjMGo1hnmnoAQPeAEcV/QqaX5CHFE9DjaguEeadqjcZikB5U2GQ==
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.29.1.tgz#1b47f5a45f21754d42be814dbae94ff723a6a1a2"
+  integrity sha512-dldHNYlnuFUG10TlENbeL41tujqgYD7S/9nzV1J/szBryCO6AIVz/QWn/AUv3zrsO2sn8TNF8BMEXRvLgCxyeg==
   dependencies:
     "@types/ejs" "^3.0.5"
     "@types/fs-extra" "^9.0.4"
@@ -2536,7 +2572,7 @@
     "@types/recursive-readdir" "^2.2.0"
     "@wdio/config" "7.26.0"
     "@wdio/logger" "7.26.0"
-    "@wdio/protocols" "7.22.0"
+    "@wdio/protocols" "7.27.0"
     "@wdio/types" "7.26.0"
     "@wdio/utils" "7.26.0"
     async-exit-hook "^2.0.1"
@@ -2551,7 +2587,7 @@
     lodash.union "^4.6.0"
     mkdirp "^1.0.4"
     recursive-readdir "^2.2.2"
-    webdriverio "7.26.0"
+    webdriverio "7.29.1"
     yargs "^17.0.0"
     yarn-install "^1.0.0"
 
@@ -2567,14 +2603,14 @@
     glob "^8.0.3"
 
 "@wdio/local-runner@^7.25.2":
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.26.0.tgz#a056c6e9d73c7f48e54fe3f07ce573a90dae26ab"
-  integrity sha512-GdCP7Y8s8qvoctC0WaSGBSmTSbVw74WEJm6Y3n3DpoCI8ABFNkQlhFlqJH+taQDs3sRVEM65bHGcU4C4FOVWXQ==
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.29.1.tgz#f93a2953847b4271b59ba1b9635920e8046f0e55"
+  integrity sha512-4w9Dsp9/4+MEU8yG7M8ynsCqpSP6UbKqZ2M/gWpvkvy57rb3eS9evFdIFfRzuQmbsztG9qeAlGILwlZ4/oaopg==
   dependencies:
     "@types/stream-buffers" "^3.0.3"
     "@wdio/logger" "7.26.0"
     "@wdio/repl" "7.26.0"
-    "@wdio/runner" "7.26.0"
+    "@wdio/runner" "7.29.1"
     "@wdio/types" "7.26.0"
     async-exit-hook "^2.0.1"
     split2 "^4.0.0"
@@ -2602,10 +2638,10 @@
     expect-webdriverio "^3.0.0"
     mocha "^10.0.0"
 
-"@wdio/protocols@7.22.0":
-  version "7.22.0"
-  resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.22.0.tgz#d89faef687cb08981d734bbc5e5dffc6fb5a064c"
-  integrity sha512-8EXRR+Ymdwousm/VGtW3H1hwxZ/1g1H99A1lF0U4GuJ5cFWHCd0IVE5H31Z52i8ZruouW8jueMkGZPSo2IIUSQ==
+"@wdio/protocols@7.27.0":
+  version "7.27.0"
+  resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.27.0.tgz#8e2663ec877dce7a5f76b021209c18dd0132e853"
+  integrity sha512-hT/U22R5i3HhwPjkaKAG0yd59eaOaZB0eibRj2+esCImkb5Y6rg8FirrlYRxIGFVBl0+xZV0jKHzR5+o097nvg==
 
 "@wdio/repl@7.26.0":
   version "7.26.0"
@@ -2614,10 +2650,26 @@
   dependencies:
     "@wdio/utils" "7.26.0"
 
-"@wdio/reporter@7.26.0":
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.26.0.tgz#26c0e7114a4c1e7b29a79e4d178e5312e04d7934"
-  integrity sha512-kEb7i1A4V4E1wJgdyvLsDbap4cEp1fPZslErGtbAbK+9HI8Lt/SlTZCiOpZbvhgzvawEqOV6UqxZT1RsL8wZWw==
+"@wdio/reporter@7.25.4":
+  version "7.25.4"
+  resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.25.4.tgz#b6a69652dd0c4ec131255000af128eac403a18b9"
+  integrity sha512-M37qzEmF5qNffyZmRQGjDlrXqWW21EFvgW8wsv1b/NtfpZc0c0MoRpeh6BnvX1KcE4nCXfjXgSJPOqV4ZCzUEQ==
+  dependencies:
+    "@types/diff" "^5.0.0"
+    "@types/node" "^18.0.0"
+    "@types/object-inspect" "^1.8.0"
+    "@types/supports-color" "^8.1.0"
+    "@types/tmp" "^0.2.0"
+    "@wdio/types" "7.25.4"
+    diff "^5.0.0"
+    fs-extra "^10.0.0"
+    object-inspect "^1.10.3"
+    supports-color "8.1.1"
+
+"@wdio/reporter@7.29.1":
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.29.1.tgz#7fc2e3b7aa3843172dcd97221c44257384cbbd27"
+  integrity sha512-mpusCpbw7RxnJSDu9qa1qv5IfEMCh7377y1Typ4J2TlMy+78CQzGZ8coEXjBxLcqijTUwcyyoLNI5yRSvbDExw==
   dependencies:
     "@types/diff" "^5.0.0"
     "@types/node" "^18.0.0"
@@ -2630,10 +2682,10 @@
     object-inspect "^1.10.3"
     supports-color "8.1.1"
 
-"@wdio/runner@7.26.0":
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.26.0.tgz#c0b2848dc885b655e8690d3e0381dfb0ad221af5"
-  integrity sha512-DhQiOs10oPeLlv7/R+997arPg5OY7iEgespGkn6r+kdx2o+awxa6PFegQrjJmRKUmNv3TTuKXHouP34TbR/8sw==
+"@wdio/runner@7.29.1":
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.29.1.tgz#9fd2fa6dd28b8b130a10d23452eb155e1e887576"
+  integrity sha512-lJEk/HJ5IiuvAJws8zTx9XL5LJuoexvjWIZmOmFJ6Gv8qRpUx6b0n+JM7vhhbTeIqs4QLXOwTQUHlDDRldQlzQ==
   dependencies:
     "@wdio/config" "7.26.0"
     "@wdio/logger" "7.26.0"
@@ -2641,21 +2693,41 @@
     "@wdio/utils" "7.26.0"
     deepmerge "^4.0.0"
     gaze "^1.1.2"
-    webdriver "7.26.0"
-    webdriverio "7.26.0"
+    webdriver "7.27.0"
+    webdriverio "7.29.1"
+
+"@wdio/shared-store-service@^7.25.2":
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/shared-store-service/-/shared-store-service-7.29.1.tgz#c43a3dbc7d47c8334970bc173e963688977e8a79"
+  integrity sha512-13VOxyz956DSs2wloQ8gtyEx42zjAuOg+N8/4tGk1p2igPzHB2qUiY/P0yi6zamxYGb6PKLIumIeUjitWHtyWA==
+  dependencies:
+    "@polka/parse" "^1.0.0-next.0"
+    "@wdio/logger" "7.26.0"
+    "@wdio/types" "7.26.0"
+    got "^11.0.2"
+    polka "^0.5.2"
+    webdriverio "7.29.1"
 
 "@wdio/spec-reporter@^7.25.1":
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.26.0.tgz#13eaa5a0fd089684d4c1bcd8ac11dc8646afb5b7"
-  integrity sha512-oisyVWn+MRoq0We0qORoDHNk+iKr7CFG4+IE5GCRecR8cgP7dUjVXZcEbn6blgRpry4jOxsAl24frfaPDOsZVA==
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.29.1.tgz#08e13c02ea0876672226d5a2c326dda7e1a66c8e"
+  integrity sha512-bwSGM72QrDedqacY7Wq9Gn86VgRwIGPYzZtcaD7aDnvppCuV8Z/31Wpdfen+CzUk2+whXjXKe66ohPyl9TG5+w==
   dependencies:
     "@types/easy-table" "^1.2.0"
-    "@wdio/reporter" "7.26.0"
+    "@wdio/reporter" "7.29.1"
     "@wdio/types" "7.26.0"
     chalk "^4.0.0"
     easy-table "^1.1.1"
     pretty-ms "^7.0.0"
 
+"@wdio/types@7.25.4":
+  version "7.25.4"
+  resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.25.4.tgz#6f8f028e3108dc880de5068264695f1572e65352"
+  integrity sha512-muvNmq48QZCvocctnbe0URq2FjJjUPIG4iLoeMmyF0AQgdbjaUkMkw3BHYNHVTbSOU9WMsr2z8alhj/I2H6NRQ==
+  dependencies:
+    "@types/node" "^18.0.0"
+    got "^11.8.1"
+
 "@wdio/types@7.26.0":
   version "7.26.0"
   resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.26.0.tgz#70bc879c5dbe316a0eebbac4a46f0f66430b1d84"
@@ -2882,6 +2954,11 @@ addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.4:
   resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88"
   integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg==
 
+addressparser@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
+  integrity sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==
+
 adjust-sourcemap-loader@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99"
@@ -3057,9 +3134,9 @@ ansi-styles@^5.0.0:
   integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
 
 anymatch@~3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
-  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
   dependencies:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
@@ -3188,7 +3265,7 @@ async-exit-hook@^2.0.1:
   resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3"
   integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==
 
-async@^3.2.3:
+async@^3.1.0, async@^3.2.3:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
   integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
@@ -3327,6 +3404,11 @@ balanced-match@^2.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
   integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
 
+base32.js@0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202"
+  integrity sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==
+
 base64-js@^1.2.0, base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -3933,9 +4015,9 @@ chunk-store-stream@^4.3.0:
     readable-stream "^3.6.0"
 
 ci-info@^3.2.0:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf"
-  integrity sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f"
+  integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==
 
 clean-css@5.2.0:
   version "5.2.0"
@@ -4504,16 +4586,18 @@ decompress-response@^6.0.0:
     mimic-response "^3.1.0"
 
 deep-equal@^2.0.5:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.1.0.tgz#5ba60402cf44ab92c2c07f3f3312c3d857a0e1dd"
-  integrity sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6"
+  integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==
   dependencies:
     call-bind "^1.0.2"
     es-get-iterator "^1.1.2"
     get-intrinsic "^1.1.3"
     is-arguments "^1.1.1"
+    is-array-buffer "^3.0.1"
     is-date-object "^1.0.5"
     is-regex "^1.1.4"
+    is-shared-array-buffer "^1.0.2"
     isarray "^2.0.5"
     object-is "^1.1.5"
     object-keys "^1.1.1"
@@ -4522,7 +4606,7 @@ deep-equal@^2.0.5:
     side-channel "^1.0.4"
     which-boxed-primitive "^1.0.2"
     which-collection "^1.0.1"
-    which-typed-array "^1.1.8"
+    which-typed-array "^1.1.9"
 
 deep-is@^0.1.3:
   version "0.1.4"
@@ -4606,21 +4690,21 @@ devtools-protocol@0.0.981744:
   resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf"
   integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==
 
-devtools-protocol@^0.0.1069585:
-  version "0.0.1069585"
-  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1069585.tgz#c9a9f330462aabf054d581f254b13774297b84f2"
-  integrity sha512-sHmkZB6immWQWU4Wx3ogXwxjQUvQc92MmUDL52+q1z2hQmvpOcvDmbsjwX7QZOPTA32dMV7fgT6zUytcpPzy4A==
+devtools-protocol@^0.0.1085790:
+  version "0.0.1085790"
+  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1085790.tgz#315e4700eb960cf111cc908b9be2caca2257cb13"
+  integrity sha512-f5kfwdOTxPqX5v8ZfAAl9xBgoEVazBYtIONDWIRqYbb7yjOIcnk6vpzCgBCQvav5AuBRLzyUGG0V74OAx93LoA==
 
-devtools@7.26.0:
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.26.0.tgz#3d568aea2238d190ad0cd71c00483c07c707124a"
-  integrity sha512-+8HNbNpzgo4Sn+WcrvXuwsHW9XPJfLo4bs9lgs6DPJHIIDXYJXQGsd7940wMX0Rp0D2vHXA4ibK0oTI5rogM3Q==
+devtools@7.28.1:
+  version "7.28.1"
+  resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.28.1.tgz#9699e0ca41c9a3adfa351d8afac2928f8e1d381c"
+  integrity sha512-sDoszzrXDMLiBQqsg9A5gDqDBwhH4sjYzJIW15lQinB8qgNs0y4o1zdfNlqiKs4HstCA2uFixQeibbDCyMa7hQ==
   dependencies:
     "@types/node" "^18.0.0"
     "@types/ua-parser-js" "^0.7.33"
     "@wdio/config" "7.26.0"
     "@wdio/logger" "7.26.0"
-    "@wdio/protocols" "7.22.0"
+    "@wdio/protocols" "7.27.0"
     "@wdio/types" "7.26.0"
     "@wdio/utils" "7.26.0"
     chrome-launcher "^0.15.0"
@@ -4923,18 +5007,19 @@ es-abstract@^1.19.0, es-abstract@^1.20.4:
     unbox-primitive "^1.0.2"
 
 es-get-iterator@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
-  integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
+  integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==
   dependencies:
     call-bind "^1.0.2"
-    get-intrinsic "^1.1.0"
-    has-symbols "^1.0.1"
-    is-arguments "^1.1.0"
+    get-intrinsic "^1.1.3"
+    has-symbols "^1.0.3"
+    is-arguments "^1.1.1"
     is-map "^2.0.2"
     is-set "^2.0.2"
-    is-string "^1.0.5"
+    is-string "^1.0.7"
     isarray "^2.0.5"
+    stop-iteration-iterator "^1.0.0"
 
 es-module-lexer@^0.9.0:
   version "0.9.3"
@@ -5104,7 +5189,7 @@ escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
@@ -5404,7 +5489,7 @@ express@^4.17.3:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
-extend@~3.0.2:
+extend@~3.0.0, extend@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
   integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -5856,6 +5941,18 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
+git-repo-info@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.1.tgz#220ffed8cbae74ef8a80e3052f2ccb5179aed058"
+  integrity sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==
+
+gitconfiglocal@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz#07c28685c55cc5338b27b5acbcfe34aeb92e43d1"
+  integrity sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==
+  dependencies:
+    ini "^1.3.2"
+
 glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -5887,7 +5984,7 @@ glob@7.2.0:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@8.0.3, glob@^8.0.1, glob@^8.0.3:
+glob@8.0.3, glob@^8.0.1:
   version "8.0.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e"
   integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==
@@ -5910,6 +6007,17 @@ glob@^7.0.5, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@^8.0.3:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
+  integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^5.0.1"
+    once "^1.3.0"
+
 glob@~7.1.1:
   version "7.1.7"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@@ -6002,7 +6110,7 @@ gopd@^1.0.1:
   dependencies:
     get-intrinsic "^1.1.3"
 
-got@11.8.5, got@^11.0.2, got@^11.8.1:
+got@11.8.5:
   version "11.8.5"
   resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046"
   integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==
@@ -6019,6 +6127,23 @@ got@11.8.5, got@^11.0.2, got@^11.8.1:
     p-cancelable "^2.0.0"
     responselike "^2.0.0"
 
+got@^11.0.2, got@^11.8.1:
+  version "11.8.6"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
+  integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==
+  dependencies:
+    "@sindresorhus/is" "^4.0.0"
+    "@szmarczak/http-timer" "^4.0.5"
+    "@types/cacheable-request" "^6.0.1"
+    "@types/responselike" "^1.0.0"
+    cacheable-lookup "^5.0.3"
+    cacheable-request "^7.0.2"
+    decompress-response "^6.0.0"
+    http2-wrapper "^1.0.0-beta.5.2"
+    lowercase-keys "^2.0.0"
+    p-cancelable "^2.0.0"
+    responselike "^2.0.0"
+
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
   version "4.2.10"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
@@ -6093,7 +6218,7 @@ has-property-descriptors@^1.0.0:
   dependencies:
     get-intrinsic "^1.1.1"
 
-has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3:
+has-symbols@^1.0.2, has-symbols@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
   integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
@@ -6347,7 +6472,7 @@ humanize-ms@^1.2.1:
   dependencies:
     ms "^2.0.0"
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24:
+iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -6469,7 +6594,7 @@ ini@3.0.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.0.tgz#2f6de95006923aa75feed8894f5686165adc08f1"
   integrity sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw==
 
-ini@^1.3.5:
+ini@^1.3.2, ini@^1.3.5:
   version "1.3.8"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
   integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
@@ -6504,6 +6629,15 @@ internal-slot@^1.0.3:
     has "^1.0.3"
     side-channel "^1.0.4"
 
+internal-slot@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3"
+  integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==
+  dependencies:
+    get-intrinsic "^1.1.3"
+    has "^1.0.3"
+    side-channel "^1.0.4"
+
 interpret@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
@@ -6556,7 +6690,12 @@ ipaddr.js@1.9.1:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
   integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
 
-is-arguments@^1.1.0, is-arguments@^1.1.1:
+ipv6-normalize@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8"
+  integrity sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA==
+
+is-arguments@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
   integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
@@ -6564,6 +6703,15 @@ is-arguments@^1.1.0, is-arguments@^1.1.1:
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
+is-array-buffer@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a"
+  integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.3"
+    is-typed-array "^1.1.10"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -7508,6 +7656,16 @@ magnet-uri@^6.2.0:
     bep53-range "^1.1.0"
     thirty-two "^1.0.2"
 
+mailparser-mit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4"
+  integrity sha512-sckRITNb3VCT1sQ275g47MAN786pQ5lU20bLY5f794dF/ARGzuVATQ64gO13FOw8jayjFT10e5ttsripKGGXcw==
+  dependencies:
+    addressparser "^1.0.1"
+    iconv-lite "~0.4.24"
+    mime "^1.6.0"
+    uue "^3.1.0"
+
 make-dir@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -7576,6 +7734,13 @@ marky@^1.2.2:
   resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0"
   integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==
 
+matchit@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/matchit/-/matchit-1.1.0.tgz#c4ccf17d9c824cc1301edbcffde9b75a61d10a7c"
+  integrity sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==
+  dependencies:
+    "@arr/every" "^1.0.0"
+
 mathml-tag-names@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
@@ -7679,7 +7844,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17,
   dependencies:
     mime-db "1.52.0"
 
-mime@1.6.0, mime@^1.4.1:
+mime@1.6.0, mime@^1.4.1, mime@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@@ -7740,7 +7905,7 @@ minimatch@5.0.1:
   dependencies:
     brace-expansion "^2.0.1"
 
-minimatch@5.1.0, minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0:
+minimatch@5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7"
   integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
@@ -7754,6 +7919,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
   dependencies:
     brace-expansion "^1.1.7"
 
+minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
+  integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimatch@~3.0.2:
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
@@ -7848,9 +8020,9 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
 mocha@^10.0.0:
-  version "10.1.0"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a"
-  integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8"
+  integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==
   dependencies:
     ansi-colors "4.1.1"
     browser-stdout "1.3.1"
@@ -8080,6 +8252,11 @@ node-releases@^2.0.6:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
   integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
 
+nodemailer@6.7.3:
+  version "6.7.3"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018"
+  integrity sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g==
+
 nopt@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d"
@@ -8267,7 +8444,12 @@ oauth-sign@~0.9.0:
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
   integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
 
-object-inspect@^1.10.3, object-inspect@^1.12.2, object-inspect@^1.9.0:
+object-inspect@^1.10.3, object-inspect@^1.9.0:
+  version "1.12.3"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
+  integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+
+object-inspect@^1.12.2:
   version "1.12.2"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
   integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
@@ -8775,6 +8957,14 @@ pngjs@^5.0.0:
   resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
   integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
 
+polka@^0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/polka/-/polka-0.5.2.tgz#588bee0c5806dbc6c64958de3a1251860e9f2e26"
+  integrity sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==
+  dependencies:
+    "@polka/url" "^0.5.0"
+    trouter "^2.0.1"
+
 postcss-attribute-case-insensitive@^5.0.2:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741"
@@ -9285,9 +9475,9 @@ qs@~6.5.2:
   integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
 
 query-selector-shadow-dom@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.0.tgz#8fa7459a4620f094457640e74e953a9dbe61a38e"
-  integrity sha512-bK0/0cCI+R8ZmOF1QjT7HupDUYCxbf/9TJgAmSXQxZpftXmTAeil9DRoCnTDkWbvOyZzhcMBwKpptWcdkGFIMg==
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349"
+  integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==
 
 querystring@0.2.0:
   version "0.2.0"
@@ -9736,9 +9926,9 @@ responselike@^2.0.0:
     lowercase-keys "^2.0.0"
 
 resq@^1.9.1:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/resq/-/resq-1.10.2.tgz#cedf4f20d53f6e574b1e12afbda446ad9576c193"
-  integrity sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A==
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/resq/-/resq-1.11.0.tgz#edec8c58be9af800fd628118c0ca8815283de196"
+  integrity sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==
   dependencies:
     fast-deep-equal "^2.0.1"
 
@@ -9835,13 +10025,20 @@ rxjs@6.6.7:
   dependencies:
     tslib "^1.9.0"
 
-rxjs@^7.3.0, rxjs@^7.4.0, rxjs@^7.5.5:
+rxjs@^7.3.0, rxjs@^7.4.0:
   version "7.5.7"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
   integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==
   dependencies:
     tslib "^2.1.0"
 
+rxjs@^7.5.5:
+  version "7.8.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
+  integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
+  dependencies:
+    tslib "^2.1.0"
+
 safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -10191,6 +10388,15 @@ smart-buffer@^4.2.0:
   resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
   integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
 
+smtp-server@^3.9.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/smtp-server/-/smtp-server-3.11.0.tgz#8820c191124fab37a8f16c8325a7f1fd38092c4f"
+  integrity sha512-j/W6mEKeMNKuiM9oCAAjm87agPEN1O3IU4cFLT4ZOCyyq3UXN7HiIXF+q7izxJcYSar15B/JaSxcijoPCR8Tag==
+  dependencies:
+    base32.js "0.1.0"
+    ipv6-normalize "1.0.1"
+    nodemailer "6.7.3"
+
 socket.io-client@^4.5.4:
   version "4.5.4"
   resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9"
@@ -10420,6 +10626,13 @@ statuses@2.0.1:
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
 
+stop-iteration-iterator@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
+  integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==
+  dependencies:
+    internal-slot "^1.0.4"
+
 stream-browserify@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f"
@@ -10980,6 +11193,13 @@ trim-newlines@^3.0.0:
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
   integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
+trouter@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/trouter/-/trouter-2.0.1.tgz#2726a5f8558e090d24c3a393f09eaab1df232df6"
+  integrity sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==
+  dependencies:
+    matchit "^1.0.0"
+
 ts-loader@^9.3.0:
   version "9.4.1"
   resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.1.tgz#b6f3d82db0eac5a8295994f8cb5e4940ff6b1060"
@@ -11280,6 +11500,14 @@ utp-native@^2.5.3:
     timeout-refresh "^1.0.0"
     unordered-set "^2.0.1"
 
+uue@^3.1.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2"
+  integrity sha512-axKLXVqwtdI/czrjG0X8hyV1KLgeWx8F4KvSbvVCnS+RUvsQMGRjx0kfuZDXXqj0LYvVJmx3B9kWlKtEdRrJLg==
+  dependencies:
+    escape-string-regexp "~1.0.5"
+    extend "~3.0.0"
+
 uuid@8.3.2, uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
@@ -11415,31 +11643,31 @@ wdio-geckodriver-service@^3.0.2:
     split2 "^4.1.0"
     tcp-port-used "^1.0.2"
 
-webdriver@7.26.0:
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.26.0.tgz#cc20640ee9906c0126044449dfe9562b6277d14e"
-  integrity sha512-T21T31wq29D/rmpFHcAahhdrvfsfXsLs/LBe2su7wL725ptOEoSssuDXjXMkwjf9MSUIXnTcUIz8oJGbKRUMwQ==
+webdriver@7.27.0:
+  version "7.27.0"
+  resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.27.0.tgz#41d23a6c38bd79ea868f0b9fb9c9e3d4b6e4f8bd"
+  integrity sha512-870uIBnrGJ86g3DdYjM+PHhqdWf6NxysSme1KIs6irWxK+LqcaWKWhN75PldE+04xJB2mVWt1tKn0NBBFTWeMg==
   dependencies:
     "@types/node" "^18.0.0"
     "@wdio/config" "7.26.0"
     "@wdio/logger" "7.26.0"
-    "@wdio/protocols" "7.22.0"
+    "@wdio/protocols" "7.27.0"
     "@wdio/types" "7.26.0"
     "@wdio/utils" "7.26.0"
     got "^11.0.2"
     ky "0.30.0"
     lodash.merge "^4.6.1"
 
-webdriverio@7.26.0:
-  version "7.26.0"
-  resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.26.0.tgz#d6036d950ef96fb6cc29c6c5c9cfc452fcafa59a"
-  integrity sha512-7m9TeP871aYxZYKBI4GDh5aQZLN9Fd/PASu5K/jEIT65J4OBB6g5ZaycGFOmfNHCfjWKjwPXZuKiN1f2mcrcRg==
+webdriverio@7.29.1:
+  version "7.29.1"
+  resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.29.1.tgz#f71c9de317326cff36d22f6277669477e5340c6f"
+  integrity sha512-2xhoaZvV0tzOgnj8H/B4Yol8LTcIrWdfTdfe01d+ERtdzKCoqimmPNP4vpr2lVRVKL/TW4rfoBTBNvDUaJHe2g==
   dependencies:
     "@types/aria-query" "^5.0.0"
     "@types/node" "^18.0.0"
     "@wdio/config" "7.26.0"
     "@wdio/logger" "7.26.0"
-    "@wdio/protocols" "7.22.0"
+    "@wdio/protocols" "7.27.0"
     "@wdio/repl" "7.26.0"
     "@wdio/types" "7.26.0"
     "@wdio/utils" "7.26.0"
@@ -11447,8 +11675,8 @@ webdriverio@7.26.0:
     aria-query "^5.0.0"
     css-shorthand-properties "^1.1.1"
     css-value "^0.0.1"
-    devtools "7.26.0"
-    devtools-protocol "^0.0.1069585"
+    devtools "7.28.1"
+    devtools-protocol "^0.0.1085790"
     fs-extra "^10.0.0"
     grapheme-splitter "^1.0.2"
     lodash.clonedeep "^4.5.0"
@@ -11461,7 +11689,7 @@ webdriverio@7.26.0:
     resq "^1.9.1"
     rgb2hex "0.2.5"
     serialize-error "^8.0.0"
-    webdriver "7.26.0"
+    webdriver "7.27.0"
 
 webidl-conversions@^3.0.0:
   version "3.0.1"
@@ -11732,7 +11960,7 @@ which-module@^2.0.0:
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
 
-which-typed-array@^1.1.8:
+which-typed-array@^1.1.9:
   version "1.1.9"
   resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"
   integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==
@@ -11770,6 +11998,11 @@ wildcard@^2.0.0:
   resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
   integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
 
+wildstring@1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/wildstring/-/wildstring-1.0.9.tgz#82a696d5653c7d4ec9ba716859b6b53aba2761c5"
+  integrity sha512-XBNxKIMLO6uVHf1Xvo++HGWAZZoiVCHmEMCmZJzJ82vQsuUJCLw13Gzq0mRCATk7a3+ZcgeOKSDioavuYqtlfA==
+
 word-wrap@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"