git subrepo commit (merge) mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "02ae5285"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "649a5c01"
git-subrepo: version:  "0.4.3"
  origin:   "???"
  commit:   "???"
Change-Id: I870ad468fba026cc5abf3c5699ed1e12ff28b32b
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.gitattributes b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.gitattributes
new file mode 100644
index 0000000..2a0e9d2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.gitattributes
@@ -0,0 +1 @@
+tests/ export-ignore
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/FUNDING.yml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/FUNDING.yml
new file mode 100644
index 0000000..ad129a4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: [stevebauman]
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/bug_report.md b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..a56a2af
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,15 @@
+---
+name: Bug report
+about: Create a report to help improve LdapRecord
+title: "[Bug]"
+labels: bug
+assignees: ''
+
+---
+
+<!-- Please update the below information with your environment. -->
+**Environment:**
+ - LDAP Server Type: [e.g. ActiveDirectory / OpenLDAP / FreeIPA]
+ - PHP Version: [e.g. 7.3 / 7.4 / 8.0]
+
+**Describe the bug:**
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/feature_request.md b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..68e5bcd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,10 @@
+---
+name: Feature request
+about: Suggest an idea for LdapRecord
+title: "[Feature]"
+labels: enhancement
+assignees: ''
+
+---
+
+**Describe the feature you'd like:**
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/support---help-request.md b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/support---help-request.md
new file mode 100644
index 0000000..230916c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/support---help-request.md
@@ -0,0 +1,17 @@
+---
+name: Support / help request
+about: Request help using LdapRecord (requires sponsorship)
+title: "[Support]"
+labels: question
+assignees: ''
+
+---
+
+<!-- ISSUE WILL BE CLOSED WITHOUT SPONSORSHIP: -->
+<!-- https://github.com/sponsors/stevebauman -->
+<!-- Thank you for your understanding. -->
+
+<!-- Please update the below information with your environment. -->
+**Environment:**
+ - LDAP Server Type: [e.g. ActiveDirectory / OpenLDAP / FreeIPA]
+ - PHP Version: [e.g. 7.3 / 7.4 / 8.0]
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-tests.yml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..ba00218
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-tests.yml
@@ -0,0 +1,41 @@
+name: run-tests
+
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  run-tests:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        php: [8.0, 7.4, 7.3]
+
+    name: ${{ matrix.os }} - P${{ matrix.php }}
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: Cache dependencies
+        uses: actions/cache@v2
+        with:
+          path: ~/.composer/cache/files
+          key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
+
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: ldap, json
+          coverage: none
+
+      - name: Install dependencies
+        run: composer update --prefer-dist --no-interaction
+
+      - name: Execute tests
+        run: vendor/bin/phpunit
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.gitignore b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.gitignore
new file mode 100644
index 0000000..d5389fd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.gitignore
@@ -0,0 +1,5 @@
+vendor
+composer.lock
+.php_cs.cache
+.phpunit.result.cache
+.php-cs-fixer.cache
\ No newline at end of file
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.scrutinizer.yml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.scrutinizer.yml
new file mode 100644
index 0000000..b3f8f88
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.scrutinizer.yml
@@ -0,0 +1,9 @@
+filter:
+    excluded_paths:
+        - tests/*
+build:
+    nodes:
+        analysis:
+            tests:
+                override:
+                    - command: php-scrutinizer-run
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.styleci.yml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.styleci.yml
new file mode 100644
index 0000000..c774021
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/.styleci.yml
@@ -0,0 +1,4 @@
+preset: laravel
+enabled:
+  - phpdoc_align
+  - unalign_double_arrow
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/composer.json b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/composer.json
new file mode 100644
index 0000000..2e995d9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/composer.json
@@ -0,0 +1,59 @@
+{
+    "name": "directorytree/ldaprecord",
+    "type": "library",
+    "description": "A fully-featured LDAP ORM.",
+    "homepage": "https://www.ldaprecord.com",
+    "keywords": [
+        "active directory",
+        "directory",
+        "ad",
+        "ldap",
+        "windows",
+        "adldap",
+        "adldap2",
+        "ldaprecord",
+        "orm"
+    ],
+    "license": "MIT",
+    "support": {
+        "docs": "https://ldaprecord.com",
+        "issues": "https://github.com/DirectoryTree/LdapRecord/issues",
+        "source": "https://github.com/DirectoryTree/LdapRecord",
+        "email": "steven_bauman@outlook.com"
+    },
+    "authors": [
+        {
+            "name": "Steve Bauman",
+            "email": "steven_bauman@outlook.com",
+            "role": "Developer"
+        }
+    ],
+    "require": {
+        "php": ">=7.3",
+        "ext-ldap": "*",
+        "ext-json": "*",
+        "psr/log": "^1.0",
+        "psr/simple-cache": "^1.0",
+        "nesbot/carbon": "^1.0|^2.0",
+        "tightenco/collect": "^5.6|^6.0|^7.0|^8.0",
+        "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^8.0",
+        "mockery/mockery": "^1.0",
+        "spatie/ray": "^1.24"
+    },
+    "archive": {
+        "exclude": ["/tests"]
+    },
+    "autoload": {
+        "psr-4": {
+            "LdapRecord\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "LdapRecord\\Tests\\": "tests/"
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/license.md b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/license.md
new file mode 100644
index 0000000..c25dc60
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/license.md
@@ -0,0 +1,8 @@
+The MIT License (MIT)
+Copyright © Steve Bauman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/phpunit.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/phpunit.xml
new file mode 100644
index 0000000..d03fdc2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/phpunit.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+         backupStaticAttributes="false"
+         bootstrap="vendor/autoload.php"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="false"
+        >
+    <testsuites>
+        <testsuite name="LdapRecord Test Suite">
+            <directory suffix="Test.php">./tests/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/psalm.xml b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/psalm.xml
new file mode 100644
index 0000000..7c0333d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/psalm.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<psalm
+    errorLevel="7"
+    resolveFromConfigFile="true"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns="https://getpsalm.org/schema/config"
+    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+>
+    <projectFiles>
+        <directory name="src" />
+        <ignoreFiles>
+            <directory name="vendor" />
+        </ignoreFiles>
+    </projectFiles>
+</psalm>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/readme.md b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/readme.md
new file mode 100644
index 0000000..08ecd9e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/readme.md
@@ -0,0 +1,94 @@
+<!-- readme.md -->
+
+<p align="center">
+    <img src="https://ldaprecord.com/logo.svg" width="400">
+</p>
+
+<p align="center">
+    <a href="https://github.com/DirectoryTree/LdapRecord/actions">
+        <img src="https://img.shields.io/github/workflow/status/directorytree/ldaprecord/run-tests.svg?style=flat-square">
+    </a>
+    <a href="https://scrutinizer-ci.com/g/DirectoryTree/LdapRecord/?branch=master">
+        <img src="https://img.shields.io/scrutinizer/g/DirectoryTree/LdapRecord/master.svg?style=flat-square"/>
+    </a>
+    <a href="https://packagist.org/packages/DirectoryTree/LdapRecord">
+        <img src="https://img.shields.io/packagist/dt/DirectoryTree/LdapRecord.svg?style=flat-square"/>
+    </a>
+    <a href="https://packagist.org/packages/DirectoryTree/LdapRecord">
+        <img src="https://img.shields.io/packagist/v/DirectoryTree/LdapRecord.svg?style=flat-square"/>
+    </a>
+    <a href="https://packagist.org/packages/DirectoryTree/LdapRecord">
+        <img src="https://img.shields.io/github/license/DirectoryTree/LdapRecord.svg?style=flat-square"/>
+    </a>
+</p>
+
+<p align="center">
+    Working with LDAP doesn't need to be hard.
+</p>
+
+<p align="center">
+    LdapRecord is a fully-featured <a href="https://en.wikipedia.org/wiki/Active_record_pattern">Active Record</a>
+    ORM that makes working with LDAP directories a breeze 🍃
+</p>
+
+<h4 align="center">
+    <a href="https://ldaprecord.com/docs/core/v2/quickstart/">Quickstart</a>
+    <span> · </span>
+    <a href="https://ldaprecord.com/docs/core/v2/">Documentation</a>
+    <span> · </span>
+    <a href="https://github.com/DirectoryTree/LdapRecord-Laravel">Laravel Integration</a>
+    <span> · </span>
+    <a href="https://github.com/DirectoryTree/LdapRecord/discussions/new">Post a Question</a>
+</h4>
+
+---
+
+⏲ **Up and Running Fast**
+
+Connect to your LDAP servers and start running queries at lightning speed.
+
+💡 **Fluent Filter Builder**
+
+Find the LDAP objects you're looking for with a fluent LDAP filter builder.
+
+💼 **Multi-Domain Ready**
+
+Built-in connection management allows you to access multiple domains without breaking a sweat.
+
+🔥 **Supercharged Active Record**
+
+Create and modify LDAP objects with minimal code.
+
+---
+
+<h3 align="center">
+Active Directory Features
+</h3>
+
+🚪 **Enable / Disable Accounts**
+
+Detect and assign User Account Control values on accounts with the fluent [Account Control builder](https://ldaprecord.com/docs/core/v2/active-directory/users/#uac).
+
+🔑 **Reset / Change Passwords**
+
+Built-in support for [changing](https://ldaprecord.com/docs/core/v2/active-directory/users/#changing-passwords) and [resetting](https://ldaprecord.com/docs/core/v2/active-directory/users/#resetting-passwords) passwords on Active Directory accounts.
+
+🗑 **Restore Deleted Objects**
+
+We've all been there -- accidentally deleting a user or group in Active Directory. [Restore them](https://ldaprecord.com/docs/core/v2/models/#restoring-deleted-models) by seamlessly accessing your directory's recycle bin.
+
+---
+
+<h3 align="center">LdapRecord is Supportware™</h3>
+
+<p align="center">If you require support using LdapRecord, a <a href="https://github.com/sponsors/stevebauman">sponsorship</a> is required :pray:</p>
+
+<p align="center">Thank you for your understanding :heart:</p>
+
+--- 
+
+<h3 align="center">Security Vulnerabilities</h3>
+
+<p align="center">If you discover a security vulnerability within LdapRecord, please send an e-mail to Steve Bauman via <a href="mailto:steven_bauman@outlook.com">steven_bauman@outlook.com</a>.</p>
+
+<p align="center">All security vulnerabilities will be promptly addressed.</p>
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/BindException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/BindException.php
new file mode 100644
index 0000000..d87abc1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/BindException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use LdapRecord\LdapRecordException;
+
+class BindException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Attempting.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Attempting.php
new file mode 100644
index 0000000..3776401
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Attempting.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Attempting extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Binding.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Binding.php
new file mode 100644
index 0000000..faffd85
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Binding.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Binding extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Bound.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Bound.php
new file mode 100644
index 0000000..65a3fae
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Bound.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Bound extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php
new file mode 100644
index 0000000..83716b4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+use LdapRecord\LdapInterface;
+
+abstract class Event
+{
+    /**
+     * The connection that the username and password is being bound on.
+     *
+     * @var LdapInterface
+     */
+    protected $connection;
+
+    /**
+     * The username that is being used for binding.
+     *
+     * @var string
+     */
+    protected $username;
+
+    /**
+     * The password that is being used for binding.
+     *
+     * @var string
+     */
+    protected $password;
+
+    /**
+     * Constructor.
+     *
+     * @param LdapInterface $connection
+     * @param string        $username
+     * @param string        $password
+     */
+    public function __construct(LdapInterface $connection, $username, $password)
+    {
+        $this->connection = $connection;
+        $this->username = $username;
+        $this->password = $password;
+    }
+
+    /**
+     * Returns the events connection.
+     *
+     * @return LdapInterface
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * Returns the authentication events username.
+     *
+     * @return string
+     */
+    public function getUsername()
+    {
+        return $this->username;
+    }
+
+    /**
+     * Returns the authentication events password.
+     *
+     * @return string
+     */
+    public function getPassword()
+    {
+        return $this->password;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Failed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Failed.php
new file mode 100644
index 0000000..7133e43
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Failed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Failed extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Passed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Passed.php
new file mode 100644
index 0000000..2442f3e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Passed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Passed extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php
new file mode 100644
index 0000000..696cc40
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use Exception;
+use LdapRecord\Auth\Events\Attempting;
+use LdapRecord\Auth\Events\Binding;
+use LdapRecord\Auth\Events\Bound;
+use LdapRecord\Auth\Events\Failed;
+use LdapRecord\Auth\Events\Passed;
+use LdapRecord\Configuration\DomainConfiguration;
+use LdapRecord\Events\DispatcherInterface;
+use LdapRecord\LdapInterface;
+
+class Guard
+{
+    /**
+     * The connection to bind to.
+     *
+     * @var LdapInterface
+     */
+    protected $connection;
+
+    /**
+     * The domain configuration to utilize.
+     *
+     * @var DomainConfiguration
+     */
+    protected $configuration;
+
+    /**
+     * The event dispatcher.
+     *
+     * @var DispatcherInterface
+     */
+    protected $events;
+
+    /**
+     * Constructor.
+     *
+     * @param LdapInterface       $connection
+     * @param DomainConfiguration $configuration
+     */
+    public function __construct(LdapInterface $connection, DomainConfiguration $configuration)
+    {
+        $this->connection = $connection;
+        $this->configuration = $configuration;
+    }
+
+    /**
+     * Attempt binding a user to the LDAP server.
+     *
+     * @param string $username
+     * @param string $password
+     * @param bool   $stayBound
+     *
+     * @throws UsernameRequiredException
+     * @throws PasswordRequiredException
+     *
+     * @return bool
+     */
+    public function attempt($username, $password, $stayBound = false)
+    {
+        switch (true) {
+            case empty($username):
+                throw new UsernameRequiredException('A username must be specified.');
+            case empty($password):
+                throw new PasswordRequiredException('A password must be specified.');
+        }
+
+        $this->fireAttemptingEvent($username, $password);
+
+        try {
+            $this->bind($username, $password);
+
+            $authenticated = true;
+
+            $this->firePassedEvent($username, $password);
+        } catch (BindException $e) {
+            $authenticated = false;
+        }
+
+        if (! $stayBound) {
+            $this->bindAsConfiguredUser();
+        }
+
+        return $authenticated;
+    }
+
+    /**
+     * Attempt binding a user to the LDAP server. Supports anonymous binding.
+     *
+     * @param string|null $username
+     * @param string|null $password
+     *
+     * @throws BindException
+     * @throws \LdapRecord\ConnectionException
+     */
+    public function bind($username = null, $password = null)
+    {
+        $this->fireBindingEvent($username, $password);
+
+        // Prior to binding, we will upgrade our connectivity to TLS on our current
+        // connection and ensure we are not already bound before upgrading.
+        // This is to prevent subsequent upgrading on several binds.
+        if ($this->connection->isUsingTLS() && ! $this->connection->isBound()) {
+            $this->connection->startTLS();
+        }
+
+        try {
+            if (! $this->connection->bind($username, $password)) {
+                throw new Exception($this->connection->getLastError(), $this->connection->errNo());
+            }
+
+            $this->fireBoundEvent($username, $password);
+        } catch (Exception $e) {
+            $this->fireFailedEvent($username, $password);
+
+            throw BindException::withDetailedError($e, $this->connection->getDetailedError());
+        }
+    }
+
+    /**
+     * Bind to the LDAP server using the configured username and password.
+     *
+     * @throws BindException
+     * @throws \LdapRecord\ConnectionException
+     * @throws \LdapRecord\Configuration\ConfigurationException
+     */
+    public function bindAsConfiguredUser()
+    {
+        $this->bind(
+            $this->configuration->get('username'),
+            $this->configuration->get('password')
+        );
+    }
+
+    /**
+     * Get the event dispatcher instance.
+     *
+     * @return DispatcherInterface
+     */
+    public function getDispatcher()
+    {
+        return $this->events;
+    }
+
+    /**
+     * Set the event dispatcher instance.
+     *
+     * @param DispatcherInterface $dispatcher
+     *
+     * @return void
+     */
+    public function setDispatcher(DispatcherInterface $dispatcher)
+    {
+        $this->events = $dispatcher;
+    }
+
+    /**
+     * Fire the attempting event.
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return void
+     */
+    protected function fireAttemptingEvent($username, $password)
+    {
+        if (isset($this->events)) {
+            $this->events->fire(new Attempting($this->connection, $username, $password));
+        }
+    }
+
+    /**
+     * Fire the passed event.
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return void
+     */
+    protected function firePassedEvent($username, $password)
+    {
+        if (isset($this->events)) {
+            $this->events->fire(new Passed($this->connection, $username, $password));
+        }
+    }
+
+    /**
+     * Fire the failed event.
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return void
+     */
+    protected function fireFailedEvent($username, $password)
+    {
+        if (isset($this->events)) {
+            $this->events->fire(new Failed($this->connection, $username, $password));
+        }
+    }
+
+    /**
+     * Fire the binding event.
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return void
+     */
+    protected function fireBindingEvent($username, $password)
+    {
+        if (isset($this->events)) {
+            $this->events->fire(new Binding($this->connection, $username, $password));
+        }
+    }
+
+    /**
+     * Fire the bound event.
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return void
+     */
+    protected function fireBoundEvent($username, $password)
+    {
+        if (isset($this->events)) {
+            $this->events->fire(new Bound($this->connection, $username, $password));
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/PasswordRequiredException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/PasswordRequiredException.php
new file mode 100644
index 0000000..7b2bbd1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/PasswordRequiredException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use LdapRecord\LdapRecordException;
+
+class PasswordRequiredException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/UsernameRequiredException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/UsernameRequiredException.php
new file mode 100644
index 0000000..838ae58
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/UsernameRequiredException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use LdapRecord\LdapRecordException;
+
+class UsernameRequiredException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/ConfigurationException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/ConfigurationException.php
new file mode 100644
index 0000000..6a93b12
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/ConfigurationException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Configuration;
+
+use LdapRecord\LdapRecordException;
+
+class ConfigurationException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php
new file mode 100644
index 0000000..1dcdd1a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace LdapRecord\Configuration;
+
+use LdapRecord\LdapInterface;
+
+class DomainConfiguration
+{
+    /**
+     * The extended configuration options.
+     *
+     * @var array
+     */
+    protected static $extended = [];
+
+    /**
+     * The configuration options array.
+     *
+     * The default values for each key indicate the type of value it requires.
+     *
+     * @var array
+     */
+    protected $options = [
+        // An array of LDAP hosts.
+        'hosts' => [],
+
+        // The global LDAP operation timeout limit in seconds.
+        'timeout' => 5,
+
+        // The LDAP version to utilize.
+        'version' => 3,
+
+        // The port to use for connecting to your hosts.
+        'port' => LdapInterface::PORT,
+
+        // The base distinguished name of your domain.
+        'base_dn' => '',
+
+        // The username to use for binding.
+        'username' => '',
+
+        // The password to use for binding.
+        'password' => '',
+
+        // Whether or not to use SSL when connecting.
+        'use_ssl' => false,
+
+        // Whether or not to use TLS when connecting.
+        'use_tls' => false,
+
+        // Whether or not follow referrals is enabled when performing LDAP operations.
+        'follow_referrals' => false,
+
+        // Custom LDAP options.
+        'options' => [],
+    ];
+
+    /**
+     * Constructor.
+     *
+     * @param array $options
+     *
+     * @throws ConfigurationException When an option value given is an invalid type.
+     */
+    public function __construct(array $options = [])
+    {
+        $this->options = array_merge($this->options, static::$extended);
+
+        foreach ($options as $key => $value) {
+            $this->set($key, $value);
+        }
+    }
+
+    /**
+     * Extend the configuration with a custom option, or override an existing.
+     *
+     * @param string $option
+     * @param mixed  $default
+     *
+     * @return void
+     */
+    public static function extend($option, $default = null)
+    {
+        static::$extended[$option] = $default;
+    }
+
+    /**
+     * Flush the extended configuration options.
+     *
+     * @return void
+     */
+    public static function flushExtended()
+    {
+        static::$extended = [];
+    }
+
+    /**
+     * Get all configuration options.
+     *
+     * @return array
+     */
+    public function all()
+    {
+        return $this->options;
+    }
+
+    /**
+     * Set a configuration option.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @throws ConfigurationException When an option value given is an invalid type.
+     */
+    public function set($key, $value)
+    {
+        if ($this->validate($key, $value)) {
+            $this->options[$key] = $value;
+        }
+    }
+
+    /**
+     * Returns the value for the specified configuration options.
+     *
+     * @param string $key
+     *
+     * @throws ConfigurationException When the option specified does not exist.
+     *
+     * @return mixed
+     */
+    public function get($key)
+    {
+        if (! $this->has($key)) {
+            throw new ConfigurationException("Option {$key} does not exist.");
+        }
+
+        return $this->options[$key];
+    }
+
+    /**
+     * Checks if a configuration option exists.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function has($key)
+    {
+        return array_key_exists($key, $this->options);
+    }
+
+    /**
+     * Validate the configuration option.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @throws ConfigurationException When an option value given is an invalid type.
+     *
+     * @return bool
+     */
+    protected function validate($key, $value)
+    {
+        $default = $this->get($key);
+
+        if (is_array($default)) {
+            $validator = new Validators\ArrayValidator($key, $value);
+        } elseif (is_int($default)) {
+            $validator = new Validators\IntegerValidator($key, $value);
+        } elseif (is_bool($default)) {
+            $validator = new Validators\BooleanValidator($key, $value);
+        } else {
+            $validator = new Validators\StringOrNullValidator($key, $value);
+        }
+
+        return $validator->validate();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/ArrayValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/ArrayValidator.php
new file mode 100644
index 0000000..4aa43ed
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/ArrayValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class ArrayValidator extends Validator
+{
+    /**
+     * The validation exception message.
+     *
+     * @var string
+     */
+    protected $message = 'Option [:option] must be an array.';
+
+    /**
+     * @inheritdoc
+     */
+    public function passes()
+    {
+        return is_array($this->value);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/BooleanValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/BooleanValidator.php
new file mode 100644
index 0000000..1d25a4b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/BooleanValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class BooleanValidator extends Validator
+{
+    /**
+     * The validation exception message.
+     *
+     * @var string
+     */
+    protected $message = 'Option [:option] must be a boolean.';
+
+    /**
+     * @inheritdoc
+     */
+    public function passes()
+    {
+        return is_bool($this->value);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/IntegerValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/IntegerValidator.php
new file mode 100644
index 0000000..5c4f0f9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/IntegerValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class IntegerValidator extends Validator
+{
+    /**
+     * The validation exception message.
+     *
+     * @var string
+     */
+    protected $message = 'Option [:option] must be an integer.';
+
+    /**
+     * @inheritdoc
+     */
+    public function passes()
+    {
+        return is_numeric($this->value);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/StringOrNullValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/StringOrNullValidator.php
new file mode 100644
index 0000000..bc23372
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/StringOrNullValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class StringOrNullValidator extends Validator
+{
+    /**
+     * The validation exception message.
+     *
+     * @var string
+     */
+    protected $message = 'Option [:option] must be a string or null.';
+
+    /**
+     * @inheritdoc
+     */
+    public function passes()
+    {
+        return is_string($this->value) || is_null($this->value);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php
new file mode 100644
index 0000000..908a639
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+use LdapRecord\Configuration\ConfigurationException;
+
+abstract class Validator
+{
+    /**
+     * The configuration key under validation.
+     *
+     * @var string
+     */
+    protected $key;
+
+    /**
+     * The configuration value under validation.
+     *
+     * @var mixed
+     */
+    protected $value;
+
+    /**
+     * The validation exception message.
+     *
+     * @var string
+     */
+    protected $message;
+
+    /**
+     * Constructor.
+     *
+     * @param string $key
+     * @param mixed  $value
+     */
+    public function __construct($key, $value)
+    {
+        $this->key = $key;
+        $this->value = $value;
+    }
+
+    /**
+     * Determine if the validation rule passes.
+     *
+     * @return bool
+     */
+    abstract public function passes();
+
+    /**
+     * Validate the configuration value.
+     *
+     * @throws ConfigurationException
+     *
+     * @return bool
+     */
+    public function validate()
+    {
+        if (! $this->passes()) {
+            $this->fail();
+        }
+
+        return true;
+    }
+
+    /**
+     * Throw a configuration exception.
+     *
+     * @throws ConfigurationException
+     *
+     * @return void
+     */
+    protected function fail()
+    {
+        throw new ConfigurationException(
+            str_replace(':option', $this->key, $this->message)
+        );
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php
new file mode 100644
index 0000000..8ba0ef1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php
@@ -0,0 +1,511 @@
+<?php
+
+namespace LdapRecord;
+
+use Carbon\Carbon;
+use Closure;
+use LdapRecord\Auth\Guard;
+use LdapRecord\Configuration\DomainConfiguration;
+use LdapRecord\Events\DispatcherInterface;
+use LdapRecord\Query\Builder;
+use LdapRecord\Query\Cache;
+use Psr\SimpleCache\CacheInterface;
+
+class Connection
+{
+    use DetectsErrors;
+
+    /**
+     * The underlying LDAP connection.
+     *
+     * @var Ldap
+     */
+    protected $ldap;
+
+    /**
+     * The cache driver.
+     *
+     * @var Cache|null
+     */
+    protected $cache;
+
+    /**
+     * The domain configuration.
+     *
+     * @var DomainConfiguration
+     */
+    protected $configuration;
+
+    /**
+     * The event dispatcher;.
+     *
+     * @var DispatcherInterface|null
+     */
+    protected $dispatcher;
+
+    /**
+     * The current host connected to.
+     *
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * The configured domain hosts.
+     *
+     * @var array
+     */
+    protected $hosts = [];
+
+    /**
+     * The attempted hosts that failed connecting to.
+     *
+     * @var array
+     */
+    protected $attempted = [];
+
+    /**
+     * The callback to execute upon total connection failure.
+     *
+     * @var Closure
+     */
+    protected $failed;
+
+    /**
+     * The authentication guard resolver.
+     *
+     * @var Closure
+     */
+    protected $authGuardResolver;
+
+    /**
+     * Whether the connection is retrying the initial connection attempt.
+     *
+     * @var bool
+     */
+    protected $retryingInitialConnection = false;
+
+    /**
+     * Constructor.
+     *
+     * @param array              $config
+     * @param LdapInterface|null $ldap
+     */
+    public function __construct($config = [], LdapInterface $ldap = null)
+    {
+        $this->setConfiguration($config);
+
+        $this->setLdapConnection($ldap ?? new Ldap());
+
+        $this->failed = function () {
+            $this->dispatch(new Events\ConnectionFailed($this));
+        };
+
+        $this->authGuardResolver = function () {
+            return new Guard($this->ldap, $this->configuration);
+        };
+    }
+
+    /**
+     * Set the connection configuration.
+     *
+     * @param array $config
+     *
+     * @throws Configuration\ConfigurationException
+     *
+     * @return $this
+     */
+    public function setConfiguration($config = [])
+    {
+        $this->configuration = new DomainConfiguration($config);
+
+        $this->hosts = $this->configuration->get('hosts');
+
+        $this->host = reset($this->hosts);
+
+        return $this;
+    }
+
+    /**
+     * Set the LDAP connection.
+     *
+     * @param LdapInterface $ldap
+     *
+     * @return $this
+     */
+    public function setLdapConnection(LdapInterface $ldap)
+    {
+        $this->ldap = $ldap;
+
+        return $this;
+    }
+
+    /**
+     * Set the event dispatcher.
+     *
+     * @param DispatcherInterface $dispatcher
+     *
+     * @return $this
+     */
+    public function setDispatcher(DispatcherInterface $dispatcher)
+    {
+        $this->dispatcher = $dispatcher;
+
+        return $this;
+    }
+
+    /**
+     * Initializes the LDAP connection.
+     *
+     * @return void
+     */
+    public function initialize()
+    {
+        $this->configure();
+
+        $this->ldap->connect($this->host, $this->configuration->get('port'));
+    }
+
+    /**
+     * Configure the LDAP connection.
+     *
+     * @return void
+     */
+    protected function configure()
+    {
+        if ($this->configuration->get('use_ssl')) {
+            $this->ldap->ssl();
+        } elseif ($this->configuration->get('use_tls')) {
+            $this->ldap->tls();
+        }
+
+        $this->ldap->setOptions(array_replace(
+            $this->configuration->get('options'),
+            [
+                LDAP_OPT_PROTOCOL_VERSION => $this->configuration->get('version'),
+                LDAP_OPT_NETWORK_TIMEOUT => $this->configuration->get('timeout'),
+                LDAP_OPT_REFERRALS => $this->configuration->get('follow_referrals'),
+            ]
+        ));
+    }
+
+    /**
+     * Set the cache store.
+     *
+     * @param CacheInterface $store
+     *
+     * @return $this
+     */
+    public function setCache(CacheInterface $store)
+    {
+        $this->cache = new Cache($store);
+
+        return $this;
+    }
+
+    /**
+     * Get the cache store.
+     *
+     * @return Cache|null
+     */
+    public function getCache()
+    {
+        return $this->cache;
+    }
+
+    /**
+     * Get the LDAP configuration instance.
+     *
+     * @return DomainConfiguration
+     */
+    public function getConfiguration()
+    {
+        return $this->configuration;
+    }
+
+    /**
+     * Get the LDAP connection instance.
+     *
+     * @return Ldap
+     */
+    public function getLdapConnection()
+    {
+        return $this->ldap;
+    }
+
+    /**
+     * Bind to the LDAP server.
+     *
+     * If no username or password is specified, then the configured credentials are used.
+     *
+     * @param string|null $username
+     * @param string|null $password
+     *
+     * @throws Auth\BindException
+     * @throws LdapRecordException
+     *
+     * @return Connection
+     */
+    public function connect($username = null, $password = null)
+    {
+        $attempt = function () use ($username, $password) {
+            $this->dispatch(new Events\Connecting($this));
+
+            is_null($username) && is_null($password)
+                ? $this->auth()->bindAsConfiguredUser()
+                : $this->auth()->bind($username, $password);
+
+            $this->dispatch(new Events\Connected($this));
+
+            $this->retryingInitialConnection = false;
+        };
+
+        try {
+            $this->runOperationCallback($attempt);
+        } catch (LdapRecordException $e) {
+            $this->retryingInitialConnection = true;
+
+            $this->retryOnNextHost($e, $attempt);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Reconnect to the LDAP server.
+     *
+     * @throws Auth\BindException
+     * @throws ConnectionException
+     *
+     * @return void
+     */
+    public function reconnect()
+    {
+        $this->reinitialize();
+
+        $this->connect();
+    }
+
+    /**
+     * Reinitialize the connection.
+     *
+     * @return void
+     */
+    protected function reinitialize()
+    {
+        $this->disconnect();
+
+        $this->initialize();
+    }
+
+    /**
+     * Disconnect from the LDAP server.
+     *
+     * @return void
+     */
+    public function disconnect()
+    {
+        $this->ldap->close();
+    }
+
+    /**
+     * Dispatch an event.
+     *
+     * @param object $event
+     *
+     * @return void
+     */
+    public function dispatch($event)
+    {
+        if (isset($this->dispatcher)) {
+            $this->dispatcher->dispatch($event);
+        }
+    }
+
+    /**
+     * Get the attempted hosts that failed connecting to.
+     *
+     * @return array
+     */
+    public function attempted()
+    {
+        return $this->attempted;
+    }
+
+    /**
+     * Perform the operation on the LDAP connection.
+     *
+     * @param Closure $operation
+     *
+     * @return mixed
+     */
+    public function run(Closure $operation)
+    {
+        try {
+            // Before running the operation, we will check if the current
+            // connection is bound and connect if necessary. Otherwise
+            // some LDAP operations will not be executed properly.
+            if (! $this->isConnected()) {
+                $this->connect();
+            }
+
+            return $this->runOperationCallback($operation);
+        } catch (LdapRecordException $e) {
+            if ($exception = $this->getExceptionForCauseOfFailure($e)) {
+                throw $exception;
+            }
+
+            return $this->tryAgainIfCausedByLostConnection($e, $operation);
+        }
+    }
+
+    /**
+     * Attempt to get an exception for the cause of failure.
+     *
+     * @param LdapRecordException $e
+     *
+     * @return mixed
+     */
+    protected function getExceptionForCauseOfFailure(LdapRecordException $e)
+    {
+        switch (true) {
+            case $this->errorContainsMessage($e->getMessage(), 'Already exists'):
+                return Exceptions\AlreadyExistsException::withDetailedError($e, $e->getDetailedError());
+            case $this->errorContainsMessage($e->getMessage(), 'Insufficient access'):
+                return Exceptions\InsufficientAccessException::withDetailedError($e, $e->getDetailedError());
+            case $this->errorContainsMessage($e->getMessage(), 'Constraint violation'):
+                return Exceptions\ConstraintViolationException::withDetailedError($e, $e->getDetailedError());
+            default:
+                return;
+        }
+    }
+
+    /**
+     * Run the operation callback on the current LDAP connection.
+     *
+     * @param Closure $operation
+     *
+     * @throws LdapRecordException
+     *
+     * @return mixed
+     */
+    protected function runOperationCallback(Closure $operation)
+    {
+        return $operation($this->ldap);
+    }
+
+    /**
+     * Get a new auth guard instance.
+     *
+     * @return Auth\Guard
+     */
+    public function auth()
+    {
+        if (! $this->ldap->isConnected()) {
+            $this->initialize();
+        }
+
+        $guard = call_user_func($this->authGuardResolver);
+
+        $guard->setDispatcher(
+            Container::getInstance()->getEventDispatcher()
+        );
+
+        return $guard;
+    }
+
+    /**
+     * Get a new query builder for the connection.
+     *
+     * @return Query\Builder
+     */
+    public function query()
+    {
+        return (new Builder($this))
+            ->setCache($this->cache)
+            ->setBaseDn($this->configuration->get('base_dn'));
+    }
+
+    /**
+     * Determine if the LDAP connection is bound.
+     *
+     * @return bool
+     */
+    public function isConnected()
+    {
+        return $this->ldap->isBound();
+    }
+
+    /**
+     * Attempt to retry an LDAP operation if due to a lost connection.
+     *
+     * @param LdapRecordException $e
+     * @param Closure             $operation
+     *
+     * @throws LdapRecordException
+     *
+     * @return mixed
+     */
+    protected function tryAgainIfCausedByLostConnection(LdapRecordException $e, Closure $operation)
+    {
+        // If the operation failed due to a lost or failed connection,
+        // we'll attempt reconnecting and running the operation again
+        // underneath the same host, and then move onto the next.
+        if ($this->causedByLostConnection($e->getMessage())) {
+            return $this->retry($operation);
+        }
+
+        throw $e;
+    }
+
+    /**
+     * Retry the operation on the current host.
+     *
+     * @param Closure $operation
+     *
+     * @throws LdapRecordException
+     *
+     * @return mixed
+     */
+    protected function retry(Closure $operation)
+    {
+        try {
+            $this->retryingInitialConnection
+                ? $this->reinitialize()
+                : $this->reconnect();
+
+            return $this->runOperationCallback($operation);
+        } catch (LdapRecordException $e) {
+            return $this->retryOnNextHost($e, $operation);
+        }
+    }
+
+    /**
+     * Attempt the operation again on the next host.
+     *
+     * @param LdapRecordException $e
+     * @param Closure             $operation
+     *
+     * @throws LdapRecordException
+     *
+     * @return mixed
+     */
+    protected function retryOnNextHost(LdapRecordException $e, Closure $operation)
+    {
+        $this->attempted[$this->host] = Carbon::now();
+
+        if (($key = array_search($this->host, $this->hosts)) !== false) {
+            unset($this->hosts[$key]);
+        }
+
+        if ($next = reset($this->hosts)) {
+            $this->host = $next;
+
+            return $this->tryAgainIfCausedByLostConnection($e, $operation);
+        }
+
+        call_user_func($this->failed, $this->ldap);
+
+        throw $e;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionException.php
new file mode 100644
index 0000000..81691bb
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord;
+
+class ConnectionException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php
new file mode 100644
index 0000000..0eacbc3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php
@@ -0,0 +1,320 @@
+<?php
+
+namespace LdapRecord;
+
+use BadMethodCallException;
+use LdapRecord\Events\Dispatcher;
+use LdapRecord\Events\DispatcherInterface;
+use LdapRecord\Events\Logger;
+use Psr\Log\LoggerInterface;
+
+class ConnectionManager
+{
+    /**
+     * The logger instance.
+     *
+     * @var LoggerInterface|null
+     */
+    protected $logger;
+
+    /**
+     * The event dispatcher instance.
+     *
+     * @var DispatcherInterface|null
+     */
+    protected $dispatcher;
+
+    /**
+     * The added LDAP connections.
+     *
+     * @var Connection[]
+     */
+    protected $connections = [];
+
+    /**
+     * The name of the default connection.
+     *
+     * @var string
+     */
+    protected $default = 'default';
+
+    /**
+     * The events to register listeners for during initialization.
+     *
+     * @var array
+     */
+    protected $listen = [
+        'LdapRecord\Auth\Events\*',
+        'LdapRecord\Query\Events\*',
+        'LdapRecord\Models\Events\*',
+    ];
+
+    /**
+     * The method calls to proxy for compatibility.
+     *
+     * To be removed in the next major version.
+     *
+     * @var array
+     */
+    protected $proxy = [
+        'reset' => 'flush',
+        'addConnection' => 'add',
+        'getConnection' => 'get',
+        'allConnections' => 'all',
+        'removeConnection' => 'remove',
+        'getDefaultConnection' => 'getDefault',
+        'setDefaultConnection' => 'setDefault',
+        'getEventDispatcher' => 'dispatcher',
+        'setEventDispatcher' => 'setDispatcher',
+    ];
+
+    /**
+     * Constructor.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->dispatcher = new Dispatcher();
+    }
+
+    /**
+     * Forward missing method calls onto the instance.
+     *
+     * @param string $method
+     * @param mixed  $args
+     *
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        $method = $this->proxy[$method] ?? $method;
+
+        if (! method_exists($this, $method)) {
+            throw new BadMethodCallException(sprintf(
+                'Call to undefined method %s::%s()',
+                static::class,
+                $method
+            ));
+        }
+
+        return $this->{$method}(...$args);
+    }
+
+    /**
+     * Add a new connection.
+     *
+     * @param Connection  $connection
+     * @param string|null $name
+     *
+     * @return $this
+     */
+    public function add(Connection $connection, $name = null)
+    {
+        $this->connections[$name ?? $this->default] = $connection;
+
+        if ($this->dispatcher) {
+            $connection->setDispatcher($this->dispatcher);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Remove a connection.
+     *
+     * @param $name
+     *
+     * @return $this
+     */
+    public function remove($name)
+    {
+        unset($this->connections[$name]);
+
+        return $this;
+    }
+
+    /**
+     * Get all of the connections.
+     *
+     * @return Connection[]
+     */
+    public function all()
+    {
+        return $this->connections;
+    }
+
+    /**
+     * Get a connection by name or return the default.
+     *
+     * @param string|null $name
+     *
+     * @throws ContainerException If the given connection does not exist.
+     *
+     * @return Connection
+     */
+    public function get($name = null)
+    {
+        if ($this->exists($name = $name ?? $this->default)) {
+            return $this->connections[$name];
+        }
+
+        throw new ContainerException("The LDAP connection [$name] does not exist.");
+    }
+
+    /**
+     * Return the default connection.
+     *
+     * @return Connection
+     */
+    public function getDefault()
+    {
+        return $this->get($this->default);
+    }
+
+    /**
+     * Get the default connection name.
+     *
+     * @return string
+     */
+    public function getDefaultConnectionName()
+    {
+        return $this->default;
+    }
+
+    /**
+     * Checks if the connection exists.
+     *
+     * @param string $name
+     *
+     * @return bool
+     */
+    public function exists($name)
+    {
+        return array_key_exists($name, $this->connections);
+    }
+
+    /**
+     * Set the default connection name.
+     *
+     * @param string $name
+     *
+     * @return $this
+     */
+    public function setDefault($name = null)
+    {
+        $this->default = $name;
+
+        return $this;
+    }
+
+    /**
+     * Flush the manager of all instances and connections.
+     *
+     * @return $this
+     */
+    public function flush()
+    {
+        $this->logger = null;
+
+        $this->connections = [];
+
+        $this->dispatcher = new Dispatcher();
+
+        return $this;
+    }
+
+    /**
+     * Get the logger instance.
+     *
+     * @return LoggerInterface|null
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set the event logger to use.
+     *
+     * @param LoggerInterface $logger
+     *
+     * @return void
+     */
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+
+        $this->initEventLogger();
+    }
+
+    /**
+     * Initialize the event logger.
+     *
+     * @return void
+     */
+    public function initEventLogger()
+    {
+        $logger = $this->newEventLogger();
+
+        foreach ($this->listen as $event) {
+            $this->dispatcher->listen($event, function ($eventName, $events) use ($logger) {
+                foreach ($events as $event) {
+                    $logger->log($event);
+                }
+            });
+        }
+    }
+
+    /**
+     * Make a new event logger instance.
+     *
+     * @return Logger
+     */
+    protected function newEventLogger()
+    {
+        return new Logger($this->logger);
+    }
+
+    /**
+     * Unset the logger instance.
+     *
+     * @return void
+     */
+    public function unsetLogger()
+    {
+        $this->logger = null;
+    }
+
+    /**
+     * Get the event dispatcher.
+     *
+     * @return DispatcherInterface|null
+     */
+    public function dispatcher()
+    {
+        return $this->dispatcher;
+    }
+
+    /**
+     * Set the event dispatcher.
+     *
+     * @param DispatcherInterface $dispatcher
+     *
+     * @return void
+     */
+    public function setDispatcher(DispatcherInterface $dispatcher)
+    {
+        $this->dispatcher = $dispatcher;
+    }
+
+    /**
+     * Unset the event dispatcher.
+     *
+     * @return void
+     */
+    public function unsetEventDispatcher()
+    {
+        $this->dispatcher = null;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php
new file mode 100644
index 0000000..f458951
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace LdapRecord;
+
+/**
+ * @method static $this reset()
+ * @method static Connection[] all()
+ * @method static Connection[] allConnections()
+ * @method static Connection getDefaultConnection()
+ * @method static Connection get(string|null $name = null)
+ * @method static Connection getConnection(string|null $name = null)
+ * @method static bool exists(string $name)
+ * @method static $this remove(string|null $name = null)
+ * @method static $this removeConnection(string|null $name = null)
+ * @method static $this setDefault(string|null $name = null)
+ * @method static $this setDefaultConnection(string|null $name = null)
+ * @method static $this add(Connection $connection, string|null $name = null)
+ * @method static $this addConnection(Connection $connection, string|null $name = null)
+ */
+class Container
+{
+    /**
+     * The current container instance.
+     *
+     * @var Container
+     */
+    protected static $instance;
+
+    /**
+     * The connection manager instance.
+     *
+     * @var ConnectionManager
+     */
+    protected $manager;
+
+    /**
+     * The methods to passthru, for compatibility.
+     *
+     * @var array
+     */
+    protected $passthru = [
+        'reset', 'flush',
+        'add', 'addConnection',
+        'remove', 'removeConnection',
+        'setDefault', 'setDefaultConnection',
+    ];
+
+    /**
+     * Forward missing static calls onto the current instance.
+     *
+     * @param string $method
+     * @param mixed  $args
+     *
+     * @return mixed
+     */
+    public static function __callStatic($method, $args)
+    {
+        return static::getInstance()->{$method}(...$args);
+    }
+
+    /**
+     * Get or set the current instance of the container.
+     *
+     * @return Container
+     */
+    public static function getInstance()
+    {
+        return static::$instance ?? static::getNewInstance();
+    }
+
+    /**
+     * Set the container instance.
+     *
+     * @param Container|null $container
+     *
+     * @return Container|null
+     */
+    public static function setInstance(self $container = null)
+    {
+        return static::$instance = $container;
+    }
+
+    /**
+     * Set and get a new instance of the container.
+     *
+     * @return Container
+     */
+    public static function getNewInstance()
+    {
+        return static::setInstance(new static());
+    }
+
+    /**
+     * Constructor.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->manager = new ConnectionManager();
+    }
+
+    /**
+     * Forward missing method calls onto the connection manager.
+     *
+     * @param string $method
+     * @param mixed  $args
+     *
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        $value = $this->manager->{$method}(...$args);
+
+        return in_array($method, $this->passthru) ? $this : $value;
+    }
+
+    /**
+     * Get the connection manager.
+     *
+     * @return ConnectionManager
+     */
+    public function manager()
+    {
+        return $this->manager;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ContainerException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ContainerException.php
new file mode 100644
index 0000000..0ab29cf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ContainerException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord;
+
+class ContainerException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php
new file mode 100644
index 0000000..d61159e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace LdapRecord;
+
+class DetailedError
+{
+    /**
+     * The error code from ldap_errno.
+     *
+     * @var int|null
+     */
+    protected $errorCode;
+
+    /**
+     * The error message from ldap_error.
+     *
+     * @var string|null
+     */
+    protected $errorMessage;
+
+    /**
+     * The diagnostic message when retrieved after an ldap_error.
+     *
+     * @var string|null
+     */
+    protected $diagnosticMessage;
+
+    /**
+     * Constructor.
+     *
+     * @param int    $errorCode
+     * @param string $errorMessage
+     * @param string $diagnosticMessage
+     */
+    public function __construct($errorCode, $errorMessage, $diagnosticMessage)
+    {
+        $this->errorCode = $errorCode;
+        $this->errorMessage = $errorMessage;
+        $this->diagnosticMessage = $diagnosticMessage;
+    }
+
+    /**
+     * Returns the LDAP error code.
+     *
+     * @return int
+     */
+    public function getErrorCode()
+    {
+        return $this->errorCode;
+    }
+
+    /**
+     * Returns the LDAP error message.
+     *
+     * @return string
+     */
+    public function getErrorMessage()
+    {
+        return $this->errorMessage;
+    }
+
+    /**
+     * Returns the LDAP diagnostic message.
+     *
+     * @return string
+     */
+    public function getDiagnosticMessage()
+    {
+        return $this->diagnosticMessage;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php
new file mode 100644
index 0000000..e8997a9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace LdapRecord;
+
+trait DetectsErrors
+{
+    /**
+     * Determine if the error was caused by a lost connection.
+     *
+     * @param string $error
+     *
+     * @return bool
+     */
+    protected function causedByLostConnection($error)
+    {
+        return $this->errorContainsMessage($error, ["Can't contact LDAP server", 'Operations error']);
+    }
+
+    /**
+     * Determine if the error was caused by lack of pagination support.
+     *
+     * @param string $error
+     *
+     * @return bool
+     */
+    protected function causedByPaginationSupport($error)
+    {
+        return $this->errorContainsMessage($error, 'No server controls in result');
+    }
+
+    /**
+     * Determine if the error was caused by a size limit warning.
+     *
+     * @param $error
+     *
+     * @return bool
+     */
+    protected function causedBySizeLimit($error)
+    {
+        return $this->errorContainsMessage($error, ['Partial search results returned', 'Size limit exceeded']);
+    }
+
+    /**
+     * Determine if the error was caused by a "No such object" warning.
+     *
+     * @param string $error
+     *
+     * @return bool
+     */
+    protected function causedByNoSuchObject($error)
+    {
+        return $this->errorContainsMessage($error, ['No such object']);
+    }
+
+    /**
+     * Determine if the error contains the any of the messages.
+     *
+     * @param string       $error
+     * @param string|array $messages
+     *
+     * @return bool
+     */
+    protected function errorContainsMessage($error, $messages = [])
+    {
+        foreach ((array) $messages as $message) {
+            if (strpos($error, $message) !== false) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php
new file mode 100644
index 0000000..acfc020
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord;
+
+use LdapRecord\Models\Attributes\EscapedValue;
+
+trait EscapesValues
+{
+    /**
+     * Prepare a value to be escaped.
+     *
+     * @param string $value
+     * @param string $ignore
+     * @param int    $flags
+     *
+     * @return EscapedValue
+     */
+    public function escape($value, $ignore = '', $flags = 0)
+    {
+        return new EscapedValue($value, $ignore, $flags);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connected.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connected.php
new file mode 100644
index 0000000..d9505da
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connected.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Events;
+
+class Connected extends ConnectionEvent
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connecting.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connecting.php
new file mode 100644
index 0000000..d2922ad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connecting.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Events;
+
+class Connecting extends ConnectionEvent
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php
new file mode 100644
index 0000000..e9c2c35
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace LdapRecord\Events;
+
+use LdapRecord\Connection;
+
+abstract class ConnectionEvent
+{
+    /**
+     * The LDAP connection.
+     *
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
+     * Constructor.
+     *
+     * @param Connection $connection
+     */
+    public function __construct(Connection $connection)
+    {
+        $this->connection = $connection;
+    }
+
+    /**
+     * Get the connection pertaining to the event.
+     *
+     * @return Connection
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionFailed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionFailed.php
new file mode 100644
index 0000000..7e110c1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionFailed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Events;
+
+class ConnectionFailed extends ConnectionEvent
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php
new file mode 100644
index 0000000..a4ae3de
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php
@@ -0,0 +1,334 @@
+<?php
+
+namespace LdapRecord\Events;
+
+use LdapRecord\Support\Arr;
+
+/**
+ * Class Dispatcher.
+ *
+ * Handles event listening and dispatching.
+ *
+ * This code was taken out of the Laravel Framework core
+ * with broadcasting and queuing omitted to remove
+ * an extra dependency that would be required.
+ *
+ * @author Taylor Otwell
+ *
+ * @see https://github.com/laravel/framework
+ */
+class Dispatcher implements DispatcherInterface
+{
+    /**
+     * The registered event listeners.
+     *
+     * @var array
+     */
+    protected $listeners = [];
+
+    /**
+     * The wildcard listeners.
+     *
+     * @var array
+     */
+    protected $wildcards = [];
+
+    /**
+     * The cached wildcard listeners.
+     *
+     * @var array
+     */
+    protected $wildcardsCache = [];
+
+    /**
+     * @inheritdoc
+     */
+    public function listen($events, $listener)
+    {
+        foreach ((array) $events as $event) {
+            if (strpos($event, '*') !== false) {
+                $this->setupWildcardListen($event, $listener);
+            } else {
+                $this->listeners[$event][] = $this->makeListener($listener);
+            }
+        }
+    }
+
+    /**
+     * Setup a wildcard listener callback.
+     *
+     * @param string $event
+     * @param mixed  $listener
+     *
+     * @return void
+     */
+    protected function setupWildcardListen($event, $listener)
+    {
+        $this->wildcards[$event][] = $this->makeListener($listener, true);
+
+        $this->wildcardsCache = [];
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function hasListeners($eventName)
+    {
+        return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function until($event, $payload = [])
+    {
+        return $this->dispatch($event, $payload, true);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function fire($event, $payload = [], $halt = false)
+    {
+        return $this->dispatch($event, $payload, $halt);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function dispatch($event, $payload = [], $halt = false)
+    {
+        // When the given "event" is actually an object we will assume it is an event
+        // object and use the class as the event name and this event itself as the
+        // payload to the handler, which makes object based events quite simple.
+        [$event, $payload] = $this->parseEventAndPayload(
+            $event,
+            $payload
+        );
+
+        $responses = [];
+
+        foreach ($this->getListeners($event) as $listener) {
+            $response = $listener($event, $payload);
+
+            // If a response is returned from the listener and event halting is enabled
+            // we will just return this response, and not call the rest of the event
+            // listeners. Otherwise we will add the response on the response list.
+            if ($halt && ! is_null($response)) {
+                return $response;
+            }
+
+            // If a boolean false is returned from a listener, we will stop propagating
+            // the event to any further listeners down in the chain, else we keep on
+            // looping through the listeners and firing every one in our sequence.
+            if ($response === false) {
+                break;
+            }
+
+            $responses[] = $response;
+        }
+
+        return $halt ? null : $responses;
+    }
+
+    /**
+     * Parse the given event and payload and prepare them for dispatching.
+     *
+     * @param mixed $event
+     * @param mixed $payload
+     *
+     * @return array
+     */
+    protected function parseEventAndPayload($event, $payload)
+    {
+        if (is_object($event)) {
+            [$payload, $event] = [[$event], get_class($event)];
+        }
+
+        return [$event, Arr::wrap($payload)];
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getListeners($eventName)
+    {
+        $listeners = $this->listeners[$eventName] ?? [];
+
+        $listeners = array_merge(
+            $listeners,
+            $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
+        );
+
+        return class_exists($eventName, false)
+            ? $this->addInterfaceListeners($eventName, $listeners)
+            : $listeners;
+    }
+
+    /**
+     * Get the wildcard listeners for the event.
+     *
+     * @param string $eventName
+     *
+     * @return array
+     */
+    protected function getWildcardListeners($eventName)
+    {
+        $wildcards = [];
+
+        foreach ($this->wildcards as $key => $listeners) {
+            if ($this->wildcardContainsEvent($key, $eventName)) {
+                $wildcards = array_merge($wildcards, $listeners);
+            }
+        }
+
+        return $this->wildcardsCache[$eventName] = $wildcards;
+    }
+
+    /**
+     * Determine if the wildcard matches or contains the given event.
+     *
+     * This function is a direct excerpt from Laravel's Str::is().
+     *
+     * @param string $wildcard
+     * @param string $eventName
+     *
+     * @return bool
+     */
+    protected function wildcardContainsEvent($wildcard, $eventName)
+    {
+        $patterns = Arr::wrap($wildcard);
+
+        if (empty($patterns)) {
+            return false;
+        }
+
+        foreach ($patterns as $pattern) {
+            // If the given event is an exact match we can of course return true right
+            // from the beginning. Otherwise, we will translate asterisks and do an
+            // actual pattern match against the two strings to see if they match.
+            if ($pattern == $eventName) {
+                return true;
+            }
+
+            $pattern = preg_quote($pattern, '#');
+
+            // Asterisks are translated into zero-or-more regular expression wildcards
+            // to make it convenient to check if the strings starts with the given
+            // pattern such as "library/*", making any string check convenient.
+            $pattern = str_replace('\*', '.*', $pattern);
+
+            if (preg_match('#^'.$pattern.'\z#u', $eventName) === 1) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Add the listeners for the event's interfaces to the given array.
+     *
+     * @param string $eventName
+     * @param array  $listeners
+     *
+     * @return array
+     */
+    protected function addInterfaceListeners($eventName, array $listeners = [])
+    {
+        foreach (class_implements($eventName) as $interface) {
+            if (isset($this->listeners[$interface])) {
+                foreach ($this->listeners[$interface] as $names) {
+                    $listeners = array_merge($listeners, (array) $names);
+                }
+            }
+        }
+
+        return $listeners;
+    }
+
+    /**
+     * Register an event listener with the dispatcher.
+     *
+     * @param \Closure|string $listener
+     * @param bool            $wildcard
+     *
+     * @return \Closure
+     */
+    public function makeListener($listener, $wildcard = false)
+    {
+        if (is_string($listener)) {
+            return $this->createClassListener($listener, $wildcard);
+        }
+
+        return function ($event, $payload) use ($listener, $wildcard) {
+            if ($wildcard) {
+                return $listener($event, $payload);
+            }
+
+            return $listener(...array_values($payload));
+        };
+    }
+
+    /**
+     * Create a class based listener.
+     *
+     * @param string $listener
+     * @param bool   $wildcard
+     *
+     * @return \Closure
+     */
+    protected function createClassListener($listener, $wildcard = false)
+    {
+        return function ($event, $payload) use ($listener, $wildcard) {
+            if ($wildcard) {
+                return call_user_func($this->createClassCallable($listener), $event, $payload);
+            }
+
+            return call_user_func_array(
+                $this->createClassCallable($listener),
+                $payload
+            );
+        };
+    }
+
+    /**
+     * Create the class based event callable.
+     *
+     * @param string $listener
+     *
+     * @return callable
+     */
+    protected function createClassCallable($listener)
+    {
+        [$class, $method] = $this->parseListenerCallback($listener);
+
+        return [new $class(), $method];
+    }
+
+    /**
+     * Parse the class listener into class and method.
+     *
+     * @param string $listener
+     *
+     * @return array
+     */
+    protected function parseListenerCallback($listener)
+    {
+        return strpos($listener, '@') !== false
+            ? explode('@', $listener, 2)
+            : [$listener, 'handle'];
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function forget($event)
+    {
+        if (strpos($event, '*') !== false) {
+            unset($this->wildcards[$event]);
+        } else {
+            unset($this->listeners[$event]);
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php
new file mode 100644
index 0000000..6b7cb10
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace LdapRecord\Events;
+
+interface DispatcherInterface
+{
+    /**
+     * Register an event listener with the dispatcher.
+     *
+     * @param string|array $events
+     * @param mixed        $listener
+     *
+     * @return void
+     */
+    public function listen($events, $listener);
+
+    /**
+     * Determine if a given event has listeners.
+     *
+     * @param string $eventName
+     *
+     * @return bool
+     */
+    public function hasListeners($eventName);
+
+    /**
+     * Fire an event until the first non-null response is returned.
+     *
+     * @param string|object $event
+     * @param mixed         $payload
+     *
+     * @return array|null
+     */
+    public function until($event, $payload = []);
+
+    /**
+     * Fire an event and call the listeners.
+     *
+     * @param string|object $event
+     * @param mixed         $payload
+     * @param bool          $halt
+     *
+     * @return mixed
+     */
+    public function fire($event, $payload = [], $halt = false);
+
+    /**
+     * Fire an event and call the listeners.
+     *
+     * @param string|object $event
+     * @param mixed         $payload
+     * @param bool          $halt
+     *
+     * @return array|null
+     */
+    public function dispatch($event, $payload = [], $halt = false);
+
+    /**
+     * Get all of the listeners for a given event name.
+     *
+     * @param string $eventName
+     *
+     * @return array
+     */
+    public function getListeners($eventName);
+
+    /**
+     * Remove a set of listeners from the dispatcher.
+     *
+     * @param string $event
+     *
+     * @return void
+     */
+    public function forget($event);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Logger.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Logger.php
new file mode 100644
index 0000000..f3840c2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Logger.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace LdapRecord\Events;
+
+use LdapRecord\Auth\Events\Event as AuthEvent;
+use LdapRecord\Auth\Events\Failed;
+use LdapRecord\Models\Events\Event as ModelEvent;
+use LdapRecord\Query\Events\QueryExecuted as QueryEvent;
+use Psr\Log\LoggerInterface;
+use ReflectionClass;
+
+class Logger
+{
+    /**
+     * The logger instance.
+     *
+     * @var LoggerInterface|null
+     */
+    protected $logger;
+
+    /**
+     * Constructor.
+     *
+     * @param LoggerInterface|null $logger
+     */
+    public function __construct(LoggerInterface $logger = null)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Logs the given event.
+     *
+     * @param mixed $event
+     *
+     * @return void
+     */
+    public function log($event)
+    {
+        switch (true) {
+            case $event instanceof AuthEvent:
+                return $this->auth($event);
+            case $event instanceof ModelEvent:
+                return $this->model($event);
+            case $event instanceof QueryEvent:
+                return $this->query($event);
+        }
+    }
+
+    /**
+     * Logs an authentication event.
+     *
+     * @param AuthEvent $event
+     *
+     * @return void
+     */
+    public function auth(AuthEvent $event)
+    {
+        if (isset($this->logger)) {
+            $connection = $event->getConnection();
+
+            $message = "LDAP ({$connection->getHost()})"
+                ." - Operation: {$this->getOperationName($event)}"
+                ." - Username: {$event->getUsername()}";
+
+            $result = null;
+            $type = 'info';
+
+            if (is_a($event, Failed::class)) {
+                $type = 'warning';
+                $result = " - Reason: {$connection->getLastError()}";
+            }
+
+            $this->logger->$type($message.$result);
+        }
+    }
+
+    /**
+     * Logs a model event.
+     *
+     * @param ModelEvent $event
+     *
+     * @return void
+     */
+    public function model(ModelEvent $event)
+    {
+        if (isset($this->logger)) {
+            $model = $event->getModel();
+
+            $on = get_class($model);
+
+            $connection = $model->getConnection()->getLdapConnection();
+
+            $message = "LDAP ({$connection->getHost()})"
+                ." - Operation: {$this->getOperationName($event)}"
+                ." - On: {$on}"
+                ." - Distinguished Name: {$model->getDn()}";
+
+            $this->logger->info($message);
+        }
+    }
+
+    /**
+     * Logs a query event.
+     *
+     * @param QueryEvent $event
+     *
+     * @return void
+     */
+    public function query(QueryEvent $event)
+    {
+        if (isset($this->logger)) {
+            $query = $event->getQuery();
+
+            $connection = $query->getConnection()->getLdapConnection();
+
+            $selected = implode(',', $query->getSelects());
+
+            $message = "LDAP ({$connection->getHost()})"
+                ." - Operation: {$this->getOperationName($event)}"
+                ." - Base DN: {$query->getBaseDn()}"
+                ." - Filter: {$query->getQuery()}"
+                ." - Selected: ({$selected})"
+                ." - Time Elapsed: {$event->getTime()}";
+
+            $this->logger->info($message);
+        }
+    }
+
+    /**
+     * Returns the operational name of the given event.
+     *
+     * @param mixed $event
+     *
+     * @return string
+     */
+    protected function getOperationName($event)
+    {
+        return (new ReflectionClass($event))->getShortName();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/AlreadyExistsException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/AlreadyExistsException.php
new file mode 100644
index 0000000..2298caf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/AlreadyExistsException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Exceptions;
+
+use LdapRecord\LdapRecordException;
+
+class AlreadyExistsException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/ConstraintViolationException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/ConstraintViolationException.php
new file mode 100644
index 0000000..641843a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/ConstraintViolationException.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace LdapRecord\Exceptions;
+
+use LdapRecord\DetectsErrors;
+use LdapRecord\LdapRecordException;
+
+class ConstraintViolationException extends LdapRecordException
+{
+    use DetectsErrors;
+
+    /**
+     * Determine if the exception was generated due to the password policy.
+     *
+     * @return bool
+     */
+    public function causedByPasswordPolicy()
+    {
+        return isset($this->detailedError)
+                ? $this->errorContainsMessage($this->detailedError->getDiagnosticMessage(), '0000052D')
+                : false;
+    }
+
+    /**
+     * Determine if the exception was generated due to an incorrect password.
+     *
+     * @return bool
+     */
+    public function causedByIncorrectPassword()
+    {
+        return isset($this->detailedError)
+                ? $this->errorContainsMessage($this->detailedError->getDiagnosticMessage(), '00000056')
+                : false;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/InsufficientAccessException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/InsufficientAccessException.php
new file mode 100644
index 0000000..89c55fd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/InsufficientAccessException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Exceptions;
+
+use LdapRecord\LdapRecordException;
+
+class InsufficientAccessException extends LdapRecordException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/HandlesConnection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/HandlesConnection.php
new file mode 100644
index 0000000..41334b6
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/HandlesConnection.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace LdapRecord;
+
+use Closure;
+use ErrorException;
+use Exception;
+
+trait HandlesConnection
+{
+    /**
+     * The LDAP host that is currently connected.
+     *
+     * @var string|null
+     */
+    protected $host;
+
+    /**
+     * The LDAP connection resource.
+     *
+     * @var resource|null
+     */
+    protected $connection;
+
+    /**
+     * The bound status of the connection.
+     *
+     * @var bool
+     */
+    protected $bound = false;
+
+    /**
+     * Whether the connection must be bound over SSL.
+     *
+     * @var bool
+     */
+    protected $useSSL = false;
+
+    /**
+     * Whether the connection must be bound over TLS.
+     *
+     * @var bool
+     */
+    protected $useTLS = false;
+
+    /**
+     * @inheritdoc
+     */
+    public function isUsingSSL()
+    {
+        return $this->useSSL;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isUsingTLS()
+    {
+        return $this->useTLS;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isBound()
+    {
+        return $this->bound;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isConnected()
+    {
+        return ! is_null($this->connection);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function canChangePasswords()
+    {
+        return $this->isUsingSSL() || $this->isUsingTLS();
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function ssl($enabled = true)
+    {
+        $this->useSSL = $enabled;
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function tls($enabled = true)
+    {
+        $this->useTLS = $enabled;
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setOptions(array $options = [])
+    {
+        foreach ($options as $option => $value) {
+            $this->setOption($option, $value);
+        }
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getHost()
+    {
+        return $this->host;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getProtocol()
+    {
+        return $this->isUsingSSL() ? LdapInterface::PROTOCOL_SSL : LdapInterface::PROTOCOL;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getExtendedError()
+    {
+        return $this->getDiagnosticMessage();
+    }
+
+    /**
+     * Convert warnings to exceptions for the given operation.
+     *
+     * @param Closure $operation
+     *
+     * @throws LdapRecordException
+     *
+     * @return mixed
+     */
+    protected function executeFailableOperation(Closure $operation)
+    {
+        // If some older versions of PHP, errors are reported instead of throwing
+        // exceptions, which could be a signifcant detriment to our application.
+        // Here, we will enforce these operations to throw exceptions instead.
+        set_error_handler(function ($severity, $message, $file, $line) {
+            if (! $this->shouldBypassError($message)) {
+                throw new ErrorException($message, $severity, $severity, $file, $line);
+            }
+        });
+
+        try {
+            if (($result = $operation()) !== false) {
+                return $result;
+            }
+
+            // If the failed query operation was a based on a query being executed
+            // -- such as a search, read, or listing, then we can safely return
+            // the failed response here and prevent throwning an exception.
+            if ($this->shouldBypassFailure($method = debug_backtrace()[1]['function'])) {
+                return $result;
+            }
+
+            throw new Exception("LDAP operation [$method] failed.");
+        } catch (ErrorException $e) {
+            throw LdapRecordException::withDetailedError($e, $this->getDetailedError());
+        } finally {
+            restore_error_handler();
+        }
+    }
+
+    /**
+     * Determine if the failed operation should be bypassed.
+     *
+     * @param string $method
+     *
+     * @return bool
+     */
+    protected function shouldBypassFailure($method)
+    {
+        return in_array($method, ['search', 'read', 'listing']);
+    }
+
+    /**
+     * Determine if the error should be bypassed.
+     *
+     * @param string $error
+     *
+     * @return bool
+     */
+    protected function shouldBypassError($error)
+    {
+        return $this->causedByPaginationSupport($error) || $this->causedBySizeLimit($error) || $this->causedByNoSuchObject($error);
+    }
+
+    /**
+     * Determine if the current PHP version supports server controls.
+     *
+     * @deprecated since v2.5.0
+     *
+     * @return bool
+     */
+    public function supportsServerControlsInMethods()
+    {
+        return version_compare(PHP_VERSION, '7.3.0') >= 0;
+    }
+
+    /**
+     * Generates an LDAP connection string for each host given.
+     *
+     * @param string|array $hosts
+     * @param string       $port
+     *
+     * @return string
+     */
+    protected function makeConnectionUris($hosts, $port)
+    {
+        // If an attempt to connect via SSL protocol is being performed,
+        // and we are still using the default port, we will swap it
+        // for the default SSL port, for developer convenience.
+        if ($this->isUsingSSL() && $port == LdapInterface::PORT) {
+            $port = LdapInterface::PORT_SSL;
+        }
+
+        // The blank space here is intentional. PHP's LDAP extension
+        // requires additional hosts to be seperated by a blank
+        // space, so that it can parse each individually.
+        return implode(' ', $this->assembleHostUris($hosts, $port));
+    }
+
+    /**
+     * Assemble the host URI strings.
+     *
+     * @param array|string $hosts
+     * @param string       $port
+     *
+     * @return array
+     */
+    protected function assembleHostUris($hosts, $port)
+    {
+        return array_map(function ($host) use ($port) {
+            return "{$this->getProtocol()}{$host}:{$port}";
+        }, (array) $hosts);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Ldap.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Ldap.php
new file mode 100644
index 0000000..6503cea
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Ldap.php
@@ -0,0 +1,480 @@
+<?php
+
+namespace LdapRecord;
+
+class Ldap implements LdapInterface
+{
+    use HandlesConnection, DetectsErrors;
+
+    /**
+     * @inheritdoc
+     */
+    public function getEntries($searchResults)
+    {
+        return $this->executeFailableOperation(function () use ($searchResults) {
+            return ldap_get_entries($this->connection, $searchResults);
+        });
+    }
+
+    /**
+     * Retrieves the first entry from a search result.
+     *
+     * @see http://php.net/manual/en/function.ldap-first-entry.php
+     *
+     * @param resource $searchResults
+     *
+     * @return resource
+     */
+    public function getFirstEntry($searchResults)
+    {
+        return $this->executeFailableOperation(function () use ($searchResults) {
+            return ldap_first_entry($this->connection, $searchResults);
+        });
+    }
+
+    /**
+     * Retrieves the next entry from a search result.
+     *
+     * @see http://php.net/manual/en/function.ldap-next-entry.php
+     *
+     * @param resource $entry
+     *
+     * @return resource
+     */
+    public function getNextEntry($entry)
+    {
+        return $this->executeFailableOperation(function () use ($entry) {
+            return ldap_next_entry($this->connection, $entry);
+        });
+    }
+
+    /**
+     * Retrieves the ldap entry's attributes.
+     *
+     * @see http://php.net/manual/en/function.ldap-get-attributes.php
+     *
+     * @param resource $entry
+     *
+     * @return array|false
+     */
+    public function getAttributes($entry)
+    {
+        return $this->executeFailableOperation(function () use ($entry) {
+            return ldap_get_attributes($this->connection, $entry);
+        });
+    }
+
+    /**
+     * Returns the number of entries from a search result.
+     *
+     * @see http://php.net/manual/en/function.ldap-count-entries.php
+     *
+     * @param resource $searchResults
+     *
+     * @return int
+     */
+    public function countEntries($searchResults)
+    {
+        return $this->executeFailableOperation(function () use ($searchResults) {
+            return ldap_count_entries($this->connection, $searchResults);
+        });
+    }
+
+    /**
+     * Compare value of attribute found in entry specified with DN.
+     *
+     * @see http://php.net/manual/en/function.ldap-compare.php
+     *
+     * @param string $dn
+     * @param string $attribute
+     * @param string $value
+     *
+     * @return mixed
+     */
+    public function compare($dn, $attribute, $value)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $attribute, $value) {
+            return ldap_compare($this->connection, $dn, $attribute, $value);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getLastError()
+    {
+        if (! $this->connection) {
+            return;
+        }
+
+        return ldap_error($this->connection);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getDetailedError()
+    {
+        if (! $number = $this->errNo()) {
+            return;
+        }
+
+        $this->getOption(LDAP_OPT_DIAGNOSTIC_MESSAGE, $message);
+
+        return new DetailedError($number, $this->err2Str($number), $message);
+    }
+
+    /**
+     * Get all binary values from the specified result entry.
+     *
+     * @see http://php.net/manual/en/function.ldap-get-values-len.php
+     *
+     * @param $entry
+     * @param $attribute
+     *
+     * @return array
+     */
+    public function getValuesLen($entry, $attribute)
+    {
+        return $this->executeFailableOperation(function () use ($entry, $attribute) {
+            return ldap_get_values_len($this->connection, $entry, $attribute);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setOption($option, $value)
+    {
+        return ldap_set_option($this->connection, $option, $value);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getOption($option, &$value = null)
+    {
+        ldap_get_option($this->connection, $option, $value);
+
+        return $value;
+    }
+
+    /**
+     * Set a callback function to do re-binds on referral chasing.
+     *
+     * @see http://php.net/manual/en/function.ldap-set-rebind-proc.php
+     *
+     * @param callable $callback
+     *
+     * @return bool
+     */
+    public function setRebindCallback(callable $callback)
+    {
+        return ldap_set_rebind_proc($this->connection, $callback);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function startTLS()
+    {
+        return $this->executeFailableOperation(function () {
+            return ldap_start_tls($this->connection);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function connect($hosts = [], $port = 389)
+    {
+        $this->bound = false;
+
+        $this->host = $this->makeConnectionUris($hosts, $port);
+
+        return $this->connection = $this->executeFailableOperation(function () {
+            return ldap_connect($this->host);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function close()
+    {
+        $result = is_resource($this->connection) ? @ldap_close($this->connection) : false;
+
+        $this->connection = null;
+        $this->bound = false;
+        $this->host = null;
+
+        return $result;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+    {
+        return $this->executeFailableOperation(function () use (
+            $dn,
+            $filter,
+            $fields,
+            $onlyAttributes,
+            $size,
+            $time,
+            $deref,
+            $serverControls
+        ) {
+            return empty($serverControls)
+                ? ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
+                : ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+    {
+        return $this->executeFailableOperation(function () use (
+            $dn,
+            $filter,
+            $fields,
+            $onlyAttributes,
+            $size,
+            $time,
+            $deref,
+            $serverControls
+        ) {
+            return empty($serverControls)
+                ? ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
+                : ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+    {
+        return $this->executeFailableOperation(function () use (
+            $dn,
+            $filter,
+            $fields,
+            $onlyAttributes,
+            $size,
+            $time,
+            $deref,
+            $serverControls
+        ) {
+            return empty($serverControls)
+                ? ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
+                : ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
+    {
+        return $this->executeFailableOperation(function () use (
+            $result,
+            &$errorCode,
+            &$dn,
+            &$errorMessage,
+            &$referrals,
+            &$serverControls
+        ) {
+            return empty($serverControls)
+                ? ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals)
+                : ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals, $serverControls);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function bind($username, $password)
+    {
+        return $this->bound = $this->executeFailableOperation(function () use ($username, $password) {
+            return ldap_bind($this->connection, $username, html_entity_decode($password));
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function add($dn, array $entry)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $entry) {
+            return ldap_add($this->connection, $dn, $entry);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function delete($dn)
+    {
+        return $this->executeFailableOperation(function () use ($dn) {
+            return ldap_delete($this->connection, $dn);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
+    {
+        return $this->executeFailableOperation(function () use (
+            $dn,
+            $newRdn,
+            $newParent,
+            $deleteOldRdn
+        ) {
+            return ldap_rename($this->connection, $dn, $newRdn, $newParent, $deleteOldRdn);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modify($dn, array $entry)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $entry) {
+            return ldap_modify($this->connection, $dn, $entry);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modifyBatch($dn, array $values)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $values) {
+            return ldap_modify_batch($this->connection, $dn, $values);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modAdd($dn, array $entry)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $entry) {
+            return ldap_mod_add($this->connection, $dn, $entry);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modReplace($dn, array $entry)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $entry) {
+            return ldap_mod_replace($this->connection, $dn, $entry);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modDelete($dn, array $entry)
+    {
+        return $this->executeFailableOperation(function () use ($dn, $entry) {
+            return ldap_mod_del($this->connection, $dn, $entry);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
+    {
+        return $this->executeFailableOperation(function () use ($pageSize, $isCritical, $cookie) {
+            return ldap_control_paged_result($this->connection, $pageSize, $isCritical, $cookie);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function controlPagedResultResponse($result, &$cookie, &$estimated = null)
+    {
+        return $this->executeFailableOperation(function () use ($result, &$cookie, &$estimated) {
+            return ldap_control_paged_result_response($this->connection, $result, $cookie, $estimated);
+        });
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function freeResult($result)
+    {
+        return ldap_free_result($result);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function errNo()
+    {
+        return $this->connection ? ldap_errno($this->connection) : null;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function err2Str($number)
+    {
+        return ldap_err2str($number);
+    }
+
+    /**
+     * Returns the extended error hex code of the last command.
+     *
+     * @return string|null
+     */
+    public function getExtendedErrorHex()
+    {
+        if (preg_match("/(?<=data\s).*?(?=,)/", $this->getExtendedError(), $code)) {
+            return $code[0];
+        }
+    }
+
+    /**
+     * Returns the extended error code of the last command.
+     *
+     * @return bool|string
+     */
+    public function getExtendedErrorCode()
+    {
+        return $this->extractDiagnosticCode($this->getExtendedError());
+    }
+
+    /**
+     * Extract the diagnostic code from the message.
+     *
+     * @param string $message
+     *
+     * @return string|bool
+     */
+    public function extractDiagnosticCode($message)
+    {
+        preg_match('/^([\da-fA-F]+):/', $message, $matches);
+
+        return isset($matches[1]) ? $matches[1] : false;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getDiagnosticMessage()
+    {
+        $this->getOption(LDAP_OPT_ERROR_STRING, $message);
+
+        return $message;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapInterface.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapInterface.php
new file mode 100644
index 0000000..a1773ad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapInterface.php
@@ -0,0 +1,517 @@
+<?php
+
+namespace LdapRecord;
+
+interface LdapInterface
+{
+    /**
+     * The SSL LDAP protocol string.
+     *
+     * @var string
+     */
+    const PROTOCOL_SSL = 'ldaps://';
+
+    /**
+     * The standard LDAP protocol string.
+     *
+     * @var string
+     */
+    const PROTOCOL = 'ldap://';
+
+    /**
+     * The LDAP SSL port number.
+     *
+     * @var string
+     */
+    const PORT_SSL = 636;
+
+    /**
+     * The standard LDAP port number.
+     *
+     * @var string
+     */
+    const PORT = 389;
+
+    /**
+     * Various useful server control OID's.
+     *
+     * @see https://ldap.com/ldap-oid-reference-guide/
+     * @see http://msdn.microsoft.com/en-us/library/cc223359.aspx
+     */
+    const OID_SERVER_START_TLS = '1.3.6.1.4.1.1466.20037';
+    const OID_SERVER_PAGED_RESULTS = '1.2.840.113556.1.4.319';
+    const OID_SERVER_SHOW_DELETED = '1.2.840.113556.1.4.417';
+    const OID_SERVER_SORT = '1.2.840.113556.1.4.473';
+    const OID_SERVER_CROSSDOM_MOVE_TARGET = '1.2.840.113556.1.4.521';
+    const OID_SERVER_NOTIFICATION = '1.2.840.113556.1.4.528';
+    const OID_SERVER_EXTENDED_DN = '1.2.840.113556.1.4.529';
+    const OID_SERVER_LAZY_COMMIT = '1.2.840.113556.1.4.619';
+    const OID_SERVER_SD_FLAGS = '1.2.840.113556.1.4.801';
+    const OID_SERVER_TREE_DELETE = '1.2.840.113556.1.4.805';
+    const OID_SERVER_DIRSYNC = '1.2.840.113556.1.4.841';
+    const OID_SERVER_VERIFY_NAME = '1.2.840.113556.1.4.1338';
+    const OID_SERVER_DOMAIN_SCOPE = '1.2.840.113556.1.4.1339';
+    const OID_SERVER_SEARCH_OPTIONS = '1.2.840.113556.1.4.1340';
+    const OID_SERVER_PERMISSIVE_MODIFY = '1.2.840.113556.1.4.1413';
+    const OID_SERVER_ASQ = '1.2.840.113556.1.4.1504';
+    const OID_SERVER_FAST_BIND = '1.2.840.113556.1.4.1781';
+    const OID_SERVER_CONTROL_VLVREQUEST = '2.16.840.1.113730.3.4.9';
+
+    /**
+     * Query OID's.
+     *
+     * @see https://ldapwiki.com/wiki/LDAP_MATCHING_RULE_IN_CHAIN
+     */
+    const OID_MATCHING_RULE_IN_CHAIN = '1.2.840.113556.1.4.1941';
+
+    /**
+     * Set the current connection to use SSL.
+     *
+     * @param bool $enabled
+     *
+     * @return $this
+     */
+    public function ssl();
+
+    /**
+     * Determine if the current connection instance is using SSL.
+     *
+     * @return bool
+     */
+    public function isUsingSSL();
+
+    /**
+     * Set the current connection to use TLS.
+     *
+     * @param bool $enabled
+     *
+     * @return $this
+     */
+    public function tls();
+
+    /**
+     * Determine if the current connection instance is using TLS.
+     *
+     * @return bool
+     */
+    public function isUsingTLS();
+
+    /**
+     * Determine if the connection is bound.
+     *
+     * @return bool
+     */
+    public function isBound();
+
+    /**
+     * Determine if the connection has been created.
+     *
+     * @return bool
+     */
+    public function isConnected();
+
+    /**
+     * Determine the connection is able to modify passwords.
+     *
+     * @return bool
+     */
+    public function canChangePasswords();
+
+    /**
+     * Returns the full LDAP host URL.
+     *
+     * Ex: ldap://192.168.1.1:386
+     *
+     * @return string|null
+     */
+    public function getHost();
+
+    /**
+     * Get the underlying connection resource.
+     *
+     * @return resource|null
+     */
+    public function getConnection();
+
+    /**
+     * Retrieve the entries from a search result.
+     *
+     * @see http://php.net/manual/en/function.ldap-get-entries.php
+     *
+     * @param resource $searchResults
+     *
+     * @return array
+     */
+    public function getEntries($searchResults);
+
+    /**
+     * Retrieve the last error on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-error.php
+     *
+     * @return string|null
+     */
+    public function getLastError();
+
+    /**
+     * Return detailed information about an error.
+     *
+     * Returns false when there was a successful last request.
+     *
+     * Returns DetailedError when there was an error.
+     *
+     * @return DetailedError|null
+     */
+    public function getDetailedError();
+
+    /**
+     * Set an option on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-set-option.php
+     *
+     * @param int   $option
+     * @param mixed $value
+     *
+     * @return bool
+     */
+    public function setOption($option, $value);
+
+    /**
+     * Set options on the current connection.
+     *
+     * @param array $options
+     *
+     * @return void
+     */
+    public function setOptions(array $options = []);
+
+    /**
+     * Get the value for the LDAP option.
+     *
+     * @see https://www.php.net/manual/en/function.ldap-get-option.php
+     *
+     * @param int   $option
+     * @param mixed $value
+     *
+     * @return mixed
+     */
+    public function getOption($option, &$value = null);
+
+    /**
+     * Starts a connection using TLS.
+     *
+     * @see http://php.net/manual/en/function.ldap-start-tls.php
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function startTLS();
+
+    /**
+     * Connects to the specified hostname using the specified port.
+     *
+     * @see http://php.net/manual/en/function.ldap-start-tls.php
+     *
+     * @param string|array $hosts
+     * @param int          $port
+     *
+     * @return resource|false
+     */
+    public function connect($hosts = [], $port = 389);
+
+    /**
+     * Closes the current connection.
+     *
+     * Returns false if no connection is present.
+     *
+     * @see http://php.net/manual/en/function.ldap-close.php
+     *
+     * @return bool
+     */
+    public function close();
+
+    /**
+     * Performs a search on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-search.php
+     *
+     * @param string $dn
+     * @param string $filter
+     * @param array  $fields
+     * @param bool   $onlyAttributes
+     * @param int    $size
+     * @param int    $time
+     * @param int    $deref
+     * @param array  $serverControls
+     *
+     * @return resource
+     */
+    public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = []);
+
+    /**
+     * Performs a single level search on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-list.php
+     *
+     * @param string $dn
+     * @param string $filter
+     * @param array  $fields
+     * @param bool   $onlyAttributes
+     * @param int    $size
+     * @param int    $time
+     * @param int    $deref
+     * @param array  $serverControls
+     *
+     * @return resource
+     */
+    public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = []);
+
+    /**
+     * Reads an entry on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-read.php
+     *
+     * @param string $dn
+     * @param string $filter
+     * @param array  $fields
+     * @param bool   $onlyAttributes
+     * @param int    $size
+     * @param int    $time
+     * @param int    $deref
+     * @param array  $serverControls
+     *
+     * @return resource
+     */
+    public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = []);
+
+    /**
+     * Extract information from an LDAP result.
+     *
+     * @see https://www.php.net/manual/en/function.ldap-parse-result.php
+     *
+     * @param resource $result
+     * @param int      $errorCode
+     * @param string   $dn
+     * @param string   $errorMessage
+     * @param array    $referrals
+     * @param array    $serverControls
+     *
+     * @return bool
+     */
+    public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = []);
+
+    /**
+     * Binds to the current connection using the specified username and password.
+     * If sasl is true, the current connection is bound using SASL.
+     *
+     * @see http://php.net/manual/en/function.ldap-bind.php
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function bind($username, $password);
+
+    /**
+     * Adds an entry to the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-add.php
+     *
+     * @param string $dn
+     * @param array  $entry
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function add($dn, array $entry);
+
+    /**
+     * Deletes an entry on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-delete.php
+     *
+     * @param string $dn
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function delete($dn);
+
+    /**
+     * Modify the name of an entry on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-rename.php
+     *
+     * @param string $dn
+     * @param string $newRdn
+     * @param string $newParent
+     * @param bool   $deleteOldRdn
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false);
+
+    /**
+     * Modifies an existing entry on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-modify.php
+     *
+     * @param string $dn
+     * @param array  $entry
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function modify($dn, array $entry);
+
+    /**
+     * Batch modifies an existing entry on the current connection.
+     *
+     * @see http://php.net/manual/en/function.ldap-modify-batch.php
+     *
+     * @param string $dn
+     * @param array  $values
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function modifyBatch($dn, array $values);
+
+    /**
+     * Add attribute values to current attributes.
+     *
+     * @see http://php.net/manual/en/function.ldap-mod-add.php
+     *
+     * @param string $dn
+     * @param array  $entry
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function modAdd($dn, array $entry);
+
+    /**
+     * Replaces attribute values with new ones.
+     *
+     * @see http://php.net/manual/en/function.ldap-mod-replace.php
+     *
+     * @param string $dn
+     * @param array  $entry
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function modReplace($dn, array $entry);
+
+    /**
+     * Delete attribute values from current attributes.
+     *
+     * @see http://php.net/manual/en/function.ldap-mod-del.php
+     *
+     * @param string $dn
+     * @param array  $entry
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function modDelete($dn, array $entry);
+
+    /**
+     * Send LDAP pagination control.
+     *
+     * @see http://php.net/manual/en/function.ldap-control-paged-result.php
+     *
+     * @param int    $pageSize
+     * @param bool   $isCritical
+     * @param string $cookie
+     *
+     * @return bool
+     */
+    public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '');
+
+    /**
+     * Retrieve the LDAP pagination cookie.
+     *
+     * @see http://php.net/manual/en/function.ldap-control-paged-result-response.php
+     *
+     * @param resource $result
+     * @param string   $cookie
+     *
+     * @return bool
+     */
+    public function controlPagedResultResponse($result, &$cookie);
+
+    /**
+     * Frees up the memory allocated internally to store the result.
+     *
+     * @see https://www.php.net/manual/en/function.ldap-free-result.php
+     *
+     * @param resource $result
+     *
+     * @return bool
+     */
+    public function freeResult($result);
+
+    /**
+     * Returns the error number of the last command executed.
+     *
+     * @see http://php.net/manual/en/function.ldap-errno.php
+     *
+     * @return int|null
+     */
+    public function errNo();
+
+    /**
+     * Returns the error string of the specified error number.
+     *
+     * @see http://php.net/manual/en/function.ldap-err2str.php
+     *
+     * @param int $number
+     *
+     * @return string
+     */
+    public function err2Str($number);
+
+    /**
+     * Returns the LDAP protocol to utilize for the current connection.
+     *
+     * @return string
+     */
+    public function getProtocol();
+
+    /**
+     * Returns the extended error code of the last command.
+     *
+     * @return string
+     */
+    public function getExtendedError();
+
+    /**
+     * Return the diagnostic Message.
+     *
+     * @return string
+     */
+    public function getDiagnosticMessage();
+
+    /**
+     * Determine if the current PHP version supports server controls.
+     *
+     * @deprecated since v2.5.0
+     *
+     * @return bool
+     */
+    public function supportsServerControlsInMethods();
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapRecordException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapRecordException.php
new file mode 100644
index 0000000..b2439bf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapRecordException.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace LdapRecord;
+
+use Exception;
+
+class LdapRecordException extends Exception
+{
+    /**
+     * The detailed LDAP error (if available).
+     *
+     * @var DetailedError|null
+     */
+    protected $detailedError;
+
+    /**
+     * Create a new Bind Exception with a detailed connection error.
+     *
+     * @param Exception          $e
+     * @param DetailedError|null $error
+     *
+     * @return $this
+     */
+    public static function withDetailedError(Exception $e, DetailedError $error = null)
+    {
+        return (new static($e->getMessage(), $e->getCode(), $e))->setDetailedError($error);
+    }
+
+    /**
+     * Set the detailed error.
+     *
+     * @param DetailedError|null $error
+     *
+     * @return $this
+     */
+    public function setDetailedError(DetailedError $error = null)
+    {
+        $this->detailedError = $error;
+
+        return $this;
+    }
+
+    /**
+     * Returns the detailed error.
+     *
+     * @return DetailedError|null
+     */
+    public function getDetailedError()
+    {
+        return $this->detailedError;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Computer.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Computer.php
new file mode 100644
index 0000000..72db0a0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Computer.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+use LdapRecord\Models\ActiveDirectory\Concerns\HasPrimaryGroup;
+
+class Computer extends Entry
+{
+    use HasPrimaryGroup;
+
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'person',
+        'organizationalperson',
+        'user',
+        'computer',
+    ];
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the current computer is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(Group::class, 'member')->with($this->primaryGroup());
+    }
+
+    /**
+     * The primary group relationship.
+     *
+     * @return Relations\HasOnePrimaryGroup
+     */
+    public function primaryGroup()
+    {
+        return $this->hasOnePrimaryGroup(Group::class, 'primarygroupid');
+    }
+
+    /**
+     * The managed by relationship.
+     *
+     * @return \LdapRecord\Models\Relations\HasOne
+     */
+    public function managedBy()
+    {
+        return $this->hasOne([Contact::class, Group::class, User::class], 'managedby');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Concerns/HasPrimaryGroup.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Concerns/HasPrimaryGroup.php
new file mode 100644
index 0000000..97fd3a1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Concerns/HasPrimaryGroup.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Concerns;
+
+use LdapRecord\Models\ActiveDirectory\Relations\HasOnePrimaryGroup;
+
+trait HasPrimaryGroup
+{
+    /**
+     * Returns a new has one primary group relationship.
+     *
+     * @param mixed  $related
+     * @param string $relationKey
+     * @param string $foreignKey
+     *
+     * @return HasOnePrimaryGroup
+     */
+    public function hasOnePrimaryGroup($related, $relationKey, $foreignKey = 'primarygroupid')
+    {
+        return new HasOnePrimaryGroup($this->newQuery(), $this, $related, $relationKey, $foreignKey);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Contact.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Contact.php
new file mode 100644
index 0000000..52c451f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Contact.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Contact extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'person',
+        'organizationalperson',
+        'contact',
+    ];
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the current contact is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(Group::class, 'member');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Container.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Container.php
new file mode 100644
index 0000000..1636cf3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Container.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Container extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'container',
+    ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Entry.php
new file mode 100644
index 0000000..79a9d63
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Entry.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+use InvalidArgumentException;
+use LdapRecord\Connection;
+use LdapRecord\Models\Attributes\Sid;
+use LdapRecord\Models\Entry as BaseEntry;
+use LdapRecord\Models\Events\Updated;
+use LdapRecord\Models\Types\ActiveDirectory;
+use LdapRecord\Query\Model\ActiveDirectoryBuilder;
+
+/** @mixin ActiveDirectoryBuilder */
+class Entry extends BaseEntry implements ActiveDirectory
+{
+    /**
+     * The default attributes that should be mutated to dates.
+     *
+     * @var array
+     */
+    protected $defaultDates = [
+        'whenchanged' => 'windows',
+        'whencreated' => 'windows',
+        'dscorepropagationdata' => 'windows',
+    ];
+
+    /**
+     * The attribute key that contains the Object SID.
+     *
+     * @var string
+     */
+    protected $sidKey = 'objectsid';
+
+    /**
+     * @inheritdoc
+     */
+    public function getObjectSidKey()
+    {
+        return $this->sidKey;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getObjectSid()
+    {
+        return $this->getFirstAttribute($this->sidKey);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getConvertedSid()
+    {
+        try {
+            return (string) new Sid($this->getObjectSid());
+        } catch (InvalidArgumentException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Create a new query builder.
+     *
+     * @param Connection $connection
+     *
+     * @return ActiveDirectoryBuilder
+     */
+    public function newQueryBuilder(Connection $connection)
+    {
+        return new ActiveDirectoryBuilder($connection);
+    }
+
+    /**
+     * Determine if the object is deleted.
+     *
+     * @return bool
+     */
+    public function isDeleted()
+    {
+        return strtoupper($this->getFirstAttribute('isDeleted')) === 'TRUE';
+    }
+
+    /**
+     * Restore a deleted object.
+     *
+     * @param string|null $newParentDn
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return bool
+     */
+    public function restore($newParentDn = null)
+    {
+        if (! $this->isDeleted()) {
+            return false;
+        }
+
+        $root = $newParentDn ?? $this->getDefaultRestoreLocation();
+        $rdn = explode('\0A', $this->getDn(), 2)[0];
+        $newDn = implode(',', [$rdn, $root]);
+
+        // We will initialize a model listener for the "updated" event to set
+        // the models distinguished name so all attributes are synchronized
+        // properly after the model has been successfully restored.
+        $this->listenForModelEvent(Updated::class, function (Updated $event) use ($newDn) {
+            if ($this->is($event->getModel())) {
+                $this->setDn($newDn);
+            }
+        });
+
+        $this->save([
+            'isDeleted' => null,
+            'distinguishedName' => $newDn,
+        ]);
+    }
+
+    /**
+     * Get the RootDSE (AD schema) record from the directory.
+     *
+     * @param string|null $connection
+     *
+     * @throws \LdapRecord\Models\ModelNotFoundException
+     *
+     * @return static
+     */
+    public static function getRootDse($connection = null)
+    {
+        return static::on($connection ?? (new static())->getConnectionName())
+            ->in(null)
+            ->read()
+            ->whereHas('objectclass')
+            ->firstOrFail();
+    }
+
+    /**
+     * Get the objects restore location.
+     *
+     * @return string
+     */
+    protected function getDefaultRestoreLocation()
+    {
+        return $this->getFirstAttribute('lastKnownParent') ?? $this->getParentDn($this->getParentDn($this->getDn()));
+    }
+
+    /**
+     * Converts attributes for JSON serialization.
+     *
+     * @param array $attributes
+     *
+     * @return array
+     */
+    protected function convertAttributesForJson(array $attributes = [])
+    {
+        $attributes = parent::convertAttributesForJson($attributes);
+
+        if ($this->hasAttribute($this->sidKey)) {
+            // If the model has a SID set, we need to convert it due to it being in
+            // binary. Otherwise we will receive a JSON serialization exception.
+            return array_replace($attributes, [
+                $this->sidKey => [$this->getConvertedSid()],
+            ]);
+        }
+
+        return $attributes;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeDatabase.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeDatabase.php
new file mode 100644
index 0000000..77abbbc
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeDatabase.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class ExchangeDatabase extends Entry
+{
+    /**
+     * @inheritdoc
+     */
+    public static $objectClasses = ['msExchMDB'];
+
+    /**
+     * @inheritdoc
+     */
+    public static function boot()
+    {
+        parent::boot();
+
+        static::addGlobalScope(new Scopes\InConfigurationContext());
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeServer.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeServer.php
new file mode 100644
index 0000000..d304876
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeServer.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class ExchangeServer extends Entry
+{
+    /**
+     * @inheritdoc
+     */
+    public static $objectClasses = ['msExchExchangeServer'];
+
+    /**
+     * @inheritdoc
+     */
+    public static function boot()
+    {
+        parent::boot();
+
+        static::addGlobalScope(new Scopes\HasServerRoleAttribute());
+        static::addGlobalScope(new Scopes\InConfigurationContext());
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ForeignSecurityPrincipal.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ForeignSecurityPrincipal.php
new file mode 100644
index 0000000..25287ae
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ForeignSecurityPrincipal.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class ForeignSecurityPrincipal extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = ['foreignsecurityprincipal'];
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the current security principal is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(Group::class, 'member');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Group.php
new file mode 100644
index 0000000..6076f2f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Group.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Group extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'group',
+    ];
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the current group is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(static::class, 'member');
+    }
+
+    /**
+     * The members relationship.
+     *
+     * Retrieves members that are apart of the group.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function members()
+    {
+        return $this->hasMany([
+            static::class, User::class, Contact::class, Computer::class,
+        ], 'memberof')
+            ->using($this, 'member')
+            ->with($this->primaryGroupMembers());
+    }
+
+    /**
+     * The primary group members relationship.
+     *
+     * Retrieves members that are apart the primary group.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function primaryGroupMembers()
+    {
+        return $this->hasMany([
+            static::class, User::class, Contact::class, Computer::class,
+        ], 'primarygroupid', 'rid');
+    }
+
+    /**
+     * Get the RID of the group.
+     *
+     * @return array
+     */
+    public function getRidAttribute()
+    {
+        $objectSidComponents = explode('-', $this->getConvertedSid());
+
+        return [end($objectSidComponents)];
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/OrganizationalUnit.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/OrganizationalUnit.php
new file mode 100644
index 0000000..80aae9f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/OrganizationalUnit.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class OrganizationalUnit extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'organizationalunit',
+    ];
+
+    /**
+     * Get the creatable RDN attribute name.
+     *
+     * @return string
+     */
+    public function getCreatableRdnAttribute()
+    {
+        return 'ou';
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Printer.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Printer.php
new file mode 100644
index 0000000..df74216
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Printer.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Printer extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = ['printqueue'];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Relations/HasOnePrimaryGroup.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Relations/HasOnePrimaryGroup.php
new file mode 100644
index 0000000..540ec77
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Relations/HasOnePrimaryGroup.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Relations;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Relations\HasOne;
+
+class HasOnePrimaryGroup extends HasOne
+{
+    /**
+     * Get the foreign model by the given value.
+     *
+     * @param string $value
+     *
+     * @return Model|null
+     */
+    protected function getForeignModelByValue($value)
+    {
+        return $this->query->findBySid(
+            $this->getParentModelObjectSid()
+        );
+    }
+
+    /**
+     * Get the foreign value from the given model.
+     *
+     * Retrieves the last RID from the models Object SID.
+     *
+     * @param Model $model
+     *
+     * @return string
+     */
+    protected function getForeignValueFromModel(Model $model)
+    {
+        $objectSidComponents = explode('-', $model->getConvertedSid());
+
+        return end($objectSidComponents);
+    }
+
+    /**
+     * Get the parent relationship models converted object sid.
+     *
+     * @return string
+     */
+    protected function getParentModelObjectSid()
+    {
+        return preg_replace(
+            '/\d+$/',
+            $this->parent->getFirstAttribute($this->relationKey),
+            $this->parent->getConvertedSid()
+        );
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/HasServerRoleAttribute.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/HasServerRoleAttribute.php
new file mode 100644
index 0000000..cd08648
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/HasServerRoleAttribute.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class HasServerRoleAttribute implements Scope
+{
+    /**
+     * Includes condition of having a serverRole attribute.
+     *
+     * @param Builder $query
+     * @param Model   $model
+     *
+     * @return void
+     */
+    public function apply(Builder $query, Model $model)
+    {
+        $query->whereHas('serverRole');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/InConfigurationContext.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/InConfigurationContext.php
new file mode 100644
index 0000000..2b1a177
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/InConfigurationContext.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Scopes;
+
+use LdapRecord\Models\ActiveDirectory\Entry;
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class InConfigurationContext implements Scope
+{
+    /**
+     * Refines the base dn to be inside the configuration context.
+     *
+     * @param Builder $query
+     * @param Model   $model
+     *
+     * @throws \LdapRecord\Models\ModelNotFoundException
+     *
+     * @return void
+     */
+    public function apply(Builder $query, Model $model)
+    {
+        $query->in($this->getConfigurationNamingContext($model));
+    }
+
+    /**
+     * Get the LDAP server configuration naming context distinguished name.
+     *
+     * @param Model $model
+     *
+     * @throws \LdapRecord\Models\ModelNotFoundException
+     *
+     * @return mixed
+     */
+    protected function getConfigurationNamingContext(Model $model)
+    {
+        return Entry::getRootDse($model->getConnectionName())
+            ->getFirstAttribute('configurationNamingContext');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/RejectComputerObjectClass.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/RejectComputerObjectClass.php
new file mode 100644
index 0000000..a616db1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/RejectComputerObjectClass.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class RejectComputerObjectClass implements Scope
+{
+    /**
+     * Prevent computer objects from being included in results.
+     *
+     * @param Builder $query
+     * @param Model   $model
+     *
+     * @return void
+     */
+    public function apply(Builder $query, Model $model)
+    {
+        $query->where('objectclass', '!=', 'computer');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/User.php
new file mode 100644
index 0000000..84dd74b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/User.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+use Illuminate\Contracts\Auth\Authenticatable;
+use LdapRecord\Models\ActiveDirectory\Concerns\HasPrimaryGroup;
+use LdapRecord\Models\ActiveDirectory\Scopes\RejectComputerObjectClass;
+use LdapRecord\Models\Concerns\CanAuthenticate;
+use LdapRecord\Models\Concerns\HasPassword;
+use LdapRecord\Query\Model\Builder;
+
+class User extends Entry implements Authenticatable
+{
+    use HasPassword;
+    use HasPrimaryGroup;
+    use CanAuthenticate;
+
+    /**
+     * The password's attribute name.
+     *
+     * @var string
+     */
+    protected $passwordAttribute = 'unicodepwd';
+
+    /**
+     * The password's hash method.
+     *
+     * @var string
+     */
+    protected $passwordHashMethod = 'encode';
+
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'person',
+        'organizationalperson',
+        'user',
+    ];
+
+    /**
+     * The attributes that should be mutated to dates.
+     *
+     * @var array
+     */
+    protected $dates = [
+        'lastlogon' => 'windows-int',
+        'lastlogoff' => 'windows-int',
+        'pwdlastset' => 'windows-int',
+        'lockouttime' => 'windows-int',
+        'accountexpires' => 'windows-int',
+        'badpasswordtime' => 'windows-int',
+        'lastlogontimestamp' => 'windows-int',
+    ];
+
+    /**
+     * @inheritdoc
+     */
+    protected static function boot()
+    {
+        parent::boot();
+
+        // Here we will add a global scope to reject the 'computer' object
+        // class. This is needed due to computer objects containing all
+        // of the ActiveDirectory 'user' object classes. Without
+        // this scope, they would be included in results.
+        static::addGlobalScope(new RejectComputerObjectClass());
+    }
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the user is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(Group::class, 'member')->with($this->primaryGroup());
+    }
+
+    /**
+     * The manager relationship.
+     *
+     * Retrieves the manager of the user.
+     *
+     * @return \LdapRecord\Models\Relations\HasOne
+     */
+    public function manager()
+    {
+        return $this->hasOne(static::class, 'manager');
+    }
+
+    /**
+     * The primary group relationship of the current user.
+     *
+     * Retrieves the primary group the user is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasOne
+     */
+    public function primaryGroup()
+    {
+        return $this->hasOnePrimaryGroup(Group::class, 'primarygroupid');
+    }
+
+    /**
+     * Scopes the query to exchange mailbox users.
+     *
+     * @param Builder $query
+     *
+     * @return Builder
+     */
+    public function scopeWhereHasMailbox(Builder $query)
+    {
+        return $query->whereHas('msExchMailboxGuid');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/AccountControl.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/AccountControl.php
new file mode 100644
index 0000000..9c6240b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/AccountControl.php
@@ -0,0 +1,502 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use ReflectionClass;
+
+class AccountControl
+{
+    const SCRIPT = 1;
+
+    const ACCOUNTDISABLE = 2;
+
+    const HOMEDIR_REQUIRED = 8;
+
+    const LOCKOUT = 16;
+
+    const PASSWD_NOTREQD = 32;
+
+    const PASSWD_CANT_CHANGE = 64;
+
+    const ENCRYPTED_TEXT_PWD_ALLOWED = 128;
+
+    const TEMP_DUPLICATE_ACCOUNT = 256;
+
+    const NORMAL_ACCOUNT = 512;
+
+    const INTERDOMAIN_TRUST_ACCOUNT = 2048;
+
+    const WORKSTATION_TRUST_ACCOUNT = 4096;
+
+    const SERVER_TRUST_ACCOUNT = 8192;
+
+    const DONT_EXPIRE_PASSWORD = 65536;
+
+    const MNS_LOGON_ACCOUNT = 131072;
+
+    const SMARTCARD_REQUIRED = 262144;
+
+    const TRUSTED_FOR_DELEGATION = 524288;
+
+    const NOT_DELEGATED = 1048576;
+
+    const USE_DES_KEY_ONLY = 2097152;
+
+    const DONT_REQ_PREAUTH = 4194304;
+
+    const PASSWORD_EXPIRED = 8388608;
+
+    const TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216;
+
+    const PARTIAL_SECRETS_ACCOUNT = 67108864;
+
+    /**
+     * The account control flag values.
+     *
+     * @var array
+     */
+    protected $values = [];
+
+    /**
+     * Constructor.
+     *
+     * @param int $flag
+     */
+    public function __construct($flag = null)
+    {
+        if (! is_null($flag)) {
+            $this->apply($flag);
+        }
+    }
+
+    /**
+     * Get the value when casted to string.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->getValue();
+    }
+
+    /**
+     * Get the value when casted to int.
+     *
+     * @return int
+     */
+    public function __toInt()
+    {
+        return $this->getValue();
+    }
+
+    /**
+     * Add the flag to the account control values.
+     *
+     * @param int $flag
+     *
+     * @return $this
+     */
+    public function add($flag)
+    {
+        // Use the value as a key so if the same value
+        // is used, it will always be overwritten
+        $this->values[$flag] = $flag;
+
+        return $this;
+    }
+
+    /**
+     * Remove the flag from the account control.
+     *
+     * @param int $flag
+     *
+     * @return $this
+     */
+    public function remove($flag)
+    {
+        unset($this->values[$flag]);
+
+        return $this;
+    }
+
+    /**
+     * Extract and apply the flag.
+     *
+     * @param int $flag
+     *
+     * @return void
+     */
+    public function apply($flag)
+    {
+        $this->setValues($this->extractFlags($flag));
+    }
+
+    /**
+     * Determine if the account control contains the given UAC flag(s).
+     *
+     * @param int $flag
+     *
+     * @return bool
+     */
+    public function has($flag)
+    {
+        // Here we will extract the given flag into an array
+        // of possible flags. This will allow us to see if
+        // our AccountControl object contains any of them.
+        $flagsUsed = array_intersect(
+            $this->extractFlags($flag),
+            $this->values
+        );
+
+        return in_array($flag, $flagsUsed);
+    }
+
+    /**
+     * Determine if the account control does not contain the given UAC flag(s).
+     *
+     * @param int $flag
+     *
+     * @return bool
+     */
+    public function doesntHave($flag)
+    {
+        return ! $this->has($flag);
+    }
+
+    /**
+     * Generate an LDAP filter based on the current value.
+     *
+     * @return string
+     */
+    public function filter()
+    {
+        return sprintf('(UserAccountControl:1.2.840.113556.1.4.803:=%s)', $this->getValue());
+    }
+
+    /**
+     * The logon script will be run.
+     *
+     * @return $this
+     */
+    public function runLoginScript()
+    {
+        return $this->add(static::SCRIPT);
+    }
+
+    /**
+     * The user account is locked.
+     *
+     * @return $this
+     */
+    public function accountIsLocked()
+    {
+        return $this->add(static::LOCKOUT);
+    }
+
+    /**
+     * The user account is disabled.
+     *
+     * @return $this
+     */
+    public function accountIsDisabled()
+    {
+        return $this->add(static::ACCOUNTDISABLE);
+    }
+
+    /**
+     * This is an account for users whose primary account is in another domain.
+     *
+     * This account provides user access to this domain, but not to any domain that
+     * trusts this domain. This is sometimes referred to as a local user account.
+     *
+     * @return $this
+     */
+    public function accountIsTemporary()
+    {
+        return $this->add(static::TEMP_DUPLICATE_ACCOUNT);
+    }
+
+    /**
+     * This is a default account type that represents a typical user.
+     *
+     * @return $this
+     */
+    public function accountIsNormal()
+    {
+        return $this->add(static::NORMAL_ACCOUNT);
+    }
+
+    /**
+     * This is a permit to trust an account for a system domain that trusts other domains.
+     *
+     * @return $this
+     */
+    public function accountIsForInterdomain()
+    {
+        return $this->add(static::INTERDOMAIN_TRUST_ACCOUNT);
+    }
+
+    /**
+     * This is a computer account for a computer that is running Microsoft
+     * Windows NT 4.0 Workstation, Microsoft Windows NT 4.0 Server, Microsoft
+     * Windows 2000 Professional, or Windows 2000 Server and is a member of this domain.
+     *
+     * @return $this
+     */
+    public function accountIsForWorkstation()
+    {
+        return $this->add(static::WORKSTATION_TRUST_ACCOUNT);
+    }
+
+    /**
+     * This is a computer account for a domain controller that is a member of this domain.
+     *
+     * @return $this
+     */
+    public function accountIsForServer()
+    {
+        return $this->add(static::SERVER_TRUST_ACCOUNT);
+    }
+
+    /**
+     * This is an MNS logon account.
+     *
+     * @return $this
+     */
+    public function accountIsMnsLogon()
+    {
+        return $this->add(static::MNS_LOGON_ACCOUNT);
+    }
+
+    /**
+     * (Windows 2000/Windows Server 2003) This account does
+     * not require Kerberos pre-authentication for logging on.
+     *
+     * @return $this
+     */
+    public function accountDoesNotRequirePreAuth()
+    {
+        return $this->add(static::DONT_REQ_PREAUTH);
+    }
+
+    /**
+     * When this flag is set, it forces the user to log on by using a smart card.
+     *
+     * @return $this
+     */
+    public function accountRequiresSmartCard()
+    {
+        return $this->add(static::SMARTCARD_REQUIRED);
+    }
+
+    /**
+     * (Windows Server 2008/Windows Server 2008 R2) The account is a read-only domain controller (RODC).
+     *
+     * This is a security-sensitive setting. Removing this setting from an RODC compromises security on that server.
+     *
+     * @return $this
+     */
+    public function accountIsReadOnly()
+    {
+        return $this->add(static::PARTIAL_SECRETS_ACCOUNT);
+    }
+
+    /**
+     * The home folder is required.
+     *
+     * @return $this
+     */
+    public function homeFolderIsRequired()
+    {
+        return $this->add(static::HOMEDIR_REQUIRED);
+    }
+
+    /**
+     * No password is required.
+     *
+     * @return $this
+     */
+    public function passwordIsNotRequired()
+    {
+        return $this->add(static::PASSWD_NOTREQD);
+    }
+
+    /**
+     * The user cannot change the password. This is a permission on the user's object.
+     *
+     * For information about how to programmatically set this permission, visit the following link:
+     *
+     * @see http://msdn2.microsoft.com/en-us/library/aa746398.aspx
+     *
+     * @return $this
+     */
+    public function passwordCannotBeChanged()
+    {
+        return $this->add(static::PASSWD_CANT_CHANGE);
+    }
+
+    /**
+     * Represents the password, which should never expire on the account.
+     *
+     * @return $this
+     */
+    public function passwordDoesNotExpire()
+    {
+        return $this->add(static::DONT_EXPIRE_PASSWORD);
+    }
+
+    /**
+     * (Windows 2000/Windows Server 2003) The user's password has expired.
+     *
+     * @return $this
+     */
+    public function passwordIsExpired()
+    {
+        return $this->add(static::PASSWORD_EXPIRED);
+    }
+
+    /**
+     * The user can send an encrypted password.
+     *
+     * @return $this
+     */
+    public function allowEncryptedTextPassword()
+    {
+        return $this->add(static::ENCRYPTED_TEXT_PWD_ALLOWED);
+    }
+
+    /**
+     * When this flag is set, the service account (the user or computer account)
+     * under which a service runs is trusted for Kerberos delegation.
+     *
+     * Any such service can impersonate a client requesting the service.
+     *
+     * To enable a service for Kerberos delegation, you must set this
+     * flag on the userAccountControl property of the service account.
+     *
+     * @return $this
+     */
+    public function trustForDelegation()
+    {
+        return $this->add(static::TRUSTED_FOR_DELEGATION);
+    }
+
+    /**
+     * (Windows 2000/Windows Server 2003) The account is enabled for delegation.
+     *
+     * This is a security-sensitive setting. Accounts that have this option enabled
+     * should be tightly controlled. This setting lets a service that runs under the
+     * account assume a client's identity and authenticate as that user to other remote
+     * servers on the network.
+     *
+     * @return $this
+     */
+    public function trustToAuthForDelegation()
+    {
+        return $this->add(static::TRUSTED_TO_AUTH_FOR_DELEGATION);
+    }
+
+    /**
+     * When this flag is set, the security context of the user is not delegated to a
+     * service even if the service account is set as trusted for Kerberos delegation.
+     *
+     * @return $this
+     */
+    public function doNotTrustForDelegation()
+    {
+        return $this->add(static::NOT_DELEGATED);
+    }
+
+    /**
+     * (Windows 2000/Windows Server 2003) Restrict this principal to
+     * use only Data Encryption Standard (DES) encryption types for keys.
+     *
+     * @return $this
+     */
+    public function useDesKeyOnly()
+    {
+        return $this->add(static::USE_DES_KEY_ONLY);
+    }
+
+    /**
+     * Get the account control value.
+     *
+     * @return int
+     */
+    public function getValue()
+    {
+        return array_sum($this->values);
+    }
+
+    /**
+     * Get the account control flag values.
+     *
+     * @return array
+     */
+    public function getValues()
+    {
+        return $this->values;
+    }
+
+    /**
+     * Set the account control values.
+     *
+     * @param array $flags
+     *
+     * @return void
+     */
+    public function setValues(array $flags)
+    {
+        $this->values = $flags;
+    }
+
+    /**
+     * Get all flags that are currently applied to the value.
+     *
+     * @return array
+     */
+    public function getAppliedFlags()
+    {
+        $flags = $this->getAllFlags();
+
+        $exists = [];
+
+        foreach ($flags as $name => $flag) {
+            if ($this->has($flag)) {
+                $exists[$name] = $flag;
+            }
+        }
+
+        return $exists;
+    }
+
+    /**
+     * Get all possible account control flags.
+     *
+     * @return array
+     */
+    public function getAllFlags()
+    {
+        return (new ReflectionClass(__CLASS__))->getConstants();
+    }
+
+    /**
+     * Extracts the given flag into an array of flags used.
+     *
+     * @param int $flag
+     *
+     * @return array
+     */
+    public function extractFlags($flag)
+    {
+        $flags = [];
+
+        for ($i = 0; $i <= 26; $i++) {
+            if ((int) $flag & (1 << $i)) {
+                $flags[1 << $i] = 1 << $i;
+            }
+        }
+
+        return $flags;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedName.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedName.php
new file mode 100644
index 0000000..c092173
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedName.php
@@ -0,0 +1,419 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use LdapRecord\EscapesValues;
+use LdapRecord\Support\Arr;
+
+class DistinguishedName
+{
+    use EscapesValues;
+
+    /**
+     * The underlying raw value.
+     *
+     * @var string|null
+     */
+    protected $value;
+
+    /**
+     * Constructor.
+     *
+     * @param string|null $value
+     */
+    public function __construct($value = null)
+    {
+        $this->value = trim($value);
+    }
+
+    /**
+     * Get the distinguished name value.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->value;
+    }
+
+    /**
+     * Alias of the "build" method.
+     *
+     * @param string|null $value
+     *
+     * @return DistinguishedNameBuilder
+     */
+    public static function of($value = null)
+    {
+        return static::build($value);
+    }
+
+    /**
+     * Get a new DN builder object from the given DN.
+     *
+     * @param string|null $value
+     *
+     * @return DistinguishedNameBuilder
+     */
+    public static function build($value = null)
+    {
+        return new DistinguishedNameBuilder($value);
+    }
+
+    /**
+     * Make a new distinguished name instance.
+     *
+     * @param string|null $value
+     *
+     * @return static
+     */
+    public static function make($value = null)
+    {
+        return new static($value);
+    }
+
+    /**
+     * Explode a distinguished name into relative distinguished names.
+     *
+     * @param string $dn
+     *
+     * @return array
+     */
+    public static function explode($dn)
+    {
+        $dn = ldap_explode_dn($dn, $withoutAttributes = false);
+
+        if (! is_array($dn)) {
+            return [];
+        }
+
+        if (! array_key_exists('count', $dn)) {
+            return [];
+        }
+
+        unset($dn['count']);
+
+        return $dn;
+    }
+
+    /**
+     * Un-escapes a hexadecimal string into its original string representation.
+     *
+     * @param string $value
+     *
+     * @return string
+     */
+    public static function unescape($value)
+    {
+        return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
+            return chr(hexdec($matches[1]));
+        }, $value);
+    }
+
+    /**
+     * Explode the RDN into an attribute and value.
+     *
+     * @param string $rdn
+     *
+     * @return array
+     */
+    public static function explodeRdn($rdn)
+    {
+        return explode('=', $rdn, $limit = 2);
+    }
+
+    /**
+     * Implode the component attribute and value into an RDN.
+     *
+     * @param string $rdn
+     *
+     * @return string
+     */
+    public static function makeRdn(array $component)
+    {
+        return implode('=', $component);
+    }
+
+    /**
+     * Get the underlying value.
+     *
+     * @return string|null
+     */
+    public function get()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Set the underlying value.
+     *
+     * @param string|null $value
+     *
+     * @return $this
+     */
+    public function set($value)
+    {
+        $this->value = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get the distinguished name values without attributes.
+     *
+     * @return array
+     */
+    public function values()
+    {
+        $values = [];
+
+        foreach ($this->multi() as [, $value]) {
+            $values[] = static::unescape($value);
+        }
+
+        return $values;
+    }
+
+    /**
+     * Get the distinguished name attributes without values.
+     *
+     * @return array
+     */
+    public function attributes()
+    {
+        $attributes = [];
+
+        foreach ($this->multi() as [$attribute]) {
+            $attributes[] = $attribute;
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Get the distinguished name components with attributes.
+     *
+     * @return array
+     */
+    public function components()
+    {
+        $components = [];
+
+        foreach ($this->multi() as [$attribute, $value]) {
+            // When a distinguished name is exploded, the values are automatically
+            // escaped. This cannot be opted out of. Here we will unescape
+            // the attribute value, then re-escape it to its original
+            // representation from the server using the "dn" flag.
+            $value = $this->escape(static::unescape($value))->dn();
+
+            $components[] = static::makeRdn([$attribute, $value]);
+        }
+
+        return $components;
+    }
+
+    /**
+     * Convert the distinguished name into an associative array.
+     *
+     * @return array
+     */
+    public function assoc()
+    {
+        $map = [];
+
+        foreach ($this->multi() as [$attribute, $value]) {
+            $attribute = $this->normalize($attribute);
+
+            array_key_exists($attribute, $map)
+                ? $map[$attribute][] = $value
+                : $map[$attribute] = [$value];
+        }
+
+        return $map;
+    }
+
+    /**
+     * Split the RDNs into a multi-dimensional array.
+     *
+     * @return array
+     */
+    public function multi()
+    {
+        return array_map(function ($rdn) {
+            return static::explodeRdn($rdn);
+        }, $this->rdns());
+    }
+
+    /**
+     * Split the distinguished name into an array of unescaped RDN's.
+     *
+     * @return array
+     */
+    public function rdns()
+    {
+        return static::explode($this->value);
+    }
+
+    /**
+     * Get the first RDNs value.
+     *
+     * @return string|null
+     */
+    public function name()
+    {
+        return Arr::first($this->values());
+    }
+
+    /**
+     * Get the first RDNs attribute.
+     *
+     * @return string|null
+     */
+    public function head()
+    {
+        return Arr::first($this->attributes());
+    }
+
+    /**
+     * Get the relative distinguished name.
+     *
+     * @return string|null
+     */
+    public function relative()
+    {
+        return Arr::first($this->components());
+    }
+
+    /**
+     * Alias of relative().
+     *
+     * Get the first RDN from the distinguished name.
+     *
+     * @return string|null
+     */
+    public function first()
+    {
+        return $this->relative();
+    }
+
+    /**
+     * Get the parent distinguished name.
+     *
+     * @return string|null
+     */
+    public function parent()
+    {
+        $components = $this->components();
+
+        array_shift($components);
+
+        return implode(',', $components) ?: null;
+    }
+
+    /**
+     * Determine if the current distinguished name is a parent of the given child.
+     *
+     * @param DistinguishedName $child
+     *
+     * @return bool
+     */
+    public function isParentOf(self $child)
+    {
+        return $child->isChildOf($this);
+    }
+
+    /**
+     * Determine if the current distinguished name is a child of the given parent.
+     *
+     * @param DistinguishedName $parent
+     *
+     * @return bool
+     */
+    public function isChildOf(self $parent)
+    {
+        if (
+            empty($components = $this->components()) ||
+            empty($parentComponents = $parent->components())
+        ) {
+            return false;
+        }
+
+        array_shift($components);
+
+        return $this->compare($components, $parentComponents);
+    }
+
+    /**
+     * Determine if the current distinguished name is an ancestor of the descendant.
+     *
+     * @param DistinguishedName $descendant
+     *
+     * @return bool
+     */
+    public function isAncestorOf(self $descendant)
+    {
+        return $descendant->isDescendantOf($this);
+    }
+
+    /**
+     * Determine if the current distinguished name is a descendant of the ancestor.
+     *
+     * @param DistinguishedName $ancestor
+     *
+     * @return bool
+     */
+    public function isDescendantOf(self $ancestor)
+    {
+        if (
+            empty($components = $this->components()) ||
+            empty($ancestorComponents = $ancestor->components())
+        ) {
+            return false;
+        }
+
+        if (! $length = count($components) - count($ancestorComponents)) {
+            return false;
+        }
+
+        array_splice($components, $offset = 0, $length);
+
+        return $this->compare($components, $ancestorComponents);
+    }
+
+    /**
+     * Compare whether the two distinguished name values are equal.
+     *
+     * @param array $values
+     * @param array $other
+     *
+     * @return bool
+     */
+    protected function compare(array $values, array $other)
+    {
+        return $this->recase($values) == $this->recase($other);
+    }
+
+    /**
+     * Recase the array values.
+     *
+     * @param array $values
+     *
+     * @return array
+     */
+    protected function recase(array $values)
+    {
+        return array_map([$this, 'normalize'], $values);
+    }
+
+    /**
+     * Normalize the string value.
+     *
+     * @param string $value
+     *
+     * @return string
+     */
+    protected function normalize($value)
+    {
+        return strtolower($value);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedNameBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedNameBuilder.php
new file mode 100644
index 0000000..83dfe71
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedNameBuilder.php
@@ -0,0 +1,251 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use LdapRecord\EscapesValues;
+use LdapRecord\Support\Arr;
+
+class DistinguishedNameBuilder
+{
+    use EscapesValues;
+
+    /**
+     * The components of the DN.
+     *
+     * @var array
+     */
+    protected $components = [];
+
+    /**
+     * Whether to output the DN in reverse.
+     *
+     * @var bool
+     */
+    protected $reverse = false;
+
+    /**
+     * Constructor.
+     *
+     * @param string|null $value
+     */
+    public function __construct($dn = null)
+    {
+        $this->components = array_map(function ($rdn) {
+            return DistinguishedName::explodeRdn($rdn);
+        }, DistinguishedName::make($dn)->components());
+    }
+
+    /**
+     * Forward missing method calls onto the Distinguished Name object.
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        return $this->get()->{$method}(...$args);
+    }
+
+    /**
+     * Get the distinguished name value.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->get();
+    }
+
+    /**
+     * Prepend an RDN onto the DN.
+     *
+     * @param string|array $attribute
+     * @param string|null  $value
+     *
+     * @return $this
+     */
+    public function prepend($attribute, $value = null)
+    {
+        array_unshift(
+            $this->components,
+            ...$this->componentize($attribute, $value)
+        );
+
+        return $this;
+    }
+
+    /**
+     * Append an RDN onto the DN.
+     *
+     * @param string|array $attribute
+     * @param string|null  $value
+     *
+     * @return $this
+     */
+    public function append($attribute, $value = null)
+    {
+        array_push(
+            $this->components,
+            ...$this->componentize($attribute, $value)
+        );
+
+        return $this;
+    }
+
+    /**
+     * Componentize the attribute and value.
+     *
+     * @param string|array $attribute
+     * @param string|null  $value
+     *
+     * @return array
+     */
+    protected function componentize($attribute, $value = null)
+    {
+        // Here we will make the assumption that an array of
+        // RDN's have been given if the value is null, and
+        // attempt to break them into their components.
+        if (is_null($value)) {
+            $attributes = Arr::wrap($attribute);
+
+            $components = array_map([$this, 'makeComponentizedArray'], $attributes);
+        } else {
+            $components = [[$attribute, $value]];
+        }
+
+        return array_map(function ($component) {
+            [$attribute, $value] = $component;
+
+            return $this->makeAppendableComponent($attribute, $value);
+        }, $components);
+    }
+
+    /**
+     * Make a componentized array by exploding the value if it's a string.
+     *
+     * @param string $value
+     *
+     * @return array
+     */
+    protected function makeComponentizedArray($value)
+    {
+        return is_array($value) ? $value : DistinguishedName::explodeRdn($value);
+    }
+
+    /**
+     * Make an appendable component array from the attribute and value.
+     *
+     * @param string|array $attribute
+     * @param string|null  $value
+     *
+     * @return array
+     */
+    protected function makeAppendableComponent($attribute, $value = null)
+    {
+        return [trim($attribute), $this->escape(trim($value))->dn()];
+    }
+
+    /**
+     * Pop an RDN off of the end of the DN.
+     *
+     * @param int   $amount
+     * @param array $removed
+     *
+     * @return $this
+     */
+    public function pop($amount = 1, &$removed = [])
+    {
+        $removed = array_map(function ($component) {
+            return DistinguishedName::makeRdn($component);
+        }, array_splice($this->components, -$amount, $amount));
+
+        return $this;
+    }
+
+    /**
+     * Shift an RDN off of the beginning of the DN.
+     *
+     * @param int   $amount
+     * @param array $removed
+     *
+     * @return $this
+     */
+    public function shift($amount = 1, &$removed = [])
+    {
+        $removed = array_map(function ($component) {
+            return DistinguishedName::makeRdn($component);
+        }, array_splice($this->components, 0, $amount));
+
+        return $this;
+    }
+
+    /**
+     * Whether to output the DN in reverse.
+     *
+     * @return $this
+     */
+    public function reverse()
+    {
+        $this->reverse = true;
+
+        return $this;
+    }
+
+    /**
+     * Get the components of the DN.
+     *
+     * @param null|string $type
+     *
+     * @return array
+     */
+    public function components($type = null)
+    {
+        return is_null($type)
+            ? $this->components
+            : $this->componentsOfType($type);
+    }
+
+    /**
+     * Get the components of a particular type.
+     *
+     * @param string $type
+     *
+     * @return array
+     */
+    protected function componentsOfType($type)
+    {
+        $components = array_filter($this->components, function ($component) use ($type) {
+            return ([$name] = $component) && strtolower($name) === strtolower($type);
+        });
+
+        return array_values($components);
+    }
+
+    /**
+     * Get the fully qualified DN.
+     *
+     * @return DistinguishedName
+     */
+    public function get()
+    {
+        return new DistinguishedName($this->build());
+    }
+
+    /**
+     * Build the distinguished name from the components.
+     *
+     * @return $this
+     */
+    protected function build()
+    {
+        $components = $this->reverse
+            ? array_reverse($this->components)
+            : $this->components;
+
+        return implode(',', array_map(function ($component) {
+            return DistinguishedName::makeRdn($component);
+        }, $components));
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/EscapedValue.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/EscapedValue.php
new file mode 100644
index 0000000..cc04a67
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/EscapedValue.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+class EscapedValue
+{
+    /**
+     * The value to be escaped.
+     *
+     * @var string
+     */
+    protected $value;
+
+    /**
+     * The characters to ignore when escaping.
+     *
+     * @var string
+     */
+    protected $ignore;
+
+    /**
+     * The escape flags.
+     *
+     * @var int
+     */
+    protected $flags;
+
+    /**
+     * Constructor.
+     *
+     * @param string $value
+     * @param string $ignore
+     * @param int    $flags
+     */
+    public function __construct($value, $ignore = '', $flags = 0)
+    {
+        $this->value = $value;
+        $this->ignore = $ignore;
+        $this->flags = $flags;
+    }
+
+    /**
+     * Get the escaped value.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->get();
+    }
+
+    /**
+     * Get the escaped value.
+     *
+     * @return mixed
+     */
+    public function get()
+    {
+        return ldap_escape($this->value, $this->ignore, $this->flags);
+    }
+
+    /**
+     * Set the characters to exclude from being escaped.
+     *
+     * @param string $characters
+     *
+     * @return $this
+     */
+    public function ignore($characters)
+    {
+        $this->ignore = $characters;
+
+        return $this;
+    }
+
+    /**
+     * Prepare the value to be escaped for use in a distinguished name.
+     *
+     * @return $this
+     */
+    public function dn()
+    {
+        $this->flags = LDAP_ESCAPE_DN;
+
+        return $this;
+    }
+
+    /**
+     * Prepare the value to be escaped for use in a filter.
+     *
+     * @return $this
+     */
+    public function filter()
+    {
+        $this->flags = LDAP_ESCAPE_FILTER;
+
+        return $this;
+    }
+
+    /**
+     * Prepare the value to be escaped for use in a distinguished name and filter.
+     *
+     * @return $this
+     */
+    public function both()
+    {
+        $this->flags = LDAP_ESCAPE_FILTER + LDAP_ESCAPE_DN;
+
+        return $this;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Guid.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Guid.php
new file mode 100644
index 0000000..d139f5f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Guid.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+use LdapRecord\Utilities;
+
+class Guid
+{
+    /**
+     * The string GUID value.
+     *
+     * @var string
+     */
+    protected $value;
+
+    /**
+     * The guid structure in order by section to parse using substr().
+     *
+     * @author Chad Sikorra <Chad.Sikorra@gmail.com>
+     *
+     * @see https://github.com/ldaptools/ldaptools
+     *
+     * @var array
+     */
+    protected $guidSections = [
+        [[-26, 2], [-28, 2], [-30, 2], [-32, 2]],
+        [[-22, 2], [-24, 2]],
+        [[-18, 2], [-20, 2]],
+        [[-16, 4]],
+        [[-12, 12]],
+    ];
+
+    /**
+     * The hexadecimal octet order based on string position.
+     *
+     * @author Chad Sikorra <Chad.Sikorra@gmail.com>
+     *
+     * @see https://github.com/ldaptools/ldaptools
+     *
+     * @var array
+     */
+    protected $octetSections = [
+        [6, 4, 2, 0],
+        [10, 8],
+        [14, 12],
+        [16, 18, 20, 22, 24, 26, 28, 30],
+    ];
+
+    /**
+     * Determines if the specified GUID is valid.
+     *
+     * @param string $guid
+     *
+     * @return bool
+     */
+    public static function isValid($guid)
+    {
+        return Utilities::isValidGuid($guid);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param mixed $value
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct($value)
+    {
+        if (static::isValid($value)) {
+            $this->value = $value;
+        } elseif ($value = $this->binaryGuidToString($value)) {
+            $this->value = $value;
+        } else {
+            throw new InvalidArgumentException('Invalid Binary / String GUID.');
+        }
+    }
+
+    /**
+     * Returns the string value of the GUID.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->getValue();
+    }
+
+    /**
+     * Returns the string value of the SID.
+     *
+     * @return string
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Get the binary representation of the GUID string.
+     *
+     * @return string
+     */
+    public function getBinary()
+    {
+        return hex2bin($this->getHex());
+    }
+
+    /**
+     * Get the hexadecimal representation of the GUID string.
+     *
+     * @return string
+     */
+    public function getHex()
+    {
+        $data = '';
+
+        $guid = str_replace('-', '', $this->value);
+
+        foreach ($this->octetSections as $section) {
+            $data .= $this->parseSection($guid, $section, $octet = true);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Returns the string variant of a binary GUID.
+     *
+     * @param string $binary
+     *
+     * @return string|null
+     */
+    protected function binaryGuidToString($binary)
+    {
+        return Utilities::binaryGuidToString($binary);
+    }
+
+    /**
+     * Return the specified section of the hexadecimal string.
+     *
+     * @author Chad Sikorra <Chad.Sikorra@gmail.com>
+     *
+     * @see https://github.com/ldaptools/ldaptools
+     *
+     * @param string $hex      The full hex string.
+     * @param array  $sections An array of start and length (unless octet is true, then length is always 2).
+     * @param bool   $octet    Whether this is for octet string form.
+     *
+     * @return string The concatenated sections in upper-case.
+     */
+    protected function parseSection($hex, array $sections, $octet = false)
+    {
+        $parsedString = '';
+
+        foreach ($sections as $section) {
+            $start = $octet ? $section : $section[0];
+
+            $length = $octet ? 2 : $section[1];
+
+            $parsedString .= substr($hex, $start, $length);
+        }
+
+        return $parsedString;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/MbString.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/MbString.php
new file mode 100644
index 0000000..672e60d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/MbString.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+class MbString
+{
+    /**
+     * Get the integer value of a specific character.
+     *
+     * @param $string
+     *
+     * @return int
+     */
+    public static function ord($string)
+    {
+        if (static::isLoaded()) {
+            $result = unpack('N', mb_convert_encoding($string, 'UCS-4BE', 'UTF-8'));
+
+            if (is_array($result)) {
+                return $result[1];
+            }
+        }
+
+        return ord($string);
+    }
+
+    /**
+     * Get the character for a specific integer value.
+     *
+     * @param $int
+     *
+     * @return string
+     */
+    public static function chr($int)
+    {
+        if (static::isLoaded()) {
+            return mb_convert_encoding(pack('n', $int), 'UTF-8', 'UTF-16BE');
+        }
+
+        return chr($int);
+    }
+
+    /**
+     * Split a string into its individual characters and return it as an array.
+     *
+     * @param string $value
+     *
+     * @return string[]
+     */
+    public static function split($value)
+    {
+        return preg_split('/(?<!^)(?!$)/u', $value);
+    }
+
+    /**
+     * Detects if the given string is UTF 8.
+     *
+     * @param $string
+     *
+     * @return string|false
+     */
+    public static function isUtf8($string)
+    {
+        if (static::isLoaded()) {
+            return mb_detect_encoding($string, 'UTF-8', $strict = true);
+        }
+
+        return $string;
+    }
+
+    /**
+     * Checks if the mbstring extension is enabled in PHP.
+     *
+     * @return bool
+     */
+    public static function isLoaded()
+    {
+        return extension_loaded('mbstring');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Password.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Password.php
new file mode 100644
index 0000000..7f0b412
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Password.php
@@ -0,0 +1,340 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+use LdapRecord\LdapRecordException;
+use ReflectionMethod;
+
+class Password
+{
+    const CRYPT_SALT_TYPE_MD5 = 1;
+    const CRYPT_SALT_TYPE_SHA256 = 5;
+    const CRYPT_SALT_TYPE_SHA512 = 6;
+
+    /**
+     * Make an encoded password for transmission over LDAP.
+     *
+     * @param string $password
+     *
+     * @return string
+     */
+    public static function encode($password)
+    {
+        return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"');
+    }
+
+    /**
+     * Make a salted md5 password.
+     *
+     * @param string      $password
+     * @param null|string $salt
+     *
+     * @return string
+     */
+    public static function smd5($password, $salt = null)
+    {
+        return '{SMD5}'.static::makeHash($password, 'md5', null, $salt ?? random_bytes(4));
+    }
+
+    /**
+     * Make a salted SHA password.
+     *
+     * @param string      $password
+     * @param null|string $salt
+     *
+     * @return string
+     */
+    public static function ssha($password, $salt = null)
+    {
+        return '{SSHA}'.static::makeHash($password, 'sha1', null, $salt ?? random_bytes(4));
+    }
+
+    /**
+     * Make a salted SSHA256 password.
+     *
+     * @param string      $password
+     * @param null|string $salt
+     *
+     * @return string
+     */
+    public static function ssha256($password, $salt = null)
+    {
+        return '{SSHA256}'.static::makeHash($password, 'hash', 'sha256', $salt ?? random_bytes(4));
+    }
+
+    /**
+     * Make a salted SSHA384 password.
+     *
+     * @param string      $password
+     * @param null|string $salt
+     *
+     * @return string
+     */
+    public static function ssha384($password, $salt = null)
+    {
+        return '{SSHA384}'.static::makeHash($password, 'hash', 'sha384', $salt ?? random_bytes(4));
+    }
+
+    /**
+     * Make a salted SSHA512 password.
+     *
+     * @param string      $password
+     * @param null|string $salt
+     *
+     * @return string
+     */
+    public static function ssha512($password, $salt = null)
+    {
+        return '{SSHA512}'.static::makeHash($password, 'hash', 'sha512', $salt ?? random_bytes(4));
+    }
+
+    /**
+     * Make a non-salted SHA password.
+     *
+     * @param string $password
+     *
+     * @return string
+     */
+    public static function sha($password)
+    {
+        return '{SHA}'.static::makeHash($password, 'sha1');
+    }
+
+    /**
+     * Make a non-salted SHA256 password.
+     *
+     * @param string $password
+     *
+     * @return string
+     */
+    public static function sha256($password)
+    {
+        return '{SHA256}'.static::makeHash($password, 'hash', 'sha256');
+    }
+
+    /**
+     * Make a non-salted SHA384 password.
+     *
+     * @param string $password
+     *
+     * @return string
+     */
+    public static function sha384($password)
+    {
+        return '{SHA384}'.static::makeHash($password, 'hash', 'sha384');
+    }
+
+    /**
+     * Make a non-salted SHA512 password.
+     *
+     * @param string $password
+     *
+     * @return string
+     */
+    public static function sha512($password)
+    {
+        return '{SHA512}'.static::makeHash($password, 'hash', 'sha512');
+    }
+
+    /**
+     * Make a non-salted md5 password.
+     *
+     * @param string $password
+     *
+     * @return string
+     */
+    public static function md5($password)
+    {
+        return '{MD5}'.static::makeHash($password, 'md5');
+    }
+
+    /**
+     * Crypt password with an MD5 salt.
+     *
+     * @param string $password
+     * @param string $salt
+     *
+     * @return string
+     */
+    public static function md5Crypt($password, $salt = null)
+    {
+        return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_MD5, $salt);
+    }
+
+    /**
+     * Crypt password with a SHA256 salt.
+     *
+     * @param string $password
+     * @param string $salt
+     *
+     * @return string
+     */
+    public static function sha256Crypt($password, $salt = null)
+    {
+        return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA256, $salt);
+    }
+
+    /**
+     * Crypt a password with a SHA512 salt.
+     *
+     * @param string $password
+     * @param string $salt
+     *
+     * @return string
+     */
+    public static function sha512Crypt($password, $salt = null)
+    {
+        return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA512, $salt);
+    }
+
+    /**
+     * Make a new password hash.
+     *
+     * @param string      $password The password to make a hash of.
+     * @param string      $method   The hash function to use.
+     * @param string|null $algo     The algorithm to use for hashing.
+     * @param string|null $salt     The salt to append onto the hash.
+     *
+     * @return string
+     */
+    protected static function makeHash($password, $method, $algo = null, $salt = null)
+    {
+        $params = $algo ? [$algo, $password.$salt] : [$password.$salt];
+
+        return base64_encode(pack('H*', call_user_func($method, ...$params)).$salt);
+    }
+
+    /**
+     * Make a hashed password.
+     *
+     * @param string      $password
+     * @param int         $type
+     * @param null|string $salt
+     *
+     * @return string
+     */
+    protected static function makeCrypt($password, $type, $salt = null)
+    {
+        return crypt($password, $salt ?? static::makeCryptSalt($type));
+    }
+
+    /**
+     * Make a salt for the crypt() method using the given type.
+     *
+     * @param int $type
+     *
+     * @return string
+     */
+    protected static function makeCryptSalt($type)
+    {
+        [$prefix, $length] = static::makeCryptPrefixAndLength($type);
+
+        $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+        while (strlen($prefix) < $length) {
+            $prefix .= substr($chars, random_int(0, strlen($chars) - 1), 1);
+        }
+
+        return $prefix;
+    }
+
+    /**
+     * Determine the crypt prefix and length.
+     *
+     * @param int $type
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return array
+     */
+    protected static function makeCryptPrefixAndLength($type)
+    {
+        switch ($type) {
+            case static::CRYPT_SALT_TYPE_MD5:
+                return ['$1$', 12];
+            case static::CRYPT_SALT_TYPE_SHA256:
+                return ['$5$', 16];
+            case static::CRYPT_SALT_TYPE_SHA512:
+                return ['$6$', 16];
+            default:
+                throw new InvalidArgumentException("Invalid crypt type [$type].");
+        }
+    }
+
+    /**
+     * Attempt to retrieve the hash method used for the password.
+     *
+     * @param string $password
+     *
+     * @return string|void
+     */
+    public static function getHashMethod($password)
+    {
+        if (! preg_match('/^\{(\w+)\}/', $password, $matches)) {
+            return;
+        }
+
+        return $matches[1];
+    }
+
+    /**
+     * Attempt to retrieve the hash method and algorithm used for the password.
+     *
+     * @param string $password
+     *
+     * @return array|void
+     */
+    public static function getHashMethodAndAlgo($password)
+    {
+        if (! preg_match('/^\{(\w+)\}\$([0-9a-z]{1})\$/', $password, $matches)) {
+            return;
+        }
+
+        return [$matches[1], $matches[2]];
+    }
+
+    /**
+     * Attempt to retrieve a salt from the encrypted password.
+     *
+     * @throws LdapRecordException
+     *
+     * @return string
+     */
+    public static function getSalt($encryptedPassword)
+    {
+        // crypt() methods.
+        if (preg_match('/^\{(\w+)\}(\$.*\$).*$/', $encryptedPassword, $matches)) {
+            return $matches[2];
+        }
+
+        // All other methods.
+        if (preg_match('/{([^}]+)}(.*)/', $encryptedPassword, $matches)) {
+            return substr(base64_decode($matches[2]), -4);
+        }
+
+        throw new LdapRecordException('Could not extract salt from encrypted password.');
+    }
+
+    /**
+     * Determine if the hash method requires a salt to be given.
+     *
+     * @param string $method
+     *
+     * @throws \ReflectionException
+     *
+     * @return bool
+     */
+    public static function hashMethodRequiresSalt($method): bool
+    {
+        $parameters = (new ReflectionMethod(static::class, $method))->getParameters();
+
+        foreach ($parameters as $parameter) {
+            if ($parameter->name === 'salt') {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Sid.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Sid.php
new file mode 100644
index 0000000..4ec46ea
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Sid.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+use LdapRecord\Utilities;
+
+class Sid
+{
+    /**
+     * The string SID value.
+     *
+     * @var string
+     */
+    protected $value;
+
+    /**
+     * Determines if the specified SID is valid.
+     *
+     * @param string $sid
+     *
+     * @return bool
+     */
+    public static function isValid($sid)
+    {
+        return Utilities::isValidSid($sid);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param mixed $value
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct($value)
+    {
+        if (static::isValid($value)) {
+            $this->value = $value;
+        } elseif ($value = $this->binarySidToString($value)) {
+            $this->value = $value;
+        } else {
+            throw new InvalidArgumentException('Invalid Binary / String SID.');
+        }
+    }
+
+    /**
+     * Returns the string value of the SID.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->getValue();
+    }
+
+    /**
+     * Returns the string value of the SID.
+     *
+     * @return string
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Returns the binary variant of the SID.
+     *
+     * @return string
+     */
+    public function getBinary()
+    {
+        $sid = explode('-', ltrim($this->value, 'S-'));
+
+        $level = (int) array_shift($sid);
+
+        $authority = (int) array_shift($sid);
+
+        $subAuthorities = array_map('intval', $sid);
+
+        $params = array_merge(
+            ['C2xxNV*', $level, count($subAuthorities), $authority],
+            $subAuthorities
+        );
+
+        return call_user_func_array('pack', $params);
+    }
+
+    /**
+     * Returns the string variant of a binary SID.
+     *
+     * @param string $binary
+     *
+     * @return string|null
+     */
+    protected function binarySidToString($binary)
+    {
+        return Utilities::binarySidToString($binary);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSProperty.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSProperty.php
new file mode 100644
index 0000000..ad56aa1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSProperty.php
@@ -0,0 +1,396 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+class TSProperty
+{
+    /**
+     * Nibble control values. The first value for each is if the nibble is <= 9, otherwise the second value is used.
+     */
+    const NIBBLE_CONTROL = [
+        'X' => ['001011', '011010'],
+        'Y' => ['001110', '011010'],
+    ];
+
+    /**
+     * The nibble header.
+     */
+    const NIBBLE_HEADER = '1110';
+
+    /**
+     * Conversion factor needed for time values in the TSPropertyArray (stored in microseconds).
+     */
+    const TIME_CONVERSION = 60 * 1000;
+
+    /**
+     * A simple map to help determine how the property needs to be decoded/encoded from/to its binary value.
+     *
+     * There are some names that are simple repeats but have 'W' at the end. Not sure as to what that signifies. I
+     * cannot find any information on them in Microsoft documentation. However, their values appear to stay in sync with
+     * their non 'W' counterparts. But not doing so when manipulating the data manually does not seem to affect anything.
+     * This probably needs more investigation.
+     *
+     * @var array
+     */
+    protected $propTypes = [
+        'string' => [
+            'CtxWFHomeDir',
+            'CtxWFHomeDirW',
+            'CtxWFHomeDirDrive',
+            'CtxWFHomeDirDriveW',
+            'CtxInitialProgram',
+            'CtxInitialProgramW',
+            'CtxWFProfilePath',
+            'CtxWFProfilePathW',
+            'CtxWorkDirectory',
+            'CtxWorkDirectoryW',
+            'CtxCallbackNumber',
+        ],
+        'time' => [
+            'CtxMaxDisconnectionTime',
+            'CtxMaxConnectionTime',
+            'CtxMaxIdleTime',
+        ],
+        'int' => [
+            'CtxCfgFlags1',
+            'CtxCfgPresent',
+            'CtxKeyboardLayout',
+            'CtxMinEncryptionLevel',
+            'CtxNWLogonServer',
+            'CtxShadow',
+        ],
+    ];
+
+    /**
+     * The property name.
+     *
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * The property value.
+     *
+     * @var string|int
+     */
+    protected $value;
+
+    /**
+     * The property value type.
+     *
+     * @var int
+     */
+    protected $valueType = 1;
+
+    /**
+     * Pass binary TSProperty data to construct its object representation.
+     *
+     * @param string|null $value
+     */
+    public function __construct($value = null)
+    {
+        if ($value) {
+            $this->decode(bin2hex($value));
+        }
+    }
+
+    /**
+     * Set the name for the TSProperty.
+     *
+     * @param string $name
+     *
+     * @return TSProperty
+     */
+    public function setName($name)
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    /**
+     * Get the name for the TSProperty.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Set the value for the TSProperty.
+     *
+     * @param string|int $value
+     *
+     * @return TSProperty
+     */
+    public function setValue($value)
+    {
+        $this->value = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get the value for the TSProperty.
+     *
+     * @return string|int
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Convert the TSProperty name/value back to its binary
+     * representation for the userParameters blob.
+     *
+     * @return string
+     */
+    public function toBinary()
+    {
+        $name = bin2hex($this->name);
+
+        $binValue = $this->getEncodedValueForProp($this->name, $this->value);
+
+        $valueLen = strlen(bin2hex($binValue)) / 3;
+
+        $binary = hex2bin(
+            $this->dec2hex(strlen($name))
+            .$this->dec2hex($valueLen)
+            .$this->dec2hex($this->valueType)
+            .$name
+        );
+
+        return $binary.$binValue;
+    }
+
+    /**
+     * Given a TSProperty blob, decode the name/value/type/etc.
+     *
+     * @param string $tsProperty
+     */
+    protected function decode($tsProperty)
+    {
+        $nameLength = hexdec(substr($tsProperty, 0, 2));
+
+        // 1 data byte is 3 encoded bytes
+        $valueLength = hexdec(substr($tsProperty, 2, 2)) * 3;
+
+        $this->valueType = hexdec(substr($tsProperty, 4, 2));
+        $this->name = pack('H*', substr($tsProperty, 6, $nameLength));
+        $this->value = $this->getDecodedValueForProp($this->name, substr($tsProperty, 6 + $nameLength, $valueLength));
+    }
+
+    /**
+     * Based on the property name/value in question, get its encoded form.
+     *
+     * @param string     $propName
+     * @param string|int $propValue
+     *
+     * @return string
+     */
+    protected function getEncodedValueForProp($propName, $propValue)
+    {
+        if (in_array($propName, $this->propTypes['string'])) {
+            // Simple strings are null terminated. Unsure if this is
+            // needed or simply a product of how ADUC does stuff?
+            $value = $this->encodePropValue($propValue."\0", true);
+        } elseif (in_array($propName, $this->propTypes['time'])) {
+            // Needs to be in microseconds (assuming it is in minute format)...
+            $value = $this->encodePropValue($propValue * self::TIME_CONVERSION);
+        } else {
+            $value = $this->encodePropValue($propValue);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Based on the property name in question, get its actual value from the binary blob value.
+     *
+     * @param string $propName
+     * @param string $propValue
+     *
+     * @return string|int
+     */
+    protected function getDecodedValueForProp($propName, $propValue)
+    {
+        if (in_array($propName, $this->propTypes['string'])) {
+            // Strip away null terminators. I think this should
+            // be desired, otherwise it just ends in confusion.
+            $value = str_replace("\0", '', $this->decodePropValue($propValue, true));
+        } elseif (in_array($propName, $this->propTypes['time'])) {
+            // Convert from microseconds to minutes (how ADUC displays
+            // it anyway, and seems the most practical).
+            $value = hexdec($this->decodePropValue($propValue)) / self::TIME_CONVERSION;
+        } elseif (in_array($propName, $this->propTypes['int'])) {
+            $value = hexdec($this->decodePropValue($propValue));
+        } else {
+            $value = $this->decodePropValue($propValue);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Decode the property by inspecting the nibbles of each blob, checking
+     * the control, and adding up the results into a final value.
+     *
+     * @param string $hex
+     * @param bool   $string Whether or not this is simple string data.
+     *
+     * @return string
+     */
+    protected function decodePropValue($hex, $string = false)
+    {
+        $decodePropValue = '';
+
+        $blobs = str_split($hex, 6);
+
+        foreach ($blobs as $blob) {
+            $bin = decbin(hexdec($blob));
+
+            $controlY = substr($bin, 4, 6);
+            $nibbleY = substr($bin, 10, 4);
+            $controlX = substr($bin, 14, 6);
+            $nibbleX = substr($bin, 20, 4);
+
+            $byte = $this->nibbleControl($nibbleX, $controlX).$this->nibbleControl($nibbleY, $controlY);
+
+            if ($string) {
+                $decodePropValue .= MbString::chr(bindec($byte));
+            } else {
+                $decodePropValue = $this->dec2hex(bindec($byte)).$decodePropValue;
+            }
+        }
+
+        return $decodePropValue;
+    }
+
+    /**
+     * Get the encoded property value as a binary blob.
+     *
+     * @param string $value
+     * @param bool   $string
+     *
+     * @return string
+     */
+    protected function encodePropValue($value, $string = false)
+    {
+        // An int must be properly padded. (then split and reversed).
+        // For a string, we just split the chars. This seems
+        // to be the easiest way to handle UTF-8 characters
+        // instead of trying to work with their hex values.
+        $chars = $string ? MbString::split($value) : array_reverse(str_split($this->dec2hex($value, 8), 2));
+
+        $encoded = '';
+
+        foreach ($chars as $char) {
+            // Get the bits for the char. Using this method to ensure it is fully padded.
+            $bits = sprintf('%08b', $string ? MbString::ord($char) : hexdec($char));
+            $nibbleX = substr($bits, 0, 4);
+            $nibbleY = substr($bits, 4, 4);
+
+            // Construct the value with the header, high nibble, then low nibble.
+            $value = self::NIBBLE_HEADER;
+
+            foreach (['Y' => $nibbleY, 'X' => $nibbleX] as $nibbleType => $nibble) {
+                $value .= $this->getNibbleWithControl($nibbleType, $nibble);
+            }
+
+            // Convert it back to a binary bit stream
+            foreach ([0, 8, 16] as $start) {
+                $encoded .= $this->packBitString(substr($value, $start, 8), 8);
+            }
+        }
+
+        return $encoded;
+    }
+
+    /**
+     * PHP's pack() function has no 'b' or 'B' template. This is
+     * a workaround that turns a literal bit-string into a
+     * packed byte-string with 8 bits per byte.
+     *
+     * @param string $bits
+     * @param bool   $len
+     *
+     * @return string
+     */
+    protected function packBitString($bits, $len)
+    {
+        $bits = substr($bits, 0, $len);
+        // Pad input with zeros to next multiple of 4 above $len
+        $bits = str_pad($bits, 4 * (int) (($len + 3) / 4), '0');
+
+        // Split input into chunks of 4 bits, convert each to hex and pack them
+        $nibbles = str_split($bits, 4);
+        foreach ($nibbles as $i => $nibble) {
+            $nibbles[$i] = base_convert($nibble, 2, 16);
+        }
+
+        return pack('H*', implode('', $nibbles));
+    }
+
+    /**
+     * Based on the control, adjust the nibble accordingly.
+     *
+     * @param string $nibble
+     * @param string $control
+     *
+     * @return string
+     */
+    protected function nibbleControl($nibble, $control)
+    {
+        // This control stays constant for the low/high nibbles,
+        // so it doesn't matter which we compare to
+        if ($control == self::NIBBLE_CONTROL['X'][1]) {
+            $dec = bindec($nibble);
+            $dec += 9;
+            $nibble = str_pad(decbin($dec), 4, '0', STR_PAD_LEFT);
+        }
+
+        return $nibble;
+    }
+
+    /**
+     * Get the nibble value with the control prefixed.
+     *
+     * If the nibble dec is <= 9, the control X equals 001011 and Y equals 001110, otherwise if the nibble dec is > 9
+     * the control for X or Y equals 011010. Additionally, if the dec value of the nibble is > 9, then the nibble value
+     * must be subtracted by 9 before the final value is constructed.
+     *
+     * @param string $nibbleType Either X or Y
+     * @param string $nibble
+     *
+     * @return string
+     */
+    protected function getNibbleWithControl($nibbleType, $nibble)
+    {
+        $dec = bindec($nibble);
+
+        if ($dec > 9) {
+            $dec -= 9;
+            $control = self::NIBBLE_CONTROL[$nibbleType][1];
+        } else {
+            $control = self::NIBBLE_CONTROL[$nibbleType][0];
+        }
+
+        return $control.sprintf('%04d', decbin($dec));
+    }
+
+    /**
+     * Need to make sure hex values are always an even length, so pad as needed.
+     *
+     * @param int $int
+     * @param int $padLength The hex string must be padded to this length (with zeros).
+     *
+     * @return string
+     */
+    protected function dec2hex($int, $padLength = 2)
+    {
+        return str_pad(dechex($int), $padLength, 0, STR_PAD_LEFT);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSPropertyArray.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSPropertyArray.php
new file mode 100644
index 0000000..1831688
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSPropertyArray.php
@@ -0,0 +1,295 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+
+class TSPropertyArray
+{
+    /**
+     * Represents that the TSPropertyArray data is valid.
+     */
+    const VALID_SIGNATURE = 'P';
+
+    /**
+     * The default values for the TSPropertyArray structure.
+     *
+     * @var array
+     */
+    const DEFAULTS = [
+        'CtxCfgPresent' => 2953518677,
+        'CtxWFProfilePath' => '',
+        'CtxWFProfilePathW' => '',
+        'CtxWFHomeDir' => '',
+        'CtxWFHomeDirW' => '',
+        'CtxWFHomeDirDrive' => '',
+        'CtxWFHomeDirDriveW' => '',
+        'CtxShadow' => 1,
+        'CtxMaxDisconnectionTime' => 0,
+        'CtxMaxConnectionTime' => 0,
+        'CtxMaxIdleTime' => 0,
+        'CtxWorkDirectory' => '',
+        'CtxWorkDirectoryW' => '',
+        'CtxCfgFlags1' => 2418077696,
+        'CtxInitialProgram' => '',
+        'CtxInitialProgramW' => '',
+    ];
+
+    /**
+     * @var string The default data that occurs before the TSPropertyArray (CtxCfgPresent with a bunch of spaces...?)
+     */
+    protected $defaultPreBinary = '43747843666750726573656e742020202020202020202020202020202020202020202020202020202020202020202020';
+
+    /**
+     * @var TSProperty[]
+     */
+    protected $tsProperty = [];
+
+    /**
+     * @var string
+     */
+    protected $signature = self::VALID_SIGNATURE;
+
+    /**
+     * Binary data that occurs before the TSPropertyArray data in userParameters.
+     *
+     * @var string
+     */
+    protected $preBinary = '';
+
+    /**
+     * Binary data that occurs after the TSPropertyArray data in userParameters.
+     *
+     * @var string
+     */
+    protected $postBinary = '';
+
+    /**
+     * Construct in one of the following ways:.
+     *
+     *   - Pass an array of TSProperty key => value pairs (See DEFAULTS constant).
+     *   - Pass the userParameters binary value. The object representation of that will be decoded and constructed.
+     *   - Pass nothing and a default set of TSProperty key => value pairs will be used (See DEFAULTS constant).
+     *
+     * @param mixed $tsPropertyArray
+     */
+    public function __construct($tsPropertyArray = null)
+    {
+        $this->preBinary = hex2bin($this->defaultPreBinary);
+
+        if (is_null($tsPropertyArray) || is_array($tsPropertyArray)) {
+            $tsPropertyArray = $tsPropertyArray ?: self::DEFAULTS;
+
+            foreach ($tsPropertyArray as $key => $value) {
+                $tsProperty = new TSProperty();
+
+                $this->tsProperty[$key] = $tsProperty->setName($key)->setValue($value);
+            }
+        } else {
+            $this->decodeUserParameters($tsPropertyArray);
+        }
+    }
+
+    /**
+     * Check if a specific TSProperty exists by its property name.
+     *
+     * @param string $propName
+     *
+     * @return bool
+     */
+    public function has($propName)
+    {
+        return array_key_exists(strtolower($propName), array_change_key_case($this->tsProperty));
+    }
+
+    /**
+     * Get a TSProperty object by its property name (ie. CtxWFProfilePath).
+     *
+     * @param string $propName
+     *
+     * @return TSProperty
+     */
+    public function get($propName)
+    {
+        $this->validateProp($propName);
+
+        return $this->getTsPropObj($propName);
+    }
+
+    /**
+     * Add a TSProperty object. If it already exists, it will be overwritten.
+     *
+     * @param TSProperty $tsProperty
+     *
+     * @return $this
+     */
+    public function add(TSProperty $tsProperty)
+    {
+        $this->tsProperty[$tsProperty->getName()] = $tsProperty;
+
+        return $this;
+    }
+
+    /**
+     * Remove a TSProperty by its property name (ie. CtxMinEncryptionLevel).
+     *
+     * @param string $propName
+     *
+     * @return $this
+     */
+    public function remove($propName)
+    {
+        foreach (array_keys($this->tsProperty) as $property) {
+            if (strtolower($propName) == strtolower($property)) {
+                unset($this->tsProperty[$property]);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set the value for a specific TSProperty by its name.
+     *
+     * @param string $propName
+     * @param mixed  $propValue
+     *
+     * @return $this
+     */
+    public function set($propName, $propValue)
+    {
+        $this->validateProp($propName);
+
+        $this->getTsPropObj($propName)->setValue($propValue);
+
+        return $this;
+    }
+
+    /**
+     * Get the full binary representation of the userParameters containing the TSPropertyArray data.
+     *
+     * @return string
+     */
+    public function toBinary()
+    {
+        $binary = $this->preBinary;
+
+        $binary .= hex2bin(str_pad(dechex(MbString::ord($this->signature)), 2, 0, STR_PAD_LEFT));
+
+        $binary .= hex2bin(str_pad(dechex(count($this->tsProperty)), 2, 0, STR_PAD_LEFT));
+
+        foreach ($this->tsProperty as $tsProperty) {
+            $binary .= $tsProperty->toBinary();
+        }
+
+        return $binary.$this->postBinary;
+    }
+
+    /**
+     * Get a simple associative array containing of all TSProperty names and values.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $userParameters = [];
+
+        foreach ($this->tsProperty as $property => $tsPropObj) {
+            $userParameters[$property] = $tsPropObj->getValue();
+        }
+
+        return $userParameters;
+    }
+
+    /**
+     * Get all TSProperty objects.
+     *
+     * @return TSProperty[]
+     */
+    public function getTSProperties()
+    {
+        return $this->tsProperty;
+    }
+
+    /**
+     * Validates that the given property name exists.
+     *
+     * @param string $propName
+     */
+    protected function validateProp($propName)
+    {
+        if (! $this->has($propName)) {
+            throw new InvalidArgumentException(sprintf('TSProperty for "%s" does not exist.', $propName));
+        }
+    }
+
+    /**
+     * @param string $propName
+     *
+     * @return TSProperty
+     */
+    protected function getTsPropObj($propName)
+    {
+        return array_change_key_case($this->tsProperty)[strtolower($propName)];
+    }
+
+    /**
+     * Get an associative array with all of the userParameters property names and values.
+     *
+     * @param string $userParameters
+     *
+     * @return void
+     */
+    protected function decodeUserParameters($userParameters)
+    {
+        $userParameters = bin2hex($userParameters);
+
+        // Save the 96-byte array of reserved data, so as to not ruin anything that may be stored there.
+        $this->preBinary = hex2bin(substr($userParameters, 0, 96));
+        // The signature is a 2-byte unicode character at the front
+        $this->signature = MbString::chr(hexdec(substr($userParameters, 96, 2)));
+        // This asserts the validity of the tsPropertyArray data. For some reason 'P' means valid...
+        if ($this->signature != self::VALID_SIGNATURE) {
+            throw new InvalidArgumentException('Invalid TSPropertyArray data');
+        }
+
+        // The property count is a 2-byte unsigned integer indicating the number of elements for the tsPropertyArray
+        // It starts at position 98. The actual variable data begins at position 100.
+        $length = $this->addTSPropData(substr($userParameters, 100), hexdec(substr($userParameters, 98, 2)));
+
+        // Reserved data length + (count and sig length == 4) + the added lengths of the TSPropertyArray
+        // This saves anything after that variable TSPropertyArray data, so as to not squash anything stored there
+        if (strlen($userParameters) > (96 + 4 + $length)) {
+            $this->postBinary = hex2bin(substr($userParameters, (96 + 4 + $length)));
+        }
+    }
+
+    /**
+     * Given the start of TSPropertyArray hex data, and the count for the number
+     * of TSProperty structures in contains, parse and split out the
+     * individual TSProperty structures. Return the full length
+     * of the TSPropertyArray data.
+     *
+     * @param string $tsPropertyArray
+     * @param int    $tsPropCount
+     *
+     * @return int The length of the data in the TSPropertyArray
+     */
+    protected function addTSPropData($tsPropertyArray, $tsPropCount)
+    {
+        $length = 0;
+
+        for ($i = 0; $i < $tsPropCount; $i++) {
+            // Prop length = name length + value length + type length + the space for the length data.
+            $propLength = hexdec(substr($tsPropertyArray, $length, 2)) + (hexdec(substr($tsPropertyArray, $length + 2, 2)) * 3) + 6;
+
+            $tsProperty = new TSProperty(hex2bin(substr($tsPropertyArray, $length, $propLength)));
+
+            $this->tsProperty[$tsProperty->getName()] = $tsProperty;
+
+            $length += $propLength;
+        }
+
+        return $length;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Timestamp.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Timestamp.php
new file mode 100644
index 0000000..abd656c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Timestamp.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use Carbon\Carbon;
+use Carbon\CarbonInterface;
+use DateTime;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Utilities;
+
+class Timestamp
+{
+    /**
+     * The current timestamp type.
+     *
+     * @var string
+     */
+    protected $type;
+
+    /**
+     * The available timestamp types.
+     *
+     * @var array
+     */
+    protected $types = [
+        'ldap',
+        'windows',
+        'windows-int',
+    ];
+
+    /**
+     * Constructor.
+     *
+     * @param string $type
+     *
+     * @throws LdapRecordException
+     */
+    public function __construct($type)
+    {
+        $this->setType($type);
+    }
+
+    /**
+     * Set the type of timestamp to convert from / to.
+     *
+     * @param string $type
+     *
+     * @throws LdapRecordException
+     */
+    public function setType($type)
+    {
+        if (! in_array($type, $this->types)) {
+            throw new LdapRecordException("Unrecognized LDAP date type [$type]");
+        }
+
+        $this->type = $type;
+    }
+
+    /**
+     * Converts the value to an LDAP date string.
+     *
+     * @param mixed $value
+     *
+     * @throws LdapRecordException
+     *
+     * @return float|string
+     */
+    public function fromDateTime($value)
+    {
+        $value = is_array($value) ? reset($value) : $value;
+
+        // If the value is being converted to a windows integer format but it
+        // is already in that format, we will simply return the value back.
+        if ($this->type == 'windows-int' && $this->valueIsWindowsIntegerType($value)) {
+            return $value;
+        }
+        // If the value is numeric, we will assume it's a UNIX timestamp.
+        elseif (is_numeric($value)) {
+            $value = Carbon::createFromTimestamp($value);
+        }
+        // If a string is given, we will pass it into a new carbon instance.
+        elseif (is_string($value)) {
+            $value = Carbon::parse($value);
+        }
+        // If a date object is given, we will convert it to a carbon instance.
+        elseif ($value instanceof DateTime) {
+            $value = Carbon::instance($value);
+        }
+
+        switch ($this->type) {
+            case 'ldap':
+                $value = $this->convertDateTimeToLdapTime($value);
+                break;
+            case 'windows':
+                $value = $this->convertDateTimeToWindows($value);
+                break;
+            case 'windows-int':
+                $value = $this->convertDateTimeToWindowsInteger($value);
+                break;
+            default:
+                throw new LdapRecordException("Unrecognized date type [{$this->type}]");
+        }
+
+        return $value;
+    }
+
+    /**
+     * Determine if the value given is in Windows Integer (NTFS Filetime) format.
+     *
+     * @param int|string $value
+     *
+     * @return bool
+     */
+    protected function valueIsWindowsIntegerType($value)
+    {
+        return is_numeric($value) && strlen((string) $value) === 18;
+    }
+
+    /**
+     * Converts the LDAP timestamp value to a Carbon instance.
+     *
+     * @param mixed $value
+     *
+     * @throws LdapRecordException
+     *
+     * @return Carbon|false
+     */
+    public function toDateTime($value)
+    {
+        $value = is_array($value) ? reset($value) : $value;
+
+        if ($value instanceof CarbonInterface || $value instanceof DateTime) {
+            return Carbon::instance($value);
+        }
+
+        switch ($this->type) {
+            case 'ldap':
+                $value = $this->convertLdapTimeToDateTime($value);
+                break;
+            case 'windows':
+                $value = $this->convertWindowsTimeToDateTime($value);
+                break;
+            case 'windows-int':
+                $value = $this->convertWindowsIntegerTimeToDateTime($value);
+                break;
+            default:
+                throw new LdapRecordException("Unrecognized date type [{$this->type}]");
+        }
+
+        return $value instanceof DateTime ? Carbon::instance($value) : $value;
+    }
+
+    /**
+     * Converts standard LDAP timestamps to a date time object.
+     *
+     * @param string $value
+     *
+     * @return DateTime|bool
+     */
+    protected function convertLdapTimeToDateTime($value)
+    {
+        return DateTime::createFromFormat(
+            strpos($value, 'Z') !== false ? 'YmdHis\Z' : 'YmdHisT',
+            $value
+        );
+    }
+
+    /**
+     * Converts date objects to a standard LDAP timestamp.
+     *
+     * @param DateTime $date
+     *
+     * @return string
+     */
+    protected function convertDateTimeToLdapTime(DateTime $date)
+    {
+        return $date->format(
+            $date->getOffset() == 0 ? 'YmdHis\Z' : 'YmdHisO'
+        );
+    }
+
+    /**
+     * Converts standard windows timestamps to a date time object.
+     *
+     * @param string $value
+     *
+     * @return DateTime|bool
+     */
+    protected function convertWindowsTimeToDateTime($value)
+    {
+        return DateTime::createFromFormat(
+            strpos($value, '0Z') !== false ? 'YmdHis.0\Z' : 'YmdHis.0T',
+            $value
+        );
+    }
+
+    /**
+     * Converts date objects to a windows timestamp.
+     *
+     * @param DateTime $date
+     *
+     * @return string
+     */
+    protected function convertDateTimeToWindows(DateTime $date)
+    {
+        return $date->format(
+            $date->getOffset() == 0 ? 'YmdHis.0\Z' : 'YmdHis.0O'
+        );
+    }
+
+    /**
+     * Converts standard windows integer dates to a date time object.
+     *
+     * @param int $value
+     *
+     * @throws \Exception
+     *
+     * @return DateTime|bool
+     */
+    protected function convertWindowsIntegerTimeToDateTime($value)
+    {
+        // ActiveDirectory dates that contain integers may return
+        // "0" when they are not set. We will validate that here.
+        if (! $value) {
+            return false;
+        }
+
+        return (new DateTime())->setTimestamp(
+            Utilities::convertWindowsTimeToUnixTime($value)
+        );
+    }
+
+    /**
+     * Converts date objects to a windows integer timestamp.
+     *
+     * @param DateTime $date
+     *
+     * @return float
+     */
+    protected function convertDateTimeToWindowsInteger(DateTime $date)
+    {
+        return Utilities::convertUnixTimeToWindowsTime($date->getTimestamp());
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/BatchModification.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/BatchModification.php
new file mode 100644
index 0000000..37f0e87
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/BatchModification.php
@@ -0,0 +1,307 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use InvalidArgumentException;
+
+class BatchModification
+{
+    use DetectsResetIntegers;
+
+    /**
+     * The array keys to be used in batch modifications.
+     */
+    const KEY_ATTRIB = 'attrib';
+    const KEY_MODTYPE = 'modtype';
+    const KEY_VALUES = 'values';
+
+    /**
+     * The attribute of the modification.
+     *
+     * @var string|null
+     */
+    protected $attribute;
+
+    /**
+     * The original value of the attribute before modification.
+     *
+     * @var array
+     */
+    protected $original = [];
+
+    /**
+     * The values of the modification.
+     *
+     * @var array
+     */
+    protected $values = [];
+
+    /**
+     * The modtype integer of the batch modification.
+     *
+     * @var int|null
+     */
+    protected $type;
+
+    /**
+     * Constructor.
+     *
+     * @param string|null     $attribute
+     * @param string|int|null $type
+     * @param array           $values
+     */
+    public function __construct($attribute = null, $type = null, array $values = [])
+    {
+        $this->setAttribute($attribute)
+            ->setType($type)
+            ->setValues($values);
+    }
+
+    /**
+     * Set the original value of the attribute before modification.
+     *
+     * @param array|string $original
+     *
+     * @return $this
+     */
+    public function setOriginal($original = [])
+    {
+        $this->original = $this->normalizeAttributeValues($original);
+
+        return $this;
+    }
+
+    /**
+     * Returns the original value of the attribute before modification.
+     *
+     * @return array
+     */
+    public function getOriginal()
+    {
+        return $this->original;
+    }
+
+    /**
+     * Set the attribute of the modification.
+     *
+     * @param string $attribute
+     *
+     * @return $this
+     */
+    public function setAttribute($attribute)
+    {
+        $this->attribute = $attribute;
+
+        return $this;
+    }
+
+    /**
+     * Returns the attribute of the modification.
+     *
+     * @return string
+     */
+    public function getAttribute()
+    {
+        return $this->attribute;
+    }
+
+    /**
+     * Set the values of the modification.
+     *
+     * @param array $values
+     *
+     * @return $this
+     */
+    public function setValues(array $values = [])
+    {
+        // Null and empty values must also not be added to a batch
+        // modification. Passing null or empty values will result
+        // in an exception when trying to save the modification.
+        $this->values = array_filter($this->normalizeAttributeValues($values), function ($value) {
+            return is_numeric($value) && $this->valueIsResetInteger((int) $value) ?: ! empty($value);
+        });
+
+        return $this;
+    }
+
+    /**
+     * Normalize all of the attribute values.
+     *
+     * @param array|string $values
+     *
+     * @return array
+     */
+    protected function normalizeAttributeValues($values = [])
+    {
+        // We must convert all of the values to strings. Only strings can
+        // be used in batch modifications, otherwise we will we will
+        // receive an LDAP exception while attempting to save.
+        return array_map('strval', (array) $values);
+    }
+
+    /**
+     * Returns the values of the modification.
+     *
+     * @return array
+     */
+    public function getValues()
+    {
+        return $this->values;
+    }
+
+    /**
+     * Set the type of the modification.
+     *
+     * @param int|null $type
+     *
+     * @return $this
+     */
+    public function setType($type = null)
+    {
+        if (is_null($type)) {
+            return $this;
+        }
+
+        if (! $this->isValidType($type)) {
+            throw new InvalidArgumentException('Given batch modification type is invalid.');
+        }
+
+        $this->type = $type;
+
+        return $this;
+    }
+
+    /**
+     * Returns the type of the modification.
+     *
+     * @return int
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Determines if the batch modification is valid in its current state.
+     *
+     * @return bool
+     */
+    public function isValid()
+    {
+        return ! is_null($this->get());
+    }
+
+    /**
+     * Builds the type of modification automatically
+     * based on the current and original values.
+     *
+     * @return $this
+     */
+    public function build()
+    {
+        switch (true) {
+            case empty($this->original) && empty($this->values):
+                return $this;
+            case ! empty($this->original) && empty($this->values):
+                return $this->setType(LDAP_MODIFY_BATCH_REMOVE_ALL);
+            case empty($this->original) && ! empty($this->values):
+                return $this->setType(LDAP_MODIFY_BATCH_ADD);
+            default:
+               return $this->determineBatchTypeFromOriginal();
+        }
+    }
+
+    /**
+     * Determine the batch modification type from the original values.
+     *
+     * @return $this
+     */
+    protected function determineBatchTypeFromOriginal()
+    {
+        $added = $this->getAddedValues();
+        $removed = $this->getRemovedValues();
+
+        switch (true) {
+            case ! empty($added) && ! empty($removed):
+                return $this->setType(LDAP_MODIFY_BATCH_REPLACE);
+            case ! empty($added):
+                return $this->setValues($added)->setType(LDAP_MODIFY_BATCH_ADD);
+            case ! empty($removed):
+                return $this->setValues($removed)->setType(LDAP_MODIFY_BATCH_REMOVE);
+            default:
+                return $this;
+        }
+    }
+
+    /**
+     * Get the values that were added to the attribute.
+     *
+     * @return array
+     */
+    protected function getAddedValues()
+    {
+        return array_values(
+            array_diff($this->values, $this->original)
+        );
+    }
+
+    /**
+     * Get the values that were removed from the attribute.
+     *
+     * @return array
+     */
+    protected function getRemovedValues()
+    {
+        return array_values(
+            array_diff($this->original, $this->values)
+        );
+    }
+
+    /**
+     * Returns the built batch modification array.
+     *
+     * @return array|null
+     */
+    public function get()
+    {
+        switch ($this->type) {
+            case LDAP_MODIFY_BATCH_REMOVE_ALL:
+                // A values key cannot be provided when
+                // a remove all type is selected.
+                return [
+                    static::KEY_ATTRIB => $this->attribute,
+                    static::KEY_MODTYPE => $this->type,
+                ];
+            case LDAP_MODIFY_BATCH_REMOVE:
+                // Fallthrough.
+            case LDAP_MODIFY_BATCH_ADD:
+                // Fallthrough.
+            case LDAP_MODIFY_BATCH_REPLACE:
+                return [
+                    static::KEY_ATTRIB => $this->attribute,
+                    static::KEY_MODTYPE => $this->type,
+                    static::KEY_VALUES => $this->values,
+                ];
+            default:
+                // If the modtype isn't recognized, we'll return null.
+                return;
+        }
+    }
+
+    /**
+     * Determines if the given modtype is valid.
+     *
+     * @param int $type
+     *
+     * @return bool
+     */
+    protected function isValidType($type)
+    {
+        return in_array($type, [
+            LDAP_MODIFY_BATCH_REMOVE_ALL,
+            LDAP_MODIFY_BATCH_REMOVE,
+            LDAP_MODIFY_BATCH_REPLACE,
+            LDAP_MODIFY_BATCH_ADD,
+        ]);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Collection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Collection.php
new file mode 100644
index 0000000..850167b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Collection.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use Closure;
+use LdapRecord\Models\Attributes\DistinguishedName;
+use LdapRecord\Query\Collection as QueryCollection;
+use LdapRecord\Support\Arr;
+
+class Collection extends QueryCollection
+{
+    /**
+     * Determine if the collection contains all of the given models, or any models.
+     *
+     * @param mixed $models
+     *
+     * @return bool
+     */
+    public function exists($models = null)
+    {
+        $models = $this->getArrayableModels($models);
+
+        // If any arguments were given and the result set is
+        // empty, we can simply return false here. We can't
+        // verify the existence of models without results.
+        if (func_num_args() > 0 && empty(array_filter($models))) {
+            return false;
+        }
+
+        if (! $models) {
+            return parent::isNotEmpty();
+        }
+
+        foreach ($models as $model) {
+            $exists = parent::contains(function (Model $related) use ($model) {
+                return $this->compareModelWithRelated($model, $related);
+            });
+
+            if (! $exists) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Determine if any of the given models are contained in the collection.
+     *
+     * @param mixed $key
+     * @param mixed $operator
+     * @param mixed $value
+     *
+     * @return bool
+     */
+    public function contains($key, $operator = null, $value = null)
+    {
+        if (func_num_args() > 1 || $key instanceof Closure) {
+            // If we are supplied with more than one argument, or
+            // we were passed a closure, we will utilize the
+            // parents contains method, for compatibility.
+            return parent::contains($key, $operator, $value);
+        }
+
+        foreach ($this->getArrayableModels($key) as $model) {
+            $exists = parent::contains(function (Model $related) use ($model) {
+                return $this->compareModelWithRelated($model, $related);
+            });
+
+            if ($exists) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the provided models as an array.
+     *
+     * @param mixed $models
+     *
+     * @return array
+     */
+    protected function getArrayableModels($models = null)
+    {
+        return $models instanceof QueryCollection
+            ? $models->toArray()
+            : Arr::wrap($models);
+    }
+
+    /**
+     * Compare the related model with the given.
+     *
+     * @param Model|string $model
+     * @param Model        $related
+     *
+     * @return bool
+     */
+    protected function compareModelWithRelated($model, $related)
+    {
+        if (is_string($model)) {
+            return $this->isValidDn($model)
+                ? $related->getDn() == $model
+                : $related->getName() == $model;
+        }
+
+        return $related->is($model);
+    }
+
+    /**
+     * Determine if the given string is a valid distinguished name.
+     *
+     * @param string $dn
+     *
+     * @return bool
+     */
+    protected function isValidDn($dn)
+    {
+        return ! empty((new DistinguishedName($dn))->components());
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/CanAuthenticate.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/CanAuthenticate.php
new file mode 100644
index 0000000..f287454
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/CanAuthenticate.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+trait CanAuthenticate
+{
+    /**
+     * Get the name of the unique identifier for the user.
+     *
+     * @return string
+     */
+    public function getAuthIdentifierName()
+    {
+        return $this->guidKey;
+    }
+
+    /**
+     * Get the unique identifier for the user.
+     *
+     * @return mixed
+     */
+    public function getAuthIdentifier()
+    {
+        return $this->getConvertedGuid();
+    }
+
+    /**
+     * Get the password for the user.
+     *
+     * @return string
+     */
+    public function getAuthPassword()
+    {
+    }
+
+    /**
+     * Get the token value for the "remember me" session.
+     *
+     * @return string
+     */
+    public function getRememberToken()
+    {
+    }
+
+    /**
+     * Set the token value for the "remember me" session.
+     *
+     * @param string $value
+     *
+     * @return void
+     */
+    public function setRememberToken($value)
+    {
+    }
+
+    /**
+     * Get the column name for the "remember me" token.
+     *
+     * @return string
+     */
+    public function getRememberTokenName()
+    {
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasAttributes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasAttributes.php
new file mode 100644
index 0000000..20fcec0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasAttributes.php
@@ -0,0 +1,1106 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use Carbon\Carbon;
+use DateTimeInterface;
+use Exception;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Attributes\MbString;
+use LdapRecord\Models\Attributes\Timestamp;
+use LdapRecord\Models\DetectsResetIntegers;
+use LdapRecord\Support\Arr;
+
+trait HasAttributes
+{
+    use DetectsResetIntegers;
+
+    /**
+     * The models original attributes.
+     *
+     * @var array
+     */
+    protected $original = [];
+
+    /**
+     * The models attributes.
+     *
+     * @var array
+     */
+    protected $attributes = [];
+
+    /**
+     * The attributes that should be mutated to dates.
+     *
+     * @var array
+     */
+    protected $dates = [];
+
+    /**
+     * The attributes that should be cast to their native types.
+     *
+     * @var array
+     */
+    protected $casts = [];
+
+    /**
+     * The accessors to append to the model's array form.
+     *
+     * @var array
+     */
+    protected $appends = [];
+
+    /**
+     * The format that dates must be output to for serialization.
+     *
+     * @var string
+     */
+    protected $dateFormat;
+
+    /**
+     * The default attributes that should be mutated to dates.
+     *
+     * @var array
+     */
+    protected $defaultDates = [
+        'createtimestamp' => 'ldap',
+        'modifytimestamp' => 'ldap',
+    ];
+
+    /**
+     * The cache of the mutated attributes for each class.
+     *
+     * @var array
+     */
+    protected static $mutatorCache = [];
+
+    /**
+     * Convert the model's attributes to an array.
+     *
+     * @return array
+     */
+    public function attributesToArray()
+    {
+        // Here we will replace our LDAP formatted dates with
+        // properly formatted ones, so dates do not need to
+        // be converted manually after being returned.
+        $attributes = $this->addDateAttributesToArray(
+            $attributes = $this->getArrayableAttributes()
+        );
+
+        $attributes = $this->addMutatedAttributesToArray(
+            $attributes,
+            $this->getMutatedAttributes()
+        );
+
+        // Before we go ahead and encode each value, we'll attempt
+        // converting any necessary attribute values to ensure
+        // they can be encoded, such as GUIDs and SIDs.
+        $attributes = $this->convertAttributesForJson($attributes);
+
+        // Here we will grab all of the appended, calculated attributes to this model
+        // as these attributes are not really in the attributes array, but are run
+        // when we need to array or JSON the model for convenience to the coder.
+        foreach ($this->getArrayableAppends() as $key) {
+            $attributes[$key] = $this->mutateAttributeForArray($key, null);
+        }
+
+        // Now we will go through each attribute to make sure it is
+        // properly encoded. If attributes aren't in UTF-8, we will
+        // encounter JSON encoding errors upon model serialization.
+        return $this->encodeAttributes($attributes);
+    }
+
+    /**
+     * Add the date attributes to the attributes array.
+     *
+     * @param array $attributes
+     *
+     * @return array
+     */
+    protected function addDateAttributesToArray(array $attributes)
+    {
+        foreach ($this->getDates() as $attribute => $type) {
+            if (! isset($attributes[$attribute])) {
+                continue;
+            }
+
+            $date = $this->asDateTime($attributes[$attribute], $type);
+
+            $attributes[$attribute] = $date instanceof Carbon
+                ? Arr::wrap($this->serializeDate($date))
+                : $attributes[$attribute];
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Prepare a date for array / JSON serialization.
+     *
+     * @param DateTimeInterface $date
+     *
+     * @return string
+     */
+    protected function serializeDate(DateTimeInterface $date)
+    {
+        return $date->format($this->getDateFormat());
+    }
+
+    /**
+     * Recursively UTF-8 encode the given attributes.
+     *
+     * @return array
+     */
+    public function encodeAttributes($attributes)
+    {
+        array_walk_recursive($attributes, function (&$value) {
+            $value = $this->encodeValue($value);
+        });
+
+        return $attributes;
+    }
+
+    /**
+     * Encode the given value for proper serialization.
+     *
+     * @param string $value
+     *
+     * @return string
+     */
+    protected function encodeValue($value)
+    {
+        // If we are able to detect the encoding, we will
+        // encode only the attributes that need to be,
+        // so that we do not double encode values.
+        return MbString::isLoaded() && MbString::isUtf8($value) ? $value : utf8_encode($value);
+    }
+
+    /**
+     * Add the mutated attributes to the attributes array.
+     *
+     * @param array $attributes
+     * @param array $mutatedAttributes
+     *
+     * @return array
+     */
+    protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
+    {
+        foreach ($mutatedAttributes as $key) {
+            // We want to spin through all the mutated attributes for this model and call
+            // the mutator for the attribute. We cache off every mutated attributes so
+            // we don't have to constantly check on attributes that actually change.
+            if (! Arr::exists($attributes, $key)) {
+                continue;
+            }
+
+            // Next, we will call the mutator for this attribute so that we can get these
+            // mutated attribute's actual values. After we finish mutating each of the
+            // attributes we will return this final array of the mutated attributes.
+            $attributes[$key] = $this->mutateAttributeForArray(
+                $key,
+                $attributes[$key]
+            );
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Set the model's original attributes with the model's current attributes.
+     *
+     * @return $this
+     */
+    public function syncOriginal()
+    {
+        $this->original = $this->attributes;
+
+        return $this;
+    }
+
+    /**
+     * Fills the entry with the supplied attributes.
+     *
+     * @param array $attributes
+     *
+     * @return $this
+     */
+    public function fill(array $attributes = [])
+    {
+        foreach ($attributes as $key => $value) {
+            $this->setAttribute($key, $value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns the models attribute by its key.
+     *
+     * @param int|string $key
+     *
+     * @return mixed
+     */
+    public function getAttribute($key)
+    {
+        if (! $key) {
+            return;
+        }
+
+        return $this->getAttributeValue($key);
+    }
+
+    /**
+     * Get an attributes value.
+     *
+     * @param string $key
+     *
+     * @return mixed
+     */
+    public function getAttributeValue($key)
+    {
+        $key = $this->normalizeAttributeKey($key);
+        $value = $this->getAttributeFromArray($key);
+
+        if ($this->hasGetMutator($key)) {
+            return $this->getMutatedAttributeValue($key, $value);
+        }
+
+        if ($this->isDateAttribute($key) && ! is_null($value)) {
+            return $this->asDateTime(Arr::first($value), $this->getDates()[$key]);
+        }
+
+        if ($this->isCastedAttribute($key) && ! is_null($value)) {
+            return $this->castAttribute($key, $value);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Determine if the given attribute is a date.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function isDateAttribute($key)
+    {
+        return array_key_exists($key, $this->getDates());
+    }
+
+    /**
+     * Get the attributes that should be mutated to dates.
+     *
+     * @return array
+     */
+    public function getDates()
+    {
+        // Since array string keys can be unique depending
+        // on casing differences, we need to normalize the
+        // array key case so they are merged properly.
+        return array_merge(
+            array_change_key_case($this->defaultDates, CASE_LOWER),
+            array_change_key_case($this->dates, CASE_LOWER)
+        );
+    }
+
+    /**
+     * Convert the given date value to an LDAP compatible value.
+     *
+     * @param string $type
+     * @param mixed  $value
+     *
+     * @throws LdapRecordException
+     *
+     * @return float|string
+     */
+    public function fromDateTime($type, $value)
+    {
+        return (new Timestamp($type))->fromDateTime($value);
+    }
+
+    /**
+     * Convert the given LDAP date value to a Carbon instance.
+     *
+     * @param mixed  $value
+     * @param string $type
+     *
+     * @throws LdapRecordException
+     *
+     * @return Carbon|false
+     */
+    public function asDateTime($value, $type)
+    {
+        return (new Timestamp($type))->toDateTime($value);
+    }
+
+    /**
+     * Determine whether an attribute should be cast to a native type.
+     *
+     * @param string            $key
+     * @param array|string|null $types
+     *
+     * @return bool
+     */
+    public function hasCast($key, $types = null)
+    {
+        if (array_key_exists($key, $this->getCasts())) {
+            return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the attributes that should be cast to their native types.
+     *
+     * @return array
+     */
+    protected function getCasts()
+    {
+        return array_change_key_case($this->casts, CASE_LOWER);
+    }
+
+    /**
+     * Determine whether a value is JSON castable for inbound manipulation.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    protected function isJsonCastable($key)
+    {
+        return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
+    }
+
+    /**
+     * Get the type of cast for a model attribute.
+     *
+     * @param string $key
+     *
+     * @return string
+     */
+    protected function getCastType($key)
+    {
+        if ($this->isDecimalCast($this->getCasts()[$key])) {
+            return 'decimal';
+        }
+
+        if ($this->isDateTimeCast($this->getCasts()[$key])) {
+            return 'datetime';
+        }
+
+        return trim(strtolower($this->getCasts()[$key]));
+    }
+
+    /**
+     * Determine if the cast is a decimal.
+     *
+     * @param string $cast
+     *
+     * @return bool
+     */
+    protected function isDecimalCast($cast)
+    {
+        return strncmp($cast, 'decimal:', 8) === 0;
+    }
+
+    /**
+     * Determine if the cast is a datetime.
+     *
+     * @param string $cast
+     *
+     * @return bool
+     */
+    protected function isDateTimeCast($cast)
+    {
+        return strncmp($cast, 'datetime:', 8) === 0;
+    }
+
+    /**
+     * Determine if the given attribute must be casted.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    protected function isCastedAttribute($key)
+    {
+        return array_key_exists($key, array_change_key_case($this->casts, CASE_LOWER));
+    }
+
+    /**
+     * Cast an attribute to a native PHP type.
+     *
+     * @param string     $key
+     * @param array|null $value
+     *
+     * @return mixed
+     */
+    protected function castAttribute($key, $value)
+    {
+        $value = $this->castRequiresArrayValue($key) ? $value : Arr::first($value);
+
+        if (is_null($value)) {
+            return $value;
+        }
+
+        switch ($this->getCastType($key)) {
+            case 'int':
+            case 'integer':
+                return (int) $value;
+            case 'real':
+            case 'float':
+            case 'double':
+                return $this->fromFloat($value);
+            case 'decimal':
+                return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
+            case 'string':
+                return (string) $value;
+            case 'bool':
+            case 'boolean':
+                return $this->asBoolean($value);
+            case 'object':
+                return $this->fromJson($value, $asObject = true);
+            case 'array':
+            case 'json':
+                return $this->fromJson($value);
+            case 'collection':
+                return $this->newCollection($value);
+            case 'datetime':
+                return $this->asDateTime($value, explode(':', $this->getCasts()[$key], 2)[1]);
+            default:
+                return $value;
+        }
+    }
+
+    /**
+     * Determine if the cast type requires the first attribute value.
+     *
+     * @return bool
+     */
+    protected function castRequiresArrayValue($key)
+    {
+        return in_array($this->getCastType($key), ['collection']);
+    }
+
+    /**
+     * Cast the given attribute to JSON.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return string
+     */
+    protected function castAttributeAsJson($key, $value)
+    {
+        $value = $this->asJson($value);
+
+        if ($value === false) {
+            $class = get_class($this);
+            $message = json_last_error_msg();
+
+            throw new Exception("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}.");
+        }
+
+        return $value;
+    }
+
+    /**
+     * Convert the model to its JSON representation.
+     *
+     * @return string
+     */
+    public function toJson()
+    {
+        return json_encode($this);
+    }
+
+    /**
+     * Encode the given value as JSON.
+     *
+     * @param mixed $value
+     *
+     * @return string
+     */
+    protected function asJson($value)
+    {
+        return json_encode($value);
+    }
+
+    /**
+     * Decode the given JSON back into an array or object.
+     *
+     * @param string $value
+     * @param bool   $asObject
+     *
+     * @return mixed
+     */
+    public function fromJson($value, $asObject = false)
+    {
+        return json_decode($value, ! $asObject);
+    }
+
+    /**
+     * Decode the given float.
+     *
+     * @param mixed $value
+     *
+     * @return mixed
+     */
+    public function fromFloat($value)
+    {
+        switch ((string) $value) {
+            case 'Infinity':
+                return INF;
+            case '-Infinity':
+                return -INF;
+            case 'NaN':
+                return NAN;
+            default:
+                return (float) $value;
+        }
+    }
+
+    /**
+     * Cast the value to a boolean.
+     *
+     * @param mixed $value
+     *
+     * @return bool
+     */
+    protected function asBoolean($value)
+    {
+        $map = ['true' => true, 'false' => false];
+
+        return $map[strtolower($value)] ?? (bool) $value;
+    }
+
+    /**
+     * Cast a decimal value as a string.
+     *
+     * @param float $value
+     * @param int   $decimals
+     *
+     * @return string
+     */
+    protected function asDecimal($value, $decimals)
+    {
+        return number_format($value, $decimals, '.', '');
+    }
+
+    /**
+     * Get an attribute array of all arrayable attributes.
+     *
+     * @return array
+     */
+    protected function getArrayableAttributes()
+    {
+        return $this->getArrayableItems($this->attributes);
+    }
+
+    /**
+     * Get an attribute array of all arrayable values.
+     *
+     * @param array $values
+     *
+     * @return array
+     */
+    protected function getArrayableItems(array $values)
+    {
+        if (count($visible = $this->getVisible()) > 0) {
+            $values = array_intersect_key($values, array_flip($visible));
+        }
+
+        if (count($hidden = $this->getHidden()) > 0) {
+            $values = array_diff_key($values, array_flip($hidden));
+        }
+
+        return $values;
+    }
+
+    /**
+     * Get all of the appendable values that are arrayable.
+     *
+     * @return array
+     */
+    protected function getArrayableAppends()
+    {
+        if (empty($this->appends)) {
+            return [];
+        }
+
+        return $this->getArrayableItems(
+            array_combine($this->appends, $this->appends)
+        );
+    }
+
+    /**
+     * Get the format for date serialization.
+     *
+     * @return string
+     */
+    public function getDateFormat()
+    {
+        return $this->dateFormat ?: DateTimeInterface::ISO8601;
+    }
+
+    /**
+     * Set the date format used by the model for serialization.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setDateFormat($format)
+    {
+        $this->dateFormat = $format;
+
+        return $this;
+    }
+
+    /**
+     * Get an attribute from the $attributes array.
+     *
+     * @param string $key
+     *
+     * @return mixed
+     */
+    protected function getAttributeFromArray($key)
+    {
+        return $this->getNormalizedAttributes()[$key] ?? null;
+    }
+
+    /**
+     * Get the attributes with their keys normalized.
+     *
+     * @return array
+     */
+    protected function getNormalizedAttributes()
+    {
+        return array_change_key_case($this->attributes, CASE_LOWER);
+    }
+
+    /**
+     * Returns the first attribute by the specified key.
+     *
+     * @param string $key
+     *
+     * @return mixed
+     */
+    public function getFirstAttribute($key)
+    {
+        return Arr::first(
+            Arr::wrap($this->getAttribute($key))
+        );
+    }
+
+    /**
+     * Returns all of the models attributes.
+     *
+     * @return array
+     */
+    public function getAttributes()
+    {
+        return $this->attributes;
+    }
+
+    /**
+     * Set an attribute value by the specified key and sub-key.
+     *
+     * @param mixed $key
+     * @param mixed $value
+     *
+     * @return $this
+     */
+    public function setAttribute($key, $value)
+    {
+        $key = $this->normalizeAttributeKey($key);
+
+        if ($this->hasSetMutator($key)) {
+            return $this->setMutatedAttributeValue($key, $value);
+        } elseif (
+            $value &&
+            $this->isDateAttribute($key) &&
+            ! $this->valueIsResetInteger($value)
+        ) {
+            $value = $this->fromDateTime($this->getDates()[$key], $value);
+        }
+
+        if ($this->isJsonCastable($key) && ! is_null($value)) {
+            $value = $this->castAttributeAsJson($key, $value);
+        }
+
+        $this->attributes[$key] = Arr::wrap($value);
+
+        return $this;
+    }
+
+    /**
+     * Set the models first attribute value.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return $this
+     */
+    public function setFirstAttribute($key, $value)
+    {
+        return $this->setAttribute($key, Arr::wrap($value));
+    }
+
+    /**
+     * Add a unique value to the given attribute.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return $this
+     */
+    public function addAttributeValue($key, $value)
+    {
+        return $this->setAttribute($key, array_unique(
+            array_merge(
+                Arr::wrap($this->getAttribute($key)),
+                Arr::wrap($value)
+            )
+        ));
+    }
+
+    /**
+     * Determine if a get mutator exists for an attribute.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function hasGetMutator($key)
+    {
+        return method_exists($this, 'get'.$this->getMutatorMethodName($key).'Attribute');
+    }
+
+    /**
+     * Determine if a set mutator exists for an attribute.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function hasSetMutator($key)
+    {
+        return method_exists($this, 'set'.$this->getMutatorMethodName($key).'Attribute');
+    }
+
+    /**
+     * Set the value of an attribute using its mutator.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return mixed
+     */
+    protected function setMutatedAttributeValue($key, $value)
+    {
+        return $this->{'set'.$this->getMutatorMethodName($key).'Attribute'}($value);
+    }
+
+    /**
+     * Get the value of an attribute using its mutator.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return mixed
+     */
+    protected function getMutatedAttributeValue($key, $value)
+    {
+        return $this->{'get'.$this->getMutatorMethodName($key).'Attribute'}($value);
+    }
+
+    /**
+     * Get the mutator attribute method name.
+     *
+     * Hyphenated attributes will use pascal cased methods.
+     *
+     * @param string $key
+     *
+     * @return mixed
+     */
+    protected function getMutatorMethodName($key)
+    {
+        $key = ucwords(str_replace('-', ' ', $key));
+
+        return str_replace(' ', '', $key);
+    }
+
+    /**
+     * Get the value of an attribute using its mutator for array conversion.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return array
+     */
+    protected function mutateAttributeForArray($key, $value)
+    {
+        return Arr::wrap(
+            $this->getMutatedAttributeValue($key, $value)
+        );
+    }
+
+    /**
+     * Set the attributes property.
+     *
+     * Used when constructing an existing LDAP record.
+     *
+     * @param array $attributes
+     *
+     * @return $this
+     */
+    public function setRawAttributes(array $attributes = [])
+    {
+        // We will filter out those annoying 'count' keys
+        // returned with LDAP results and lowercase all
+        // root array keys to prevent any casing issues.
+        $raw = array_change_key_case($this->filterRawAttributes($attributes), CASE_LOWER);
+
+        // Before setting the models attributes, we will filter
+        // out the attributes that contain an integer key. LDAP
+        // search results will contain integer keys that have
+        // attribute names as values. We don't need these.
+        $this->attributes = array_filter($raw, function ($key) {
+            return ! is_int($key);
+        }, ARRAY_FILTER_USE_KEY);
+
+        // LDAP search results will contain the distinguished
+        // name inside of the `dn` key. We will retrieve this,
+        // and then set it on the model for accessibility.
+        if (Arr::exists($attributes, 'dn')) {
+            $this->dn = Arr::accessible($attributes['dn'])
+                ? Arr::first($attributes['dn'])
+                : $attributes['dn'];
+        }
+
+        $this->syncOriginal();
+
+        // Here we will set the exists attribute to true,
+        // since raw attributes are only set in the case
+        // of attributes being loaded by query results.
+        $this->exists = true;
+
+        return $this;
+    }
+
+    /**
+     * Filters the count key recursively from raw LDAP attributes.
+     *
+     * @param array $attributes
+     * @param array $keys
+     *
+     * @return array
+     */
+    public function filterRawAttributes(array $attributes = [], array $keys = ['count', 'dn'])
+    {
+        foreach ($keys as $key) {
+            unset($attributes[$key]);
+        }
+
+        foreach ($attributes as $key => $value) {
+            $attributes[$key] = is_array($value)
+                ? $this->filterRawAttributes($value, $keys)
+                : $value;
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Determine if the model has the given attribute.
+     *
+     * @param int|string $key
+     *
+     * @return bool
+     */
+    public function hasAttribute($key)
+    {
+        return [] !== ($this->attributes[$this->normalizeAttributeKey($key)] ?? []);
+    }
+
+    /**
+     * Returns the number of attributes.
+     *
+     * @return int
+     */
+    public function countAttributes()
+    {
+        return count($this->getAttributes());
+    }
+
+    /**
+     * Returns the models original attributes.
+     *
+     * @return array
+     */
+    public function getOriginal()
+    {
+        return $this->original;
+    }
+
+    /**
+     * Get the attributes that have been changed since last sync.
+     *
+     * @return array
+     */
+    public function getDirty()
+    {
+        $dirty = [];
+
+        foreach ($this->attributes as $key => $value) {
+            if ($this->isDirty($key)) {
+                // We need to reset the array using array_values due to
+                // LDAP requiring consecutive indices (0, 1, 2 etc.).
+                // We would receive an exception otherwise.
+                $dirty[$key] = array_values($value);
+            }
+        }
+
+        return $dirty;
+    }
+
+    /**
+     * Determine if the given attribute is dirty.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function isDirty($key)
+    {
+        return ! $this->originalIsEquivalent($key);
+    }
+
+    /**
+     * Get the accessors being appended to the models array form.
+     *
+     * @return array
+     */
+    public function getAppends()
+    {
+        return $this->appends;
+    }
+
+    /**
+     * Set the accessors to append to model arrays.
+     *
+     * @param array $appends
+     *
+     * @return $this
+     */
+    public function setAppends(array $appends)
+    {
+        $this->appends = $appends;
+
+        return $this;
+    }
+
+    /**
+     * Return whether the accessor attribute has been appended.
+     *
+     * @param string $attribute
+     *
+     * @return bool
+     */
+    public function hasAppended($attribute)
+    {
+        return in_array($attribute, $this->appends);
+    }
+
+    /**
+     * Returns a normalized attribute key.
+     *
+     * @param string $key
+     *
+     * @return string
+     */
+    public function normalizeAttributeKey($key)
+    {
+        // Since LDAP supports hyphens in attribute names,
+        // we'll convert attributes being retrieved by
+        // underscores into hyphens for convenience.
+        return strtolower(
+            str_replace('_', '-', $key)
+        );
+    }
+
+    /**
+     * Determine if the new and old values for a given key are equivalent.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    protected function originalIsEquivalent($key)
+    {
+        if (! array_key_exists($key, $this->original)) {
+            return false;
+        }
+
+        $current = $this->attributes[$key];
+        $original = $this->original[$key];
+
+        if ($current === $original) {
+            return true;
+        }
+
+        return  is_numeric($current) &&
+                is_numeric($original) &&
+                strcmp((string) $current, (string) $original) === 0;
+    }
+
+    /**
+     * Get the mutated attributes for a given instance.
+     *
+     * @return array
+     */
+    public function getMutatedAttributes()
+    {
+        $class = static::class;
+
+        if (! isset(static::$mutatorCache[$class])) {
+            static::cacheMutatedAttributes($class);
+        }
+
+        return static::$mutatorCache[$class];
+    }
+
+    /**
+     * Extract and cache all the mutated attributes of a class.
+     *
+     * @param string $class
+     *
+     * @return void
+     */
+    public static function cacheMutatedAttributes($class)
+    {
+        static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->reject(function ($match) {
+            return $match === 'First';
+        })->map(function ($match) {
+            return lcfirst($match);
+        })->all();
+    }
+
+    /**
+     * Get all of the attribute mutator methods.
+     *
+     * @param mixed $class
+     *
+     * @return array
+     */
+    protected static function getMutatorMethods($class)
+    {
+        preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
+
+        return $matches[1];
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasEvents.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasEvents.php
new file mode 100644
index 0000000..1bc76d0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasEvents.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use Closure;
+use LdapRecord\Models\Events\Event;
+
+trait HasEvents
+{
+    /**
+     * Fires the specified model event.
+     *
+     * @param Event $event
+     *
+     * @return mixed
+     */
+    protected function fireModelEvent(Event $event)
+    {
+        return static::getConnectionContainer()->getEventDispatcher()->fire($event);
+    }
+
+    /**
+     * Listens to a model event.
+     *
+     * @param string  $event
+     * @param Closure $listener
+     *
+     * @return mixed
+     */
+    protected function listenForModelEvent($event, Closure $listener)
+    {
+        return static::getConnectionContainer()->getEventDispatcher()->listen($event, $listener);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasGlobalScopes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasGlobalScopes.php
new file mode 100644
index 0000000..c14abad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasGlobalScopes.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use Closure;
+use InvalidArgumentException;
+use LdapRecord\Models\Scope;
+
+trait HasGlobalScopes
+{
+    /**
+     * Register a new global scope on the model.
+     *
+     * @param Scope|Closure|string $scope
+     * @param Closure|null         $implementation
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return mixed
+     */
+    public static function addGlobalScope($scope, Closure $implementation = null)
+    {
+        if (is_string($scope) && ! is_null($implementation)) {
+            return static::$globalScopes[static::class][$scope] = $implementation;
+        } elseif ($scope instanceof Closure) {
+            return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
+        } elseif ($scope instanceof Scope) {
+            return static::$globalScopes[static::class][get_class($scope)] = $scope;
+        }
+
+        throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.');
+    }
+
+    /**
+     * Determine if a model has a global scope.
+     *
+     * @param Scope|string $scope
+     *
+     * @return bool
+     */
+    public static function hasGlobalScope($scope)
+    {
+        return ! is_null(static::getGlobalScope($scope));
+    }
+
+    /**
+     * Get a global scope registered with the model.
+     *
+     * @param Scope|string $scope
+     *
+     * @return Scope|Closure|null
+     */
+    public static function getGlobalScope($scope)
+    {
+        if (array_key_exists(static::class, static::$globalScopes)) {
+            $scopeName = is_string($scope) ? $scope : get_class($scope);
+
+            return array_key_exists($scopeName, static::$globalScopes[static::class])
+                ? static::$globalScopes[static::class][$scopeName]
+                : null;
+        }
+    }
+
+    /**
+     * Get the global scopes for this class instance.
+     *
+     * @return array
+     */
+    public function getGlobalScopes()
+    {
+        return array_key_exists(static::class, static::$globalScopes)
+            ? static::$globalScopes[static::class]
+            : [];
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasPassword.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasPassword.php
new file mode 100644
index 0000000..9822456
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasPassword.php
@@ -0,0 +1,251 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use LdapRecord\ConnectionException;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Attributes\Password;
+
+trait HasPassword
+{
+    /**
+     * Set the password on the user.
+     *
+     * @param string|array $password
+     *
+     * @throws ConnectionException
+     */
+    public function setPasswordAttribute($password)
+    {
+        $this->validateSecureConnection();
+
+        // Here we will attempt to determine the password hash method in use
+        // by parsing the users hashed password (if it as available). If a
+        // method is determined, we will override the default here.
+        if (! ($method = $this->determinePasswordHashMethod())) {
+            $method = $this->getPasswordHashMethod();
+        }
+
+        // If the password given is an array, we can assume we
+        // are changing the password for the current user.
+        if (is_array($password)) {
+            $this->setChangedPassword(
+                $this->getHashedPassword($method, $password[0], $this->getPasswordSalt($method)),
+                $this->getHashedPassword($method, $password[1]),
+                $this->getPasswordAttributeName()
+            );
+        }
+        // Otherwise, we will assume the password is being
+        // reset, overwriting the one currently in place.
+        else {
+            $this->setPassword(
+                $this->getHashedPassword($method, $password),
+                $this->getPasswordAttributeName()
+            );
+        }
+    }
+
+    /**
+     * Alias for setting the password on the user.
+     *
+     * @param string|array $password
+     *
+     * @throws ConnectionException
+     */
+    public function setUnicodepwdAttribute($password)
+    {
+        $this->setPasswordAttribute($password);
+    }
+
+    /**
+     * An accessor for retrieving the user's hashed password value.
+     *
+     * @return string|null
+     */
+    public function getPasswordAttribute()
+    {
+        return $this->getAttribute($this->getPasswordAttributeName())[0] ?? null;
+    }
+
+    /**
+     * Get the name of the attribute that contains the user's password.
+     *
+     * @return string
+     */
+    public function getPasswordAttributeName()
+    {
+        if (property_exists($this, 'passwordAttribute')) {
+            return $this->passwordAttribute;
+        }
+
+        if (method_exists($this, 'passwordAttribute')) {
+            return $this->passwordAttribute();
+        }
+
+        return 'unicodepwd';
+    }
+
+    /**
+     * Get the name of the method to use for hashing the user's password.
+     *
+     * @return string
+     */
+    public function getPasswordHashMethod()
+    {
+        if (property_exists($this, 'passwordHashMethod')) {
+            return $this->passwordHashMethod;
+        }
+
+        if (method_exists($this, 'passwordHashMethod')) {
+            return $this->passwordHashMethod();
+        }
+
+        return 'encode';
+    }
+
+    /**
+     * Set the changed password.
+     *
+     * @param string $oldPassword
+     * @param string $newPassword
+     * @param string $attribute
+     *
+     * @return void
+     */
+    protected function setChangedPassword($oldPassword, $newPassword, $attribute)
+    {
+        // Create batch modification for removing the old password.
+        $this->addModification(
+            $this->newBatchModification(
+                $attribute,
+                LDAP_MODIFY_BATCH_REMOVE,
+                [$oldPassword]
+            )
+        );
+
+        // Create batch modification for adding the new password.
+        $this->addModification(
+            $this->newBatchModification(
+                $attribute,
+                LDAP_MODIFY_BATCH_ADD,
+                [$newPassword]
+            )
+        );
+    }
+
+    /**
+     * Set the password on the model.
+     *
+     * @param string $password
+     * @param string $attribute
+     *
+     * @return void
+     */
+    protected function setPassword($password, $attribute)
+    {
+        $this->addModification(
+            $this->newBatchModification(
+                $attribute,
+                LDAP_MODIFY_BATCH_REPLACE,
+                [$password]
+            )
+        );
+    }
+
+    /**
+     * Encode / hash the given password.
+     *
+     * @param string $method
+     * @param string $password
+     * @param string $salt
+     *
+     * @throws LdapRecordException
+     *
+     * @return string
+     */
+    protected function getHashedPassword($method, $password, $salt = null)
+    {
+        if (! method_exists(Password::class, $method)) {
+            throw new LdapRecordException("Password hashing method [{$method}] does not exist.");
+        }
+
+        if (Password::hashMethodRequiresSalt($method)) {
+            return Password::{$method}($password, $salt);
+        }
+
+        return Password::{$method}($password);
+    }
+
+    /**
+     * Validates that the current LDAP connection is secure.
+     *
+     * @throws ConnectionException
+     *
+     * @return void
+     */
+    protected function validateSecureConnection()
+    {
+        $connection = $this->getConnection();
+
+        if ($connection->isConnected()) {
+            $secure = $connection->getLdapConnection()->canChangePasswords();
+        } else {
+            $secure = $connection->getConfiguration()->get('use_ssl') || $connection->getConfiguration()->get('use_tls');
+        }
+
+        if (! $secure) {
+            throw new ConnectionException(
+                'You must be connected to your LDAP server with TLS or SSL to perform this operation.'
+            );
+        }
+    }
+
+    /**
+     * Attempt to retrieve the password's salt.
+     *
+     * @param string $method
+     *
+     * @return string|null
+     */
+    public function getPasswordSalt($method)
+    {
+        if (! Password::hashMethodRequiresSalt($method)) {
+            return;
+        }
+
+        return Password::getSalt($this->password);
+    }
+
+    /**
+     * Determine the password hash method to use from the users current password.
+     *
+     * @return string|void
+     */
+    public function determinePasswordHashMethod()
+    {
+        if (! $password = $this->password) {
+            return;
+        }
+
+        if (! $method = Password::getHashMethod($password)) {
+            return;
+        }
+
+        [,$algo] = array_pad(
+            Password::getHashMethodAndAlgo($password) ?? [],
+            $length = 2,
+            $value = null
+        );
+
+        switch ($algo) {
+            case Password::CRYPT_SALT_TYPE_MD5:
+                return 'md5'.$method;
+            case Password::CRYPT_SALT_TYPE_SHA256:
+                return 'sha256'.$method;
+            case Password::CRYPT_SALT_TYPE_SHA512:
+                return 'sha512'.$method;
+            default:
+                return $method;
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasRelationships.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasRelationships.php
new file mode 100644
index 0000000..a8a5cac
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasRelationships.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use LdapRecord\Models\Relations\HasMany;
+use LdapRecord\Models\Relations\HasManyIn;
+use LdapRecord\Models\Relations\HasOne;
+use LdapRecord\Support\Arr;
+
+trait HasRelationships
+{
+    /**
+     * Returns a new has one relationship.
+     *
+     * @param mixed  $related
+     * @param string $relationKey
+     * @param string $foreignKey
+     *
+     * @return HasOne
+     */
+    public function hasOne($related, $relationKey, $foreignKey = 'dn')
+    {
+        return new HasOne($this->newQuery(), $this, $related, $relationKey, $foreignKey);
+    }
+
+    /**
+     * Returns a new has many relationship.
+     *
+     * @param mixed  $related
+     * @param string $relationKey
+     * @param string $foreignKey
+     *
+     * @return HasMany
+     */
+    public function hasMany($related, $relationKey, $foreignKey = 'dn')
+    {
+        return new HasMany($this->newQuery(), $this, $related, $relationKey, $foreignKey, $this->guessRelationshipName());
+    }
+
+    /**
+     * Returns a new has many in relationship.
+     *
+     * @param mixed  $related
+     * @param string $relationKey
+     * @param string $foreignKey
+     *
+     * @return HasManyIn
+     */
+    public function hasManyIn($related, $relationKey, $foreignKey = 'dn')
+    {
+        return new HasManyIn($this->newQuery(), $this, $related, $relationKey, $foreignKey, $this->guessRelationshipName());
+    }
+
+    /**
+     * Get the relationships name.
+     *
+     * @return string|null
+     */
+    protected function guessRelationshipName()
+    {
+        return Arr::last(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3))['function'];
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasScopes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasScopes.php
new file mode 100644
index 0000000..6c97cf9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasScopes.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+trait HasScopes
+{
+    /**
+     * Begin querying the direct descendants of the model.
+     *
+     * @return \LdapRecord\Query\Model\Builder
+     */
+    public function descendants()
+    {
+        return $this->in($this->getDn())->listing();
+    }
+
+    /**
+     * Begin querying the direct ancestors of the model.
+     *
+     * @return \LdapRecord\Query\Model\Builder
+     */
+    public function ancestors()
+    {
+        $parent = $this->getParentDn($this->getDn());
+
+        return $this->in($this->getParentDn($parent))->listing();
+    }
+
+    /**
+     * Begin querying the direct siblings of the model.
+     *
+     * @return \LdapRecord\Query\Model\Builder
+     */
+    public function siblings()
+    {
+        return $this->in($this->getParentDn($this->getDn()))->listing();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HidesAttributes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HidesAttributes.php
new file mode 100644
index 0000000..9cc2100
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HidesAttributes.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+/**
+ * @author Taylor Otwell
+ *
+ * @see https://laravel.com
+ */
+trait HidesAttributes
+{
+    /**
+     * The attributes that should be hidden for serialization.
+     *
+     * @var array
+     */
+    protected $hidden = [];
+
+    /**
+     * The attributes that should be visible in serialization.
+     *
+     * @var array
+     */
+    protected $visible = [];
+
+    /**
+     * Get the hidden attributes for the model.
+     *
+     * @return array
+     */
+    public function getHidden()
+    {
+        return array_map(function ($key) {
+            return $this->normalizeAttributeKey($key);
+        }, $this->hidden);
+    }
+
+    /**
+     * Set the hidden attributes for the model.
+     *
+     * @param array $hidden
+     *
+     * @return $this
+     */
+    public function setHidden(array $hidden)
+    {
+        $this->hidden = $hidden;
+
+        return $this;
+    }
+
+    /**
+     * Add hidden attributes for the model.
+     *
+     * @param array|string|null $attributes
+     *
+     * @return void
+     */
+    public function addHidden($attributes = null)
+    {
+        $this->hidden = array_merge(
+            $this->hidden,
+            is_array($attributes) ? $attributes : func_get_args()
+        );
+    }
+
+    /**
+     * Get the visible attributes for the model.
+     *
+     * @return array
+     */
+    public function getVisible()
+    {
+        return array_map(function ($key) {
+            return $this->normalizeAttributeKey($key);
+        }, $this->visible);
+    }
+
+    /**
+     * Set the visible attributes for the model.
+     *
+     * @param array $visible
+     *
+     * @return $this
+     */
+    public function setVisible(array $visible)
+    {
+        $this->visible = $visible;
+
+        return $this;
+    }
+
+    /**
+     * Add visible attributes for the model.
+     *
+     * @param array|string|null $attributes
+     *
+     * @return void
+     */
+    public function addVisible($attributes = null)
+    {
+        $this->visible = array_merge(
+            $this->visible,
+            is_array($attributes) ? $attributes : func_get_args()
+        );
+    }
+
+    /**
+     * Make the given, typically hidden, attributes visible.
+     *
+     * @param array|string $attributes
+     *
+     * @return $this
+     */
+    public function makeVisible($attributes)
+    {
+        $this->hidden = array_diff($this->hidden, (array) $attributes);
+
+        if (! empty($this->visible)) {
+            $this->addVisible($attributes);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Make the given, typically visible, attributes hidden.
+     *
+     * @param array|string $attributes
+     *
+     * @return $this
+     */
+    public function makeHidden($attributes)
+    {
+        $attributes = (array) $attributes;
+
+        $this->visible = array_diff($this->visible, $attributes);
+
+        $this->hidden = array_unique(array_merge($this->hidden, $attributes));
+
+        return $this;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DetectsResetIntegers.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DetectsResetIntegers.php
new file mode 100644
index 0000000..8712ef7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DetectsResetIntegers.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord\Models;
+
+trait DetectsResetIntegers
+{
+    /**
+     * Determine if the given value is an LDAP reset integer.
+     *
+     * The integer values '0' and '-1' can be used on certain
+     * LDAP attributes to instruct the server to reset the
+     * value to an 'unset' or 'cleared' state.
+     *
+     * @param mixed $value
+     *
+     * @return bool
+     */
+    protected function valueIsResetInteger($value)
+    {
+        return in_array($value, [0, -1], $strict = true);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Entry.php
new file mode 100644
index 0000000..1bf8325
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Entry.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace LdapRecord\Models\DirectoryServer;
+
+use LdapRecord\Models\Model;
+
+class Entry extends Model
+{
+    /**
+     * The attribute key that contains the models object GUID.
+     *
+     * @var string
+     */
+    protected $guidKey = 'gidNumber';
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Group.php
new file mode 100644
index 0000000..49a6e0a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Group.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace LdapRecord\Models\DirectoryServer;
+
+class Group extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'groupOfUniqueNames',
+        'posixGroup',
+    ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/User.php
new file mode 100644
index 0000000..430588b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/User.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace LdapRecord\Models\DirectoryServer;
+
+class User extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'nsPerson',
+        'nsAccount',
+        'nsOrgPerson',
+        'posixAccount',
+    ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Entry.php
new file mode 100644
index 0000000..dcfda57
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Entry.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models;
+
+class Entry extends Model
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Created.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Created.php
new file mode 100644
index 0000000..c101235
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Created.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Created extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Creating.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Creating.php
new file mode 100644
index 0000000..c4e6ad7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Creating.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Creating extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleted.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleted.php
new file mode 100644
index 0000000..7852659
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleted.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Deleted extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleting.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleting.php
new file mode 100644
index 0000000..9a0810d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleting.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Deleting extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Event.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Event.php
new file mode 100644
index 0000000..20de0b7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Event.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+use LdapRecord\Models\Model;
+
+abstract class Event
+{
+    /**
+     * The model that the event is being triggered on.
+     *
+     * @var Model
+     */
+    protected $model;
+
+    /**
+     * Constructor.
+     *
+     * @param Model $model
+     */
+    public function __construct(Model $model)
+    {
+        $this->model = $model;
+    }
+
+    /**
+     * Returns the model that generated the event.
+     *
+     * @return Model
+     */
+    public function getModel()
+    {
+        return $this->model;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renamed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renamed.php
new file mode 100644
index 0000000..0f02b6d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renamed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Renamed extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renaming.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renaming.php
new file mode 100644
index 0000000..83427ca
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renaming.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+use LdapRecord\Models\Model;
+
+class Renaming extends Event
+{
+    /**
+     * The models RDN.
+     *
+     * @var string
+     */
+    protected $rdn;
+
+    /**
+     * The models new parent DN.
+     *
+     * @var string
+     */
+    protected $newParentDn;
+
+    /**
+     * Constructor.
+     *
+     * @param Model  $model
+     * @param string $rdn
+     * @param string $newParentDn
+     */
+    public function __construct(Model $model, $rdn, $newParentDn)
+    {
+        parent::__construct($model);
+
+        $this->rdn = $rdn;
+        $this->newParentDn = $newParentDn;
+    }
+
+    /**
+     * Get the models RDN.
+     *
+     * @return string
+     */
+    public function getRdn()
+    {
+        return $this->rdn;
+    }
+
+    /**
+     * Get the models parent DN.
+     *
+     * @return string
+     */
+    public function getNewParentDn()
+    {
+        return $this->newParentDn;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saved.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saved.php
new file mode 100644
index 0000000..cf9c5ad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saved.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Saved extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saving.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saving.php
new file mode 100644
index 0000000..0c99403
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saving.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Saving extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updated.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updated.php
new file mode 100644
index 0000000..b0dd611
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updated.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Updated extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updating.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updating.php
new file mode 100644
index 0000000..20ae60c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updating.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Updating extends Event
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Entry.php
new file mode 100644
index 0000000..7fdda9c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Entry.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA;
+
+use LdapRecord\Connection;
+use LdapRecord\Models\Entry as BaseEntry;
+use LdapRecord\Models\FreeIPA\Scopes\AddEntryUuidToSelects;
+use LdapRecord\Models\Types\FreeIPA;
+use LdapRecord\Query\Model\FreeIpaBuilder;
+
+/** @mixin FreeIpaBuilder */
+class Entry extends BaseEntry implements FreeIPA
+{
+    /**
+     * The attribute key that contains the models object GUID.
+     *
+     * @var string
+     */
+    protected $guidKey = 'ipauniqueid';
+
+    /**
+     * The default attributes that should be mutated to dates.
+     *
+     * @var array
+     */
+    protected $defaultDates = [
+        'krblastpwdchange' => 'ldap',
+        'krbpasswordexpiration' => 'ldap',
+    ];
+
+    /**
+     * @inheritdoc
+     */
+    protected static function boot()
+    {
+        parent::boot();
+
+        // Here we'll add a global scope to all FreeIPA models to ensure the
+        // Entry UUID is always selected on each query. This attribute is
+        // virtual, so it must be manually selected to be included.
+        static::addGlobalScope(new AddEntryUuidToSelects());
+    }
+
+    /**
+     * Create a new query builder.
+     *
+     * @param Connection $connection
+     *
+     * @return FreeIpaBuilder
+     */
+    public function newQueryBuilder(Connection $connection)
+    {
+        return new FreeIpaBuilder($connection);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Group.php
new file mode 100644
index 0000000..10fd934
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Group.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA;
+
+class Group extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'groupofnames',
+        'nestedgroup',
+        'ipausergroup',
+        'posixgroup',
+    ];
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the current group is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(self::class, 'member');
+    }
+
+    /**
+     * Retrieve the members of the group.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function members()
+    {
+        return $this->hasMany(User::class, 'memberof')->using($this, 'member');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Scopes/AddEntryUuidToSelects.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Scopes/AddEntryUuidToSelects.php
new file mode 100644
index 0000000..039c05e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Scopes/AddEntryUuidToSelects.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class AddEntryUuidToSelects implements Scope
+{
+    /**
+     * Add the entry UUID to the selected attributes.
+     *
+     * @param Builder $query
+     * @param Model   $model
+     *
+     * @return void
+     */
+    public function apply(Builder $query, Model $model)
+    {
+        empty($query->columns)
+            ? $query->addSelect(['*', $model->getGuidKey()])
+            : $query->addSelect($model->getGuidKey());
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/User.php
new file mode 100644
index 0000000..24c7f3b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/User.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA;
+
+class User extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'person',
+        'inetorgperson',
+        'organizationalperson',
+    ];
+
+    /**
+     * Retrieve groups that the current user is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(Group::class, 'member');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Model.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Model.php
new file mode 100644
index 0000000..6ba24b4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Model.php
@@ -0,0 +1,1441 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use ArrayAccess;
+use InvalidArgumentException;
+use JsonSerializable;
+use LdapRecord\Connection;
+use LdapRecord\Container;
+use LdapRecord\EscapesValues;
+use LdapRecord\Models\Attributes\DistinguishedName;
+use LdapRecord\Models\Attributes\Guid;
+use LdapRecord\Models\Events\Renamed;
+use LdapRecord\Models\Events\Renaming;
+use LdapRecord\Query\Model\Builder;
+use LdapRecord\Support\Arr;
+use UnexpectedValueException;
+
+/** @mixin Builder */
+abstract class Model implements ArrayAccess, JsonSerializable
+{
+    use EscapesValues;
+    use Concerns\HasEvents;
+    use Concerns\HasScopes;
+    use Concerns\HasAttributes;
+    use Concerns\HasGlobalScopes;
+    use Concerns\HidesAttributes;
+    use Concerns\HasRelationships;
+
+    /**
+     * Indicates if the model exists in the LDAP directory.
+     *
+     * @var bool
+     */
+    public $exists = false;
+
+    /**
+     * Indicates whether the model was created during the current request lifecycle.
+     *
+     * @var bool
+     */
+    public $wasRecentlyCreated = false;
+
+    /**
+     * Indicates whether the model was renamed during the current request lifecycle.
+     *
+     * @var bool
+     */
+    public $wasRecentlyRenamed = false;
+
+    /**
+     * The models distinguished name.
+     *
+     * @var string|null
+     */
+    protected $dn;
+
+    /**
+     * The base DN of where the model should be created in.
+     *
+     * @var string|null
+     */
+    protected $in;
+
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [];
+
+    /**
+     * The connection container instance.
+     *
+     * @var Container
+     */
+    protected static $container;
+
+    /**
+     * The LDAP connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection;
+
+    /**
+     * The attribute key that contains the models object GUID.
+     *
+     * @var string
+     */
+    protected $guidKey = 'objectguid';
+
+    /**
+     * Contains the models modifications.
+     *
+     * @var array
+     */
+    protected $modifications = [];
+
+    /**
+     * The array of global scopes on the model.
+     *
+     * @var array
+     */
+    protected static $globalScopes = [];
+
+    /**
+     * The array of booted models.
+     *
+     * @var array
+     */
+    protected static $booted = [];
+
+    /**
+     * Constructor.
+     *
+     * @param array $attributes
+     */
+    public function __construct(array $attributes = [])
+    {
+        $this->bootIfNotBooted();
+
+        $this->fill($attributes);
+    }
+
+    /**
+     * Check if the model needs to be booted and if so, do it.
+     *
+     * @return void
+     */
+    protected function bootIfNotBooted()
+    {
+        if (! isset(static::$booted[static::class])) {
+            static::$booted[static::class] = true;
+
+            static::boot();
+        }
+    }
+
+    /**
+     * The "booting" method of the model.
+     *
+     * @return void
+     */
+    protected static function boot()
+    {
+        //
+    }
+
+    /**
+     * Clear the list of booted models so they will be re-booted.
+     *
+     * @return void
+     */
+    public static function clearBootedModels()
+    {
+        static::$booted = [];
+
+        static::$globalScopes = [];
+    }
+
+    /**
+     * Handle dynamic method calls into the model.
+     *
+     * @param string $method
+     * @param array  $parameters
+     *
+     * @return mixed
+     */
+    public function __call($method, $parameters)
+    {
+        if (method_exists($this, $method)) {
+            return $this->$method(...$parameters);
+        }
+
+        return $this->newQuery()->$method(...$parameters);
+    }
+
+    /**
+     * Handle dynamic static method calls into the method.
+     *
+     * @param string $method
+     * @param array  $parameters
+     *
+     * @return mixed
+     */
+    public static function __callStatic($method, $parameters)
+    {
+        return (new static())->$method(...$parameters);
+    }
+
+    /**
+     * Returns the models distinguished name.
+     *
+     * @return string|null
+     */
+    public function getDn()
+    {
+        return $this->dn;
+    }
+
+    /**
+     * Set the models distinguished name.
+     *
+     * @param string $dn
+     *
+     * @return static
+     */
+    public function setDn($dn)
+    {
+        $this->dn = (string) $dn;
+
+        return $this;
+    }
+
+    /**
+     * Get the LDAP connection for the model.
+     *
+     * @return Connection
+     */
+    public function getConnection()
+    {
+        return static::resolveConnection($this->getConnectionName());
+    }
+
+    /**
+     * Get the current connection name for the model.
+     *
+     * @return string
+     */
+    public function getConnectionName()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * Set the connection associated with the model.
+     *
+     * @param string $name
+     *
+     * @return $this
+     */
+    public function setConnection($name)
+    {
+        $this->connection = $name;
+
+        return $this;
+    }
+
+    /**
+     * Begin querying the model on a given connection.
+     *
+     * @param string|null $connection
+     *
+     * @return Builder
+     */
+    public static function on($connection = null)
+    {
+        $instance = new static();
+
+        $instance->setConnection($connection);
+
+        return $instance->newQuery();
+    }
+
+    /**
+     * Get all the models from the directory.
+     *
+     * @param array|mixed $attributes
+     *
+     * @return Collection|static[]
+     */
+    public static function all($attributes = ['*'])
+    {
+        return static::query()->select($attributes)->paginate();
+    }
+
+    /**
+     * Begin querying the model.
+     *
+     * @return Builder
+     */
+    public static function query()
+    {
+        return (new static())->newQuery();
+    }
+
+    /**
+     * Get a new query for builder filtered by the current models object classes.
+     *
+     * @return Builder
+     */
+    public function newQuery()
+    {
+        return $this->registerModelScopes(
+            $this->newQueryWithoutScopes()
+        );
+    }
+
+    /**
+     * Get a new query builder that doesn't have any global scopes.
+     *
+     * @return Builder
+     */
+    public function newQueryWithoutScopes()
+    {
+        return static::resolveConnection(
+            $this->getConnectionName()
+        )->query()->model($this);
+    }
+
+    /**
+     * Create a new query builder.
+     *
+     * @param Connection $connection
+     *
+     * @return Builder
+     */
+    public function newQueryBuilder(Connection $connection)
+    {
+        return new Builder($connection);
+    }
+
+    /**
+     * Create a new model instance.
+     *
+     * @param array $attributes
+     *
+     * @return static
+     */
+    public function newInstance(array $attributes = [])
+    {
+        return (new static($attributes))->setConnection($this->getConnectionName());
+    }
+
+    /**
+     * Resolve a connection instance.
+     *
+     * @param string|null $connection
+     *
+     * @return Connection
+     */
+    public static function resolveConnection($connection = null)
+    {
+        return static::getConnectionContainer()->get($connection);
+    }
+
+    /**
+     * Get the connection container.
+     *
+     * @return Container
+     */
+    public static function getConnectionContainer()
+    {
+        return static::$container ?? static::getDefaultConnectionContainer();
+    }
+
+    /**
+     * Get the default singleton container instance.
+     *
+     * @return Container
+     */
+    public static function getDefaultConnectionContainer()
+    {
+        return Container::getInstance();
+    }
+
+    /**
+     * Set the connection container.
+     *
+     * @param Container $container
+     *
+     * @return void
+     */
+    public static function setConnectionContainer(Container $container)
+    {
+        static::$container = $container;
+    }
+
+    /**
+     * Unset the connection container.
+     *
+     * @return void
+     */
+    public static function unsetConnectionContainer()
+    {
+        static::$container = null;
+    }
+
+    /**
+     * Register the query scopes for this builder instance.
+     *
+     * @param Builder $builder
+     *
+     * @return Builder
+     */
+    public function registerModelScopes($builder)
+    {
+        $this->applyObjectClassScopes($builder);
+
+        $this->registerGlobalScopes($builder);
+
+        return $builder;
+    }
+
+    /**
+     * Register the global model scopes.
+     *
+     * @param Builder $builder
+     *
+     * @return Builder
+     */
+    public function registerGlobalScopes($builder)
+    {
+        foreach ($this->getGlobalScopes() as $identifier => $scope) {
+            $builder->withGlobalScope($identifier, $scope);
+        }
+
+        return $builder;
+    }
+
+    /**
+     * Apply the model object class scopes to the given builder instance.
+     *
+     * @param Builder $query
+     *
+     * @return void
+     */
+    public function applyObjectClassScopes(Builder $query)
+    {
+        foreach (static::$objectClasses as $objectClass) {
+            $query->where('objectclass', '=', $objectClass);
+        }
+    }
+
+    /**
+     * Returns the models distinguished name when the model is converted to a string.
+     *
+     * @return null|string
+     */
+    public function __toString()
+    {
+        return $this->getDn();
+    }
+
+    /**
+     * Returns a new batch modification.
+     *
+     * @param string|null     $attribute
+     * @param string|int|null $type
+     * @param array           $values
+     *
+     * @return BatchModification
+     */
+    public function newBatchModification($attribute = null, $type = null, $values = [])
+    {
+        return new BatchModification($attribute, $type, $values);
+    }
+
+    /**
+     * Returns a new collection with the specified items.
+     *
+     * @param mixed $items
+     *
+     * @return Collection
+     */
+    public function newCollection($items = [])
+    {
+        return new Collection($items);
+    }
+
+    /**
+     * Dynamically retrieve attributes on the object.
+     *
+     * @param mixed $key
+     *
+     * @return bool
+     */
+    public function __get($key)
+    {
+        return $this->getAttribute($key);
+    }
+
+    /**
+     * Dynamically set attributes on the object.
+     *
+     * @param mixed $key
+     * @param mixed $value
+     *
+     * @return $this
+     */
+    public function __set($key, $value)
+    {
+        return $this->setAttribute($key, $value);
+    }
+
+    /**
+     * Determine if the given offset exists.
+     *
+     * @param string $offset
+     *
+     * @return bool
+     */
+    public function offsetExists($offset)
+    {
+        return ! is_null($this->getAttribute($offset));
+    }
+
+    /**
+     * Get the value for a given offset.
+     *
+     * @param string $offset
+     *
+     * @return mixed
+     */
+    public function offsetGet($offset)
+    {
+        return $this->getAttribute($offset);
+    }
+
+    /**
+     * Set the value at the given offset.
+     *
+     * @param string $offset
+     * @param mixed  $value
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value)
+    {
+        $this->setAttribute($offset, $value);
+    }
+
+    /**
+     * Unset the value at the given offset.
+     *
+     * @param string $offset
+     *
+     * @return void
+     */
+    public function offsetUnset($offset)
+    {
+        unset($this->attributes[$offset]);
+    }
+
+    /**
+     * Determine if an attribute exists on the model.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function __isset($key)
+    {
+        return $this->offsetExists($key);
+    }
+
+    /**
+     * Unset an attribute on the model.
+     *
+     * @param string $key
+     *
+     * @return void
+     */
+    public function __unset($key)
+    {
+        $this->offsetUnset($key);
+    }
+
+    /**
+     * Convert the object into something JSON serializable.
+     *
+     * @return array
+     */
+    public function jsonSerialize()
+    {
+        return $this->attributesToArray();
+    }
+
+    /**
+     * Converts extra attributes for JSON serialization.
+     *
+     * @param array $attributes
+     *
+     * @return array
+     */
+    protected function convertAttributesForJson(array $attributes = [])
+    {
+        // If the model has a GUID set, we need to convert
+        // it due to it being in binary. Otherwise we'll
+        // receive a JSON serialization exception.
+        if ($this->hasAttribute($this->guidKey)) {
+            return array_replace($attributes, [
+                $this->guidKey => [$this->getConvertedGuid()],
+            ]);
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Reload a fresh model instance from the directory.
+     *
+     * @return static|false
+     */
+    public function fresh()
+    {
+        if (! $this->exists) {
+            return false;
+        }
+
+        return $this->newQuery()->find($this->dn);
+    }
+
+    /**
+     * Determine if two models have the same distinguished name and belong to the same connection.
+     *
+     * @param static $model
+     *
+     * @return bool
+     */
+    public function is(self $model)
+    {
+        return $this->dn == $model->getDn() && $this->getConnectionName() == $model->getConnectionName();
+    }
+
+    /**
+     * Hydrate a new collection of models from LDAP search results.
+     *
+     * @param array $records
+     *
+     * @return Collection
+     */
+    public function hydrate($records)
+    {
+        return $this->newCollection($records)->transform(function ($attributes) {
+            return $attributes instanceof static
+                ? $attributes
+                : static::newInstance()->setRawAttributes($attributes);
+        });
+    }
+
+    /**
+     * Converts the current model into the given model.
+     *
+     * @param Model $into
+     *
+     * @return Model
+     */
+    public function convert(self $into)
+    {
+        $into->setDn($this->getDn());
+        $into->setConnection($this->getConnectionName());
+
+        $this->exists
+            ? $into->setRawAttributes($this->getAttributes())
+            : $into->fill($this->getAttributes());
+
+        return $into;
+    }
+
+    /**
+     * Refreshes the current models attributes with the directory values.
+     *
+     * @return bool
+     */
+    public function refresh()
+    {
+        if ($model = $this->fresh()) {
+            $this->setRawAttributes($model->getAttributes());
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the model's batch modifications to be processed.
+     *
+     * @return array
+     */
+    public function getModifications()
+    {
+        $builtModifications = [];
+
+        foreach ($this->buildModificationsFromDirty() as $modification) {
+            $builtModifications[] = $modification->get();
+        }
+
+        return array_merge($this->modifications, $builtModifications);
+    }
+
+    /**
+     * Set the models batch modifications.
+     *
+     * @param array $modifications
+     *
+     * @return $this
+     */
+    public function setModifications(array $modifications = [])
+    {
+        $this->modifications = [];
+
+        foreach ($modifications as $modification) {
+            $this->addModification($modification);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Adds a batch modification to the model.
+     *
+     * @param array|BatchModification $mod
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return $this
+     */
+    public function addModification($mod = [])
+    {
+        if ($mod instanceof BatchModification) {
+            $mod = $mod->get();
+        }
+
+        if ($this->isValidModification($mod)) {
+            $this->modifications[] = $mod;
+
+            return $this;
+        }
+
+        throw new InvalidArgumentException(
+            "The batch modification array does not include the mandatory 'attrib' or 'modtype' keys."
+        );
+    }
+
+    /**
+     * Get the model's guid attribute key name.
+     *
+     * @return string
+     */
+    public function getGuidKey()
+    {
+        return $this->guidKey;
+    }
+
+    /**
+     * Get the model's ANR attributes for querying when incompatible with ANR.
+     *
+     * @return array
+     */
+    public function getAnrAttributes()
+    {
+        return ['cn', 'sn', 'uid', 'name', 'mail', 'givenname', 'displayname'];
+    }
+
+    /**
+     * Get the name of the model, or the given DN.
+     *
+     * @param string|null $dn
+     *
+     * @return string|null
+     */
+    public function getName($dn = null)
+    {
+        return $this->newDn($dn ?? $this->dn)->name();
+    }
+
+    /**
+     * Get the head attribute of the model, or the given DN.
+     *
+     * @param string|null $dn
+     *
+     * @return string|null
+     */
+    public function getHead($dn = null)
+    {
+        return $this->newDn($dn ?? $this->dn)->head();
+    }
+
+    /**
+     * Get the RDN of the model, of the given DN.
+     *
+     * @param string|null
+     *
+     * @return string|null
+     */
+    public function getRdn($dn = null)
+    {
+        return $this->newDn($dn ?? $this->dn)->relative();
+    }
+
+    /**
+     * Get the parent distinguished name of the model, or the given DN.
+     *
+     * @param string|null
+     *
+     * @return string|null
+     */
+    public function getParentDn($dn = null)
+    {
+        return $this->newDn($dn ?? $this->dn)->parent();
+    }
+
+    /**
+     * Create a new Distinguished Name object.
+     *
+     * @param string|null $dn
+     *
+     * @return DistinguishedName
+     */
+    public function newDn($dn = null)
+    {
+        return new DistinguishedName($dn);
+    }
+
+    /**
+     * Get the model's object GUID key.
+     *
+     * @return void
+     */
+    public function getObjectGuidKey()
+    {
+        return $this->guidKey;
+    }
+
+    /**
+     * Get the model's binary object GUID.
+     *
+     * @see https://msdn.microsoft.com/en-us/library/ms679021(v=vs.85).aspx
+     *
+     * @return string|null
+     */
+    public function getObjectGuid()
+    {
+        return $this->getFirstAttribute($this->guidKey);
+    }
+
+    /**
+     * Get the model's object classes.
+     *
+     * @return array
+     */
+    public function getObjectClasses()
+    {
+        return $this->getAttribute('objectclass') ?: [];
+    }
+
+    /**
+     * Get the model's string GUID.
+     *
+     * @return string|null
+     */
+    public function getConvertedGuid()
+    {
+        try {
+            return (string) new Guid($this->getObjectGuid());
+        } catch (InvalidArgumentException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Determine if the current model is a direct descendant of the given.
+     *
+     * @param static|string $parent
+     *
+     * @return bool
+     */
+    public function isChildOf($parent)
+    {
+        return $this->newDn($this->getDn())->isChildOf(
+            $this->newDn((string) $parent)
+        );
+    }
+
+    /**
+     * Determine if the current model is a direct ascendant of the given.
+     *
+     * @param static|string $child
+     *
+     * @return bool
+     */
+    public function isParentOf($child)
+    {
+        return $this->newDn($this->getDn())->isParentOf(
+            $this->newDn((string) $child)
+        );
+    }
+
+    /**
+     * Determine if the current model is a descendant of the given.
+     *
+     * @param static|string $model
+     *
+     * @return bool
+     */
+    public function isDescendantOf($model)
+    {
+        return $this->dnIsInside($this->getDn(), $model);
+    }
+
+    /**
+     * Determine if the current model is a ancestor of the given.
+     *
+     * @param static|string $model
+     *
+     * @return bool
+     */
+    public function isAncestorOf($model)
+    {
+        return $this->dnIsInside($model, $this->getDn());
+    }
+
+    /**
+     * Determines if the DN is inside of the parent DN.
+     *
+     * @param static|string $dn
+     * @param static|string $parentDn
+     *
+     * @return bool
+     */
+    protected function dnIsInside($dn, $parentDn)
+    {
+        return $this->newDn((string) $dn)->isDescendantOf(
+            $this->newDn($parentDn)
+        );
+    }
+
+    /**
+     * Set the base DN of where the model should be created in.
+     *
+     * @param static|string $dn
+     *
+     * @return $this
+     */
+    public function inside($dn)
+    {
+        $this->in = $dn instanceof self ? $dn->getDn() : $dn;
+
+        return $this;
+    }
+
+    /**
+     * Save the model to the directory.
+     *
+     * @param array $attributes The attributes to update or create for the current entry.
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function save(array $attributes = [])
+    {
+        $this->fill($attributes);
+
+        $this->fireModelEvent(new Events\Saving($this));
+
+        $this->exists ? $this->performUpdate() : $this->performInsert();
+
+        $this->fireModelEvent(new Events\Saved($this));
+
+        $this->in = null;
+    }
+
+    /**
+     * Inserts the model into the directory.
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    protected function performInsert()
+    {
+        // Here we will populate the models object classes if it
+        // does not already have any set. An LDAP object cannot
+        // be successfully created in the server without them.
+        if (! $this->hasAttribute('objectclass')) {
+            $this->setAttribute('objectclass', static::$objectClasses);
+        }
+
+        $query = $this->newQuery();
+
+        // If the model does not currently have a distinguished
+        // name, we will attempt to generate one automatically
+        // using the current query builder's DN as the base.
+        if (empty($this->getDn())) {
+            $this->setDn($this->getCreatableDn());
+        }
+
+        $this->fireModelEvent(new Events\Creating($this));
+
+        // Here we perform the insert of new object in the directory,
+        // but filter out any empty attributes before sending them
+        // to the server. LDAP servers will throw an exception if
+        // attributes have been given empty or null values.
+        $query->insert($this->getDn(), array_filter($this->getAttributes()));
+
+        $this->fireModelEvent(new Events\Created($this));
+
+        $this->syncOriginal();
+
+        $this->exists = true;
+
+        $this->wasRecentlyCreated = true;
+    }
+
+    /**
+     * Updates the model in the directory.
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    protected function performUpdate()
+    {
+        if (! count($modifications = $this->getModifications())) {
+            return;
+        }
+
+        $this->fireModelEvent(new Events\Updating($this));
+
+        $this->newQuery()->update($this->dn, $modifications);
+
+        $this->fireModelEvent(new Events\Updated($this));
+
+        $this->syncOriginal();
+
+        $this->modifications = [];
+    }
+
+    /**
+     * Create the model in the directory.
+     *
+     * @param array $attributes The attributes for the new entry.
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return Model
+     */
+    public static function create(array $attributes = [])
+    {
+        $instance = new static($attributes);
+
+        $instance->save();
+
+        return $instance;
+    }
+
+    /**
+     * Create an attribute on the model.
+     *
+     * @param string $attribute The attribute to create
+     * @param mixed  $value     The value of the new attribute
+     *
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function createAttribute($attribute, $value)
+    {
+        $this->validateExistence();
+
+        $this->newQuery()->insertAttributes($this->dn, [$attribute => (array) $value]);
+
+        $this->addAttributeValue($attribute, $value);
+    }
+
+    /**
+     * Update the model.
+     *
+     * @param array $attributes The attributes to update for the current entry.
+     *
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function update(array $attributes = [])
+    {
+        $this->validateExistence();
+
+        $this->save($attributes);
+    }
+
+    /**
+     * Update the model attribute with the specified value.
+     *
+     * @param string $attribute The attribute to modify
+     * @param mixed  $value     The new value for the attribute
+     *
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function updateAttribute($attribute, $value)
+    {
+        $this->validateExistence();
+
+        $this->newQuery()->updateAttributes($this->dn, [$attribute => (array) $value]);
+
+        $this->addAttributeValue($attribute, $value);
+    }
+
+    /**
+     * Destroy the models for the given distinguished names.
+     *
+     * @param Collection|array|string $dns
+     * @param bool                    $recursive
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return int
+     */
+    public static function destroy($dns, $recursive = false)
+    {
+        $count = 0;
+
+        $dns = is_string($dns) ? (array) $dns : $dns;
+
+        $instance = new static();
+
+        foreach ($dns as $dn) {
+            if (! $model = $instance->find($dn)) {
+                continue;
+            }
+
+            $model->delete($recursive);
+
+            $count++;
+        }
+
+        return $count;
+    }
+
+    /**
+     * Delete the model from the directory.
+     *
+     * Throws a ModelNotFoundException if the current model does
+     * not exist or does not contain a distinguished name.
+     *
+     * @param bool $recursive Whether to recursively delete leaf nodes (models that are children).
+     *
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function delete($recursive = false)
+    {
+        $this->validateExistence();
+
+        $this->fireModelEvent(new Events\Deleting($this));
+
+        if ($recursive) {
+            $this->deleteLeafNodes();
+        }
+
+        $this->newQuery()->delete($this->dn);
+
+        // If the deletion is successful, we will mark the model
+        // as non-existing, and then fire the deleted event so
+        // developers can hook in and run further operations.
+        $this->exists = false;
+
+        $this->fireModelEvent(new Events\Deleted($this));
+    }
+
+    /**
+     * Deletes leaf nodes that are attached to the model.
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return Collection
+     */
+    protected function deleteLeafNodes()
+    {
+        return $this->newQueryWithoutScopes()
+            ->in($this->dn)
+            ->listing()
+            ->paginate()
+            ->each(function (self $model) {
+                $model->delete($recursive = true);
+            });
+    }
+
+    /**
+     * Delete an attribute on the model.
+     *
+     * @param string|array $attributes The attribute(s) to delete
+     *
+     * Delete specific values in attributes:
+     *
+     *     ["memberuid" => "jdoe"]
+     *
+     * Delete an entire attribute:
+     *
+     *     ["memberuid" => []]
+     *
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function deleteAttribute($attributes)
+    {
+        $this->validateExistence();
+
+        $attributes = $this->makeDeletableAttributes($attributes);
+
+        $this->newQuery()->deleteAttributes($this->dn, $attributes);
+
+        foreach ($attributes as $attribute => $value) {
+            // If the attribute value is empty, we can assume the
+            // attribute was completely deleted from the model.
+            // We will pull the attribute out and continue on.
+            if (empty($value)) {
+                unset($this->attributes[$attribute]);
+            }
+            // Otherwise, only specific attribute values have been
+            // removed. We will determine which ones have been
+            // removed and update the attributes value.
+            elseif (Arr::exists($this->attributes, $attribute)) {
+                $this->attributes[$attribute] = array_values(
+                    array_diff($this->attributes[$attribute], (array) $value)
+                );
+            }
+        }
+
+        $this->syncOriginal();
+    }
+
+    /**
+     * Make a deletable attribute array.
+     *
+     * @param string|array $attributes
+     *
+     * @return array
+     */
+    protected function makeDeletableAttributes($attributes)
+    {
+        $delete = [];
+
+        foreach (Arr::wrap($attributes) as $key => $value) {
+            is_int($key)
+                ? $delete[$value] = []
+                : $delete[$key] = Arr::wrap($value);
+        }
+
+        return $delete;
+    }
+
+    /**
+     * Move the model into the given new parent.
+     *
+     * For example: $user->move($ou);
+     *
+     * @param static|string $newParentDn  The new parent of the current model.
+     * @param bool          $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
+     *
+     * @throws UnexpectedValueException
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function move($newParentDn, $deleteOldRdn = true)
+    {
+        $this->validateExistence();
+
+        if (! $rdn = $this->getRdn()) {
+            throw new UnexpectedValueException('Current model does not contain an RDN to move.');
+        }
+
+        $this->rename($rdn, $newParentDn, $deleteOldRdn);
+    }
+
+    /**
+     * Rename the model to a new RDN and new parent.
+     *
+     * @param string             $rdn          The models new relative distinguished name. Example: "cn=JohnDoe"
+     * @param static|string|null $newParentDn  The models new parent distinguished name (if moving). Leave this null if you are only renaming. Example: "ou=MovedUsers,dc=acme,dc=org"
+     * @param bool|true          $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
+     *
+     * @throws ModelDoesNotExistException
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function rename($rdn, $newParentDn = null, $deleteOldRdn = true)
+    {
+        $this->validateExistence();
+
+        if ($newParentDn instanceof self) {
+            $newParentDn = $newParentDn->getDn();
+        }
+
+        if (is_null($newParentDn)) {
+            $newParentDn = $this->getParentDn($this->dn);
+        }
+
+        // If the RDN and the new parent DN are the same as the current,
+        // we will simply return here to prevent a rename operation
+        // being sent, which would fail anyway in such case.
+        if (
+            $rdn === $this->getRdn()
+         && $newParentDn === $this->getParentDn()
+        ) {
+            return;
+        }
+
+        $this->fireModelEvent(new Renaming($this, $rdn, $newParentDn));
+
+        $this->newQuery()->rename($this->dn, $rdn, $newParentDn, $deleteOldRdn);
+
+        // If the model was successfully renamed, we will set
+        // its new DN so any further updates to the model
+        // can be performed without any issues.
+        $this->dn = implode(',', [$rdn, $newParentDn]);
+
+        $map = $this->newDn($this->dn)->assoc();
+
+        // Here we'll populate the models new primary
+        // RDN attribute on the model so we do not
+        // have to re-synchronize with the server.
+        $modelNameAttribute = key($map);
+
+        $this->attributes[$modelNameAttribute]
+            = $this->original[$modelNameAttribute]
+            = [reset($map[$modelNameAttribute])];
+
+        $this->fireModelEvent(new Renamed($this));
+
+        $this->wasRecentlyRenamed = true;
+    }
+
+    /**
+     * Get a distinguished name that is creatable for the model.
+     *
+     * @param string|null $name
+     * @param string|null $attribute
+     *
+     * @return string
+     */
+    public function getCreatableDn($name = null, $attribute = null)
+    {
+        return implode(',', [
+            $this->getCreatableRdn($name, $attribute),
+            $this->in ?? $this->newQuery()->getbaseDn(),
+        ]);
+    }
+
+    /**
+     * Get a creatable (escaped) RDN for the model.
+     *
+     * @param string|null $name
+     * @param string|null $attribute
+     *
+     * @return string
+     */
+    public function getCreatableRdn($name = null, $attribute = null)
+    {
+        $attribute = $attribute ?? $this->getCreatableRdnAttribute();
+
+        $name = $this->escape(
+            $name ?? $this->getFirstAttribute($attribute)
+        )->dn();
+
+        return "$attribute=$name";
+    }
+
+    /**
+     * Get the creatable RDN attribute name.
+     *
+     * @return string
+     */
+    protected function getCreatableRdnAttribute()
+    {
+        return 'cn';
+    }
+
+    /**
+     * Determines if the given modification is valid.
+     *
+     * @param mixed $mod
+     *
+     * @return bool
+     */
+    protected function isValidModification($mod)
+    {
+        return Arr::accessible($mod)
+            && Arr::exists($mod, BatchModification::KEY_MODTYPE)
+            && Arr::exists($mod, BatchModification::KEY_ATTRIB);
+    }
+
+    /**
+     * Builds the models modifications from its dirty attributes.
+     *
+     * @return BatchModification[]
+     */
+    protected function buildModificationsFromDirty()
+    {
+        $modifications = [];
+
+        foreach ($this->getDirty() as $attribute => $values) {
+            $modification = $this->newBatchModification($attribute, null, (array) $values);
+
+            if (Arr::exists($this->original, $attribute)) {
+                // If the attribute we're modifying has an original value, we will
+                // give the BatchModification object its values to automatically
+                // determine which type of LDAP operation we need to perform.
+                $modification->setOriginal($this->original[$attribute]);
+            }
+
+            if (! $modification->build()->isValid()) {
+                continue;
+            }
+
+            $modifications[] = $modification;
+        }
+
+        return $modifications;
+    }
+
+    /**
+     * Validates that the current model exists.
+     *
+     * @throws ModelDoesNotExistException
+     *
+     * @return void
+     */
+    protected function validateExistence()
+    {
+        if (! $this->exists || is_null($this->dn)) {
+            throw ModelDoesNotExistException::forModel($this);
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelDoesNotExistException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelDoesNotExistException.php
new file mode 100644
index 0000000..2dd2ba9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelDoesNotExistException.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use LdapRecord\LdapRecordException;
+
+class ModelDoesNotExistException extends LdapRecordException
+{
+    /**
+     * The class name of the model that does not exist.
+     *
+     * @var Model
+     */
+    protected $model;
+
+    /**
+     * Create a new exception for the given model.
+     *
+     * @param Model $model
+     *
+     * @return ModelDoesNotExistException
+     */
+    public static function forModel(Model $model)
+    {
+        return (new static())->setModel($model);
+    }
+
+    /**
+     * Set the model that does not exist.
+     *
+     * @param Model $model
+     *
+     * @return ModelDoesNotExistException
+     */
+    public function setModel(Model $model)
+    {
+        $this->model = $model;
+
+        $class = get_class($model);
+
+        $this->message = "Model [{$class}] does not exist.";
+
+        return $this;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelNotFoundException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelNotFoundException.php
new file mode 100644
index 0000000..be88bab
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelNotFoundException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use LdapRecord\Query\ObjectNotFoundException;
+
+class ModelNotFoundException extends ObjectNotFoundException
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Entry.php
new file mode 100644
index 0000000..b7ad37a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Entry.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+use LdapRecord\Connection;
+use LdapRecord\Models\Entry as BaseEntry;
+use LdapRecord\Models\OpenLDAP\Scopes\AddEntryUuidToSelects;
+use LdapRecord\Models\Types\OpenLDAP;
+use LdapRecord\Query\Model\OpenLdapBuilder;
+
+/** @mixin OpenLdapBuilder */
+class Entry extends BaseEntry implements OpenLDAP
+{
+    /**
+     * The attribute key that contains the models object GUID.
+     *
+     * @var string
+     */
+    protected $guidKey = 'entryuuid';
+
+    /**
+     * @inheritdoc
+     */
+    protected static function boot()
+    {
+        parent::boot();
+
+        // Here we'll add a global scope to all OpenLDAP models to ensure the
+        // Entry UUID is always selected on each query. This attribute is
+        // virtual, so it must be manually selected to be included.
+        static::addGlobalScope(new AddEntryUuidToSelects());
+    }
+
+    /**
+     * Create a new query builder.
+     *
+     * @param Connection $connection
+     *
+     * @return OpenLdapBuilder
+     */
+    public function newQueryBuilder(Connection $connection)
+    {
+        return new OpenLdapBuilder($connection);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Group.php
new file mode 100644
index 0000000..2d8d94e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Group.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+class Group extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'groupofuniquenames',
+    ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/OrganizationalUnit.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/OrganizationalUnit.php
new file mode 100644
index 0000000..7ae0a37
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/OrganizationalUnit.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+class OrganizationalUnit extends Entry
+{
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'organizationalunit',
+    ];
+
+    /**
+     * Get the creatable RDN attribute name.
+     *
+     * @return string
+     */
+    public function getCreatableRdnAttribute()
+    {
+        return 'ou';
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Scopes/AddEntryUuidToSelects.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Scopes/AddEntryUuidToSelects.php
new file mode 100644
index 0000000..54376c2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Scopes/AddEntryUuidToSelects.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class AddEntryUuidToSelects implements Scope
+{
+    /**
+     * Add the entry UUID to the selected attributes.
+     *
+     * @param Builder $query
+     * @param Model   $model
+     *
+     * @return void
+     */
+    public function apply(Builder $query, Model $model)
+    {
+        empty($query->columns)
+            ? $query->addSelect(['*', $model->getGuidKey()])
+            : $query->addSelect($model->getGuidKey());
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/User.php
new file mode 100644
index 0000000..b37f390
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/User.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+use Illuminate\Contracts\Auth\Authenticatable;
+use LdapRecord\Models\Concerns\CanAuthenticate;
+use LdapRecord\Models\Concerns\HasPassword;
+
+class User extends Entry implements Authenticatable
+{
+    use HasPassword;
+    use CanAuthenticate;
+
+    /**
+     * The password's attribute name.
+     *
+     * @var string
+     */
+    protected $passwordAttribute = 'userpassword';
+
+    /**
+     * The password's hash method.
+     *
+     * @var string
+     */
+    protected $passwordHashMethod = 'ssha';
+
+    /**
+     * The object classes of the LDAP model.
+     *
+     * @var array
+     */
+    public static $objectClasses = [
+        'top',
+        'person',
+        'organizationalperson',
+        'inetorgperson',
+    ];
+
+    /**
+     * The groups relationship.
+     *
+     * Retrieves groups that the user is apart of.
+     *
+     * @return \LdapRecord\Models\Relations\HasMany
+     */
+    public function groups()
+    {
+        return $this->hasMany(Group::class, 'memberuid', 'uid');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasMany.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasMany.php
new file mode 100644
index 0000000..d8dfa08
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasMany.php
@@ -0,0 +1,344 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use Closure;
+use LdapRecord\DetectsErrors;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Model;
+use LdapRecord\Models\ModelNotFoundException;
+use LdapRecord\Query\Collection;
+
+class HasMany extends OneToMany
+{
+    use DetectsErrors;
+
+    /**
+     * The model to use for attaching / detaching.
+     *
+     * @var Model
+     */
+    protected $using;
+
+    /**
+     * The attribute key to use for attaching / detaching.
+     *
+     * @var string
+     */
+    protected $usingKey;
+
+    /**
+     * The pagination page size.
+     *
+     * @var int
+     */
+    protected $pageSize = 1000;
+
+    /**
+     * The exceptions to bypass for each relation operation.
+     *
+     * @var array
+     */
+    protected $bypass = [
+        'attach' => [
+            'Already exists', 'Type or value exists',
+        ],
+        'detach' => [
+            'No such attribute', 'Server is unwilling to perform',
+        ],
+    ];
+
+    /**
+     * Set the model and attribute to use for attaching / detaching.
+     *
+     * @param Model  $using
+     * @param string $usingKey
+     *
+     * @return $this
+     */
+    public function using(Model $using, $usingKey)
+    {
+        $this->using = $using;
+        $this->usingKey = $usingKey;
+
+        return $this;
+    }
+
+    /**
+     * Set the pagination page size of the relation query.
+     *
+     * @param int $pageSize
+     *
+     * @return $this
+     */
+    public function setPageSize($pageSize)
+    {
+        $this->pageSize = $pageSize;
+
+        return $this;
+    }
+
+    /**
+     * Paginate the relation using the given page size.
+     *
+     * @param int $pageSize
+     *
+     * @return Collection
+     */
+    public function paginate($pageSize = 1000)
+    {
+        return $this->paginateOnceUsing($pageSize);
+    }
+
+    /**
+     * Paginate the relation using the page size once.
+     *
+     * @param int $pageSize
+     *
+     * @return Collection
+     */
+    protected function paginateOnceUsing($pageSize)
+    {
+        $size = $this->pageSize;
+
+        $result = $this->setPageSize($pageSize)->get();
+
+        $this->pageSize = $size;
+
+        return $result;
+    }
+
+    /**
+     * Chunk the relation results using the given callback.
+     *
+     * @param int     $pageSize
+     * @param Closure $callback
+     *
+     * @return void
+     */
+    public function chunk($pageSize, Closure $callback)
+    {
+        $this->getRelationQuery()->chunk($pageSize, function ($entries) use ($callback) {
+            $callback($this->transformResults($entries));
+        });
+    }
+
+    /**
+     * Get the relationships results.
+     *
+     * @return Collection
+     */
+    public function getRelationResults()
+    {
+        return $this->transformResults(
+            $this->getRelationQuery()->paginate($this->pageSize)
+        );
+    }
+
+    /**
+     * Get the prepared relationship query.
+     *
+     * @return \LdapRecord\Query\Model\Builder
+     */
+    public function getRelationQuery()
+    {
+        $columns = $this->query->getSelects();
+
+        // We need to select the proper key to be able to retrieve its
+        // value from LDAP results. If we don't, we won't be able
+        // to properly attach / detach models from relation
+        // query results as the attribute will not exist.
+        $key = $this->using ? $this->usingKey : $this->relationKey;
+
+        // If the * character is missing from the attributes to select,
+        // we will add the key to the attributes to select and also
+        // validate that the key isn't already being selected
+        // to prevent stacking on multiple relation calls.
+        if (! in_array('*', $columns) && ! in_array($key, $columns)) {
+            $this->query->addSelect($key);
+        }
+
+        return $this->query->whereRaw(
+            $this->relationKey,
+            '=',
+            $this->getEscapedForeignValueFromModel($this->parent)
+        );
+    }
+
+    /**
+     * Attach a model to the relation.
+     *
+     * @param Model|string $model
+     *
+     * @return Model|string|false
+     */
+    public function attach($model)
+    {
+        return $this->attemptFailableOperation(
+            $this->buildAttachCallback($model),
+            $this->bypass['attach'],
+            $model
+        );
+    }
+
+    /**
+     * Build the attach callback.
+     *
+     * @param Model|string $model
+     *
+     * @return \Closure
+     */
+    protected function buildAttachCallback($model)
+    {
+        return function () use ($model) {
+            $foreign = $this->getAttachableForeignValue($model);
+
+            if ($this->using) {
+                return $this->using->createAttribute($this->usingKey, $foreign);
+            }
+
+            if (! $model instanceof Model) {
+                $model = $this->getForeignModelByValueOrFail($model);
+            }
+
+            return $model->createAttribute($this->relationKey, $foreign);
+        };
+    }
+
+    /**
+     * Attach a collection of models to the parent instance.
+     *
+     * @param iterable $models
+     *
+     * @return iterable
+     */
+    public function attachMany($models)
+    {
+        foreach ($models as $model) {
+            $this->attach($model);
+        }
+
+        return $models;
+    }
+
+    /**
+     * Detach the model from the relation.
+     *
+     * @param Model|string $model
+     *
+     * @return Model|string|false
+     */
+    public function detach($model)
+    {
+        return $this->attemptFailableOperation(
+            $this->buildDetachCallback($model),
+            $this->bypass['detach'],
+            $model
+        );
+    }
+
+    /**
+     * Build the detach callback.
+     *
+     * @param Model|string $model
+     *
+     * @return \Closure
+     */
+    protected function buildDetachCallback($model)
+    {
+        return function () use ($model) {
+            $foreign = $this->getAttachableForeignValue($model);
+
+            if ($this->using) {
+                return $this->using->deleteAttribute([$this->usingKey => $foreign]);
+            }
+
+            if (! $model instanceof Model) {
+                $model = $this->getForeignModelByValueOrFail($model);
+            }
+
+            return $model->deleteAttribute([$this->relationKey => $foreign]);
+        };
+    }
+
+    /**
+     * Get the attachable foreign value from the model.
+     *
+     * @param Model|string $model
+     *
+     * @return string
+     */
+    protected function getAttachableForeignValue($model)
+    {
+        if ($model instanceof Model) {
+            return $this->using
+                ? $this->getForeignValueFromModel($model)
+                : $this->getParentForeignValue();
+        }
+
+        return $this->using ? $model : $this->getParentForeignValue();
+    }
+
+    /**
+     * Get the foreign model by the given value, or fail.
+     *
+     * @param string $model
+     *
+     * @throws ModelNotFoundException
+     *
+     * @return Model
+     */
+    protected function getForeignModelByValueOrFail($model)
+    {
+        if (! is_null($model = $this->getForeignModelByValue($model))) {
+            return $model;
+        }
+
+        throw ModelNotFoundException::forQuery(
+            $this->query->getUnescapedQuery(),
+            $this->query->getDn()
+        );
+    }
+
+    /**
+     * Attempt a failable operation and return the value if successful.
+     *
+     * If a bypassable exception is encountered, the value will be returned.
+     *
+     * @param callable     $operation
+     * @param string|array $bypass
+     * @param mixed        $value
+     *
+     * @throws LdapRecordException
+     *
+     * @return mixed
+     */
+    protected function attemptFailableOperation($operation, $bypass, $value)
+    {
+        try {
+            $operation();
+
+            return $value;
+        } catch (LdapRecordException $e) {
+            if ($this->errorContainsMessage($e->getMessage(), $bypass)) {
+                return $value;
+            }
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Detach all relation models.
+     *
+     * @return Collection
+     */
+    public function detachAll()
+    {
+        return $this->onceWithoutMerging(function () {
+            return $this->get()->each(function (Model $model) {
+                $this->detach($model);
+            });
+        });
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasManyIn.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasManyIn.php
new file mode 100644
index 0000000..303a144
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasManyIn.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Query\Collection;
+
+class HasManyIn extends OneToMany
+{
+    /**
+     * Get the relationships results.
+     *
+     * @return Collection
+     */
+    public function getRelationResults()
+    {
+        $results = $this->parent->newCollection();
+
+        foreach ((array) $this->parent->getAttribute($this->relationKey) as $value) {
+            if ($foreign = $this->getForeignModelByValue($value)) {
+                $results->push($foreign);
+            }
+        }
+
+        return $this->transformResults($results);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasOne.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasOne.php
new file mode 100644
index 0000000..9a9b2f9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasOne.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Models\Model;
+
+class HasOne extends Relation
+{
+    /**
+     * Get the results of the relationship.
+     *
+     * @return \LdapRecord\Query\Collection
+     */
+    public function getResults()
+    {
+        $model = $this->getForeignModelByValue(
+            $this->getFirstAttributeValue($this->parent, $this->relationKey)
+        );
+
+        return $this->transformResults(
+            $this->parent->newCollection($model ? [$model] : null)
+        );
+    }
+
+    /**
+     * Attach a model instance to the parent model.
+     *
+     * @param Model|string $model
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return Model|string
+     */
+    public function attach($model)
+    {
+        $foreign = $model instanceof Model
+            ? $this->getForeignValueFromModel($model)
+            : $model;
+
+        $this->parent->setAttribute($this->relationKey, $foreign)->save();
+
+        return $model;
+    }
+
+    /**
+     * Detach the related model from the parent.
+     *
+     * @throws \LdapRecord\LdapRecordException
+     *
+     * @return void
+     */
+    public function detach()
+    {
+        $this->parent->setAttribute($this->relationKey, null)->save();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/OneToMany.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/OneToMany.php
new file mode 100644
index 0000000..d0a407c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/OneToMany.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Query\Collection;
+use LdapRecord\Query\Model\Builder;
+
+abstract class OneToMany extends Relation
+{
+    /**
+     * The relation to merge results with.
+     *
+     * @var OneToMany|null
+     */
+    protected $with;
+
+    /**
+     * The name of the relationship.
+     *
+     * @var string
+     */
+    protected $relationName;
+
+    /**
+     * Whether to include recursive results.
+     *
+     * @var bool
+     */
+    protected $recursive = false;
+
+    /**
+     * Constructor.
+     *
+     * @param Builder $query
+     * @param Model   $parent
+     * @param string  $related
+     * @param string  $relationKey
+     * @param string  $foreignKey
+     * @param string  $relationName
+     */
+    public function __construct(Builder $query, Model $parent, $related, $relationKey, $foreignKey, $relationName)
+    {
+        $this->relationName = $relationName;
+
+        parent::__construct($query, $parent, $related, $relationKey, $foreignKey);
+    }
+
+    /**
+     * Set the relation to load with its parent.
+     *
+     * @param OneToMany $relation
+     *
+     * @return $this
+     */
+    public function with(Relation $relation)
+    {
+        $this->with = $relation;
+
+        return $this;
+    }
+
+    /**
+     * Whether to include recursive results.
+     *
+     * @param bool $enable
+     *
+     * @return $this
+     */
+    public function recursive($enable = true)
+    {
+        $this->recursive = $enable;
+
+        return $this;
+    }
+
+    /**
+     * Get the immediate relationships results.
+     *
+     * @return Collection
+     */
+    abstract public function getRelationResults();
+
+    /**
+     * Get the results of the relationship.
+     *
+     * @return Collection
+     */
+    public function getResults()
+    {
+        $results = $this->recursive
+            ? $this->getRecursiveResults()
+            : $this->getRelationResults();
+
+        return $results->merge(
+            $this->getMergingRelationResults()
+        );
+    }
+
+    /**
+     * Execute the callback excluding the merged query result.
+     *
+     * @param callable $callback
+     *
+     * @return mixed
+     */
+    protected function onceWithoutMerging($callback)
+    {
+        $merging = $this->with;
+
+        $this->with = null;
+
+        $result = $callback();
+
+        $this->with = $merging;
+
+        return $result;
+    }
+
+    /**
+     * Get the relation name.
+     *
+     * @return string
+     */
+    public function getRelationName()
+    {
+        return $this->relationName;
+    }
+
+    /**
+     * Get the results of the merging 'with' relation.
+     *
+     * @return Collection
+     */
+    protected function getMergingRelationResults()
+    {
+        return $this->with
+            ? $this->with->recursive($this->recursive)->get()
+            : $this->parent->newCollection();
+    }
+
+    /**
+     * Get the results for the models relation recursively.
+     *
+     * @param string[] $loaded The distinguished names of models already loaded
+     *
+     * @return Collection
+     */
+    protected function getRecursiveResults(array $loaded = [])
+    {
+        $results = $this->getRelationResults()->reject(function (Model $model) use ($loaded) {
+            // Here we will exclude the models that we have already
+            // loaded the recursive results for so we don't run
+            // into issues with circular relations in LDAP.
+            return in_array($model->getDn(), $loaded);
+        });
+
+        foreach ($results as $model) {
+            $loaded[] = $model->getDn();
+
+            // Finally, we will fetch the related models relations,
+            // passing along our loaded models, to ensure we do
+            // not attempt fetching already loaded relations.
+            $results = $results->merge(
+                $this->getRecursiveRelationResults($model, $loaded)
+            );
+        }
+
+        return $results;
+    }
+
+    /**
+     * Get the recursive relation results for given model.
+     *
+     * @param Model $model
+     * @param array $loaded
+     *
+     * @return Collection
+     */
+    protected function getRecursiveRelationResults(Model $model, array $loaded)
+    {
+        return method_exists($model, $this->relationName)
+            ? $model->{$this->relationName}()->getRecursiveResults($loaded)
+            : $model->newCollection();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/Relation.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/Relation.php
new file mode 100644
index 0000000..1b108fd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/Relation.php
@@ -0,0 +1,368 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Models\Entry;
+use LdapRecord\Models\Model;
+use LdapRecord\Query\Collection;
+use LdapRecord\Query\Model\Builder;
+
+/**
+ * @method bool exists($models = null) Determine if the relation contains all of the given models, or any models
+ * @method bool contains($models)      Determine if any of the given models are contained in the relation
+ */
+abstract class Relation
+{
+    /**
+     * The underlying LDAP query.
+     *
+     * @var Builder
+     */
+    protected $query;
+
+    /**
+     * The parent model instance.
+     *
+     * @var Model
+     */
+    protected $parent;
+
+    /**
+     * The related models.
+     *
+     * @var array
+     */
+    protected $related;
+
+    /**
+     * The relation key.
+     *
+     * @var string
+     */
+    protected $relationKey;
+
+    /**
+     * The foreign key.
+     *
+     * @var string
+     */
+    protected $foreignKey;
+
+    /**
+     * The default relation model.
+     *
+     * @var string
+     */
+    protected $default = Entry::class;
+
+    /**
+     * Constructor.
+     *
+     * @param Builder $query
+     * @param Model   $parent
+     * @param mixed   $related
+     * @param string  $relationKey
+     * @param string  $foreignKey
+     */
+    public function __construct(Builder $query, Model $parent, $related, $relationKey, $foreignKey)
+    {
+        $this->query = $query;
+        $this->parent = $parent;
+        $this->related = (array) $related;
+        $this->relationKey = $relationKey;
+        $this->foreignKey = $foreignKey;
+
+        $this->initRelation();
+    }
+
+    /**
+     * Handle dynamic method calls to the relationship.
+     *
+     * @param string $method
+     * @param array  $parameters
+     *
+     * @return mixed
+     */
+    public function __call($method, $parameters)
+    {
+        if (in_array($method, ['exists', 'contains'])) {
+            return $this->get('objectclass')->$method(...$parameters);
+        }
+
+        $result = $this->query->$method(...$parameters);
+
+        if ($result === $this->query) {
+            return $this;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Get the results of the relationship.
+     *
+     * @return Collection
+     */
+    abstract public function getResults();
+
+    /**
+     * Execute the relationship query.
+     *
+     * @param array|string $columns
+     *
+     * @return Collection
+     */
+    public function get($columns = ['*'])
+    {
+        return $this->getResultsWithColumns($columns);
+    }
+
+    /**
+     * Get the results of the relationship while selecting the given columns.
+     *
+     * If the query columns are empty, the given columns are applied.
+     *
+     * @param array $columns
+     *
+     * @return Collection
+     */
+    protected function getResultsWithColumns($columns)
+    {
+        if (is_null($this->query->columns)) {
+            $this->query->select($columns);
+        }
+
+        return $this->getResults();
+    }
+
+    /**
+     * Get the first result of the relationship.
+     *
+     * @param array|string $columns
+     *
+     * @return Model|null
+     */
+    public function first($columns = ['*'])
+    {
+        return $this->get($columns)->first();
+    }
+
+    /**
+     * Prepare the relation query.
+     *
+     * @return static
+     */
+    public function initRelation()
+    {
+        $this->query
+            ->clearFilters()
+            ->withoutGlobalScopes()
+            ->setModel($this->getNewDefaultModel());
+
+        return $this;
+    }
+
+    /**
+     * Get the underlying query for the relation.
+     *
+     * @return Builder
+     */
+    public function getQuery()
+    {
+        return $this->query;
+    }
+
+    /**
+     * Get the parent model of the relation.
+     *
+     * @return Model
+     */
+    public function getParent()
+    {
+        return $this->parent;
+    }
+
+    /**
+     * Get the relation attribute key.
+     *
+     * @return string
+     */
+    public function getRelationKey()
+    {
+        return $this->relationKey;
+    }
+
+    /**
+     * Get the related model classes for the relation.
+     *
+     * @return array
+     */
+    public function getRelated()
+    {
+        return $this->related;
+    }
+
+    /**
+     * Get the relation foreign attribute key.
+     *
+     * @return string
+     */
+    public function getForeignKey()
+    {
+        return $this->foreignKey;
+    }
+
+    /**
+     * Get the class name of the default model.
+     *
+     * @return string
+     */
+    public function getDefaultModel()
+    {
+        return $this->default;
+    }
+
+    /**
+     * Get a new instance of the default model on the relation.
+     *
+     * @return Model
+     */
+    public function getNewDefaultModel()
+    {
+        $model = new $this->default();
+
+        $model->setConnection($this->parent->getConnectionName());
+
+        return $model;
+    }
+
+    /**
+     * Get the foreign model by the given value.
+     *
+     * @param string $value
+     *
+     * @return Model|null
+     */
+    protected function getForeignModelByValue($value)
+    {
+        return $this->foreignKeyIsDistinguishedName()
+            ? $this->query->find($value)
+            : $this->query->findBy($this->foreignKey, $value);
+    }
+
+    /**
+     * Returns the escaped foreign key value for use in an LDAP filter from the model.
+     *
+     * @param Model $model
+     *
+     * @return string
+     */
+    protected function getEscapedForeignValueFromModel(Model $model)
+    {
+        return $this->query->escape(
+            $this->getForeignValueFromModel($model)
+        )->both();
+    }
+
+    /**
+     * Get the relation parents foreign value.
+     *
+     * @return string
+     */
+    protected function getParentForeignValue()
+    {
+        return $this->getForeignValueFromModel($this->parent);
+    }
+
+    /**
+     * Get the foreign key value from the model.
+     *
+     * @param Model $model
+     *
+     * @return string
+     */
+    protected function getForeignValueFromModel(Model $model)
+    {
+        return $this->foreignKeyIsDistinguishedName()
+                ? $model->getDn()
+                : $this->getFirstAttributeValue($model, $this->foreignKey);
+    }
+
+    /**
+     * Get the first attribute value from the model.
+     *
+     * @param Model  $model
+     * @param string $attribute
+     *
+     * @return string|null
+     */
+    protected function getFirstAttributeValue(Model $model, $attribute)
+    {
+        return $model->getFirstAttribute($attribute);
+    }
+
+    /**
+     * Transforms the results by converting the models into their related.
+     *
+     * @param Collection $results
+     *
+     * @return Collection
+     */
+    protected function transformResults(Collection $results)
+    {
+        $related = [];
+
+        foreach ($this->related as $relation) {
+            $related[$relation] = $relation::$objectClasses;
+        }
+
+        return $results->transform(function (Model $entry) use ($related) {
+            $model = $this->determineModelFromRelated($entry, $related);
+
+            return class_exists($model) ? $entry->convert(new $model()) : $entry;
+        });
+    }
+
+    /**
+     * Determines if the foreign key is a distinguished name.
+     *
+     * @return bool
+     */
+    protected function foreignKeyIsDistinguishedName()
+    {
+        return in_array($this->foreignKey, ['dn', 'distinguishedname']);
+    }
+
+    /**
+     * Determines the model from the given relations.
+     *
+     * @param Model $model
+     * @param array $related
+     *
+     * @return string|bool
+     */
+    protected function determineModelFromRelated(Model $model, array $related)
+    {
+        // We must normalize all the related models object class
+        // names to the same case so we are able to properly
+        // determine the owning model from search results.
+        return array_search(
+            $this->normalizeObjectClasses($model->getObjectClasses()),
+            array_map([$this, 'normalizeObjectClasses'], $related)
+        );
+    }
+
+    /**
+     * Sort and normalize the object classes.
+     *
+     * @param array $classes
+     *
+     * @return array
+     */
+    protected function normalizeObjectClasses($classes)
+    {
+        sort($classes);
+
+        return array_map('strtolower', $classes);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Scope.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Scope.php
new file mode 100644
index 0000000..321cae3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Scope.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use LdapRecord\Query\Model\Builder;
+
+interface Scope
+{
+    /**
+     * Apply the scope to the given query.
+     *
+     * @param Builder $query
+     * @param Model   $model
+     *
+     * @return void
+     */
+    public function apply(Builder $query, Model $model);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/ActiveDirectory.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/ActiveDirectory.php
new file mode 100644
index 0000000..61d21ef
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/ActiveDirectory.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface ActiveDirectory extends TypeInterface
+{
+    /**
+     * Returns the models object SID key.
+     *
+     * @return string
+     */
+    public function getObjectSidKey();
+
+    /**
+     * Returns the model's hex object SID.
+     *
+     * @see https://msdn.microsoft.com/en-us/library/ms679024(v=vs.85).aspx
+     *
+     * @return string
+     */
+    public function getObjectSid();
+
+    /**
+     * Returns the model's SID.
+     *
+     * @return string|null
+     */
+    public function getConvertedSid();
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/FreeIPA.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/FreeIPA.php
new file mode 100644
index 0000000..6831318
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/FreeIPA.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface FreeIPA extends TypeInterface
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/OpenLDAP.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/OpenLDAP.php
new file mode 100644
index 0000000..e63076e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/OpenLDAP.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface OpenLDAP extends TypeInterface
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/TypeInterface.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/TypeInterface.php
new file mode 100644
index 0000000..8a45f32
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/TypeInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface TypeInterface
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ArrayCacheStore.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ArrayCacheStore.php
new file mode 100644
index 0000000..d7480a7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ArrayCacheStore.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use Psr\SimpleCache\CacheInterface;
+
+class ArrayCacheStore implements CacheInterface
+{
+    use InteractsWithTime;
+
+    /**
+     * An array of stored values.
+     *
+     * @var array
+     */
+    protected $storage = [];
+
+    /**
+     * @inheritdoc
+     */
+    public function get($key, $default = null)
+    {
+        if (! isset($this->storage[$key])) {
+            return $default;
+        }
+
+        $item = $this->storage[$key];
+
+        $expiresAt = $item['expiresAt'] ?? 0;
+
+        if ($expiresAt !== 0 && $this->currentTime() > $expiresAt) {
+            $this->delete($key);
+
+            return $default;
+        }
+
+        return $item['value'];
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function set($key, $value, $ttl = null)
+    {
+        $this->storage[$key] = [
+            'value' => $value,
+            'expiresAt' => $this->calculateExpiration($ttl),
+        ];
+
+        return true;
+    }
+
+    /**
+     * Get the expiration time of the key.
+     *
+     * @param int $seconds
+     *
+     * @return int
+     */
+    protected function calculateExpiration($seconds)
+    {
+        return $this->toTimestamp($seconds);
+    }
+
+    /**
+     * Get the UNIX timestamp for the given number of seconds.
+     *
+     * @param int $seconds
+     *
+     * @return int
+     */
+    protected function toTimestamp($seconds)
+    {
+        return $seconds > 0 ? $this->availableAt($seconds) : 0;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function delete($key)
+    {
+        unset($this->storage[$key]);
+
+        return true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function clear()
+    {
+        $this->storage = [];
+
+        return true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getMultiple($keys, $default = null)
+    {
+        $values = [];
+
+        foreach ($keys as $key) {
+            $values[$key] = $this->get($key, $default);
+        }
+
+        return $values;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setMultiple($values, $ttl = null)
+    {
+        foreach ($values as $key => $value) {
+            $this->set($key, $value, $ttl);
+        }
+
+        return true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function deleteMultiple($keys)
+    {
+        foreach ($keys as $key) {
+            $this->delete($key);
+        }
+
+        return true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function has($key)
+    {
+        return isset($this->storage[$key]);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Builder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Builder.php
new file mode 100644
index 0000000..c75afa2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Builder.php
@@ -0,0 +1,1903 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use BadMethodCallException;
+use Closure;
+use DateTimeInterface;
+use InvalidArgumentException;
+use LdapRecord\Connection;
+use LdapRecord\Container;
+use LdapRecord\EscapesValues;
+use LdapRecord\LdapInterface;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Model;
+use LdapRecord\Query\Events\QueryExecuted;
+use LdapRecord\Query\Model\Builder as ModelBuilder;
+use LdapRecord\Query\Pagination\LazyPaginator;
+use LdapRecord\Query\Pagination\Paginator;
+use LdapRecord\Support\Arr;
+use LdapRecord\Utilities;
+
+class Builder
+{
+    use EscapesValues;
+
+    /**
+     * The selected columns to retrieve on the query.
+     *
+     * @var array
+     */
+    public $columns;
+
+    /**
+     * The query filters.
+     *
+     * @var array
+     */
+    public $filters = [
+        'and' => [],
+        'or' => [],
+        'raw' => [],
+    ];
+
+    /**
+     * The LDAP server controls to be sent.
+     *
+     * @var array
+     */
+    public $controls = [];
+
+    /**
+     * The size limit of the query.
+     *
+     * @var int
+     */
+    public $limit = 0;
+
+    /**
+     * Determines whether the current query is paginated.
+     *
+     * @var bool
+     */
+    public $paginated = false;
+
+    /**
+     * The distinguished name to perform searches upon.
+     *
+     * @var string|null
+     */
+    protected $dn;
+
+    /**
+     * The base distinguished name to perform searches inside.
+     *
+     * @var string|null
+     */
+    protected $baseDn;
+
+    /**
+     * The default query type.
+     *
+     * @var string
+     */
+    protected $type = 'search';
+
+    /**
+     * Determines whether the query is nested.
+     *
+     * @var bool
+     */
+    protected $nested = false;
+
+    /**
+     * Determines whether the query should be cached.
+     *
+     * @var bool
+     */
+    protected $caching = false;
+
+    /**
+     * How long the query should be cached until.
+     *
+     * @var DateTimeInterface|null
+     */
+    protected $cacheUntil = null;
+
+    /**
+     * Determines whether the query cache must be flushed.
+     *
+     * @var bool
+     */
+    protected $flushCache = false;
+
+    /**
+     * The current connection instance.
+     *
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
+     * The current grammar instance.
+     *
+     * @var Grammar
+     */
+    protected $grammar;
+
+    /**
+     * The current cache instance.
+     *
+     * @var Cache|null
+     */
+    protected $cache;
+
+    /**
+     * Constructor.
+     *
+     * @param Connection $connection
+     */
+    public function __construct(Connection $connection)
+    {
+        $this->connection = $connection;
+        $this->grammar = new Grammar();
+    }
+
+    /**
+     * Set the current connection.
+     *
+     * @param Connection $connection
+     *
+     * @return $this
+     */
+    public function setConnection(Connection $connection)
+    {
+        $this->connection = $connection;
+
+        return $this;
+    }
+
+    /**
+     * Set the current filter grammar.
+     *
+     * @param Grammar $grammar
+     *
+     * @return $this
+     */
+    public function setGrammar(Grammar $grammar)
+    {
+        $this->grammar = $grammar;
+
+        return $this;
+    }
+
+    /**
+     * Set the cache to store query results.
+     *
+     * @param Cache|null $cache
+     *
+     * @return $this
+     */
+    public function setCache(Cache $cache = null)
+    {
+        $this->cache = $cache;
+
+        return $this;
+    }
+
+    /**
+     * Returns a new Query Builder instance.
+     *
+     * @param string $baseDn
+     *
+     * @return $this
+     */
+    public function newInstance($baseDn = null)
+    {
+        // We'll set the base DN of the new Builder so
+        // developers don't need to do this manually.
+        $dn = is_null($baseDn) ? $this->getDn() : $baseDn;
+
+        return (new static($this->connection))->setDn($dn);
+    }
+
+    /**
+     * Returns a new nested Query Builder instance.
+     *
+     * @param Closure|null $closure
+     *
+     * @return $this
+     */
+    public function newNestedInstance(Closure $closure = null)
+    {
+        $query = $this->newInstance()->nested();
+
+        if ($closure) {
+            $closure($query);
+        }
+
+        return $query;
+    }
+
+    /**
+     * Executes the LDAP query.
+     *
+     * @param string|array $columns
+     *
+     * @return Collection|array
+     */
+    public function get($columns = ['*'])
+    {
+        return $this->onceWithColumns(Arr::wrap($columns), function () {
+            return $this->query($this->getQuery());
+        });
+    }
+
+    /**
+     * Execute the given callback while selecting the given columns.
+     *
+     * After running the callback, the columns are reset to the original value.
+     *
+     * @param array    $columns
+     * @param callable $callback
+     *
+     * @return mixed
+     */
+    protected function onceWithColumns($columns, $callback)
+    {
+        $original = $this->columns;
+
+        if (is_null($original)) {
+            $this->columns = $columns;
+        }
+
+        $result = $callback();
+
+        $this->columns = $original;
+
+        return $result;
+    }
+
+    /**
+     * Compiles and returns the current query string.
+     *
+     * @return string
+     */
+    public function getQuery()
+    {
+        // We need to ensure we have at least one filter, as
+        // no query results will be returned otherwise.
+        if (count(array_filter($this->filters)) === 0) {
+            $this->whereHas('objectclass');
+        }
+
+        return $this->grammar->compile($this);
+    }
+
+    /**
+     * Returns the unescaped query.
+     *
+     * @return string
+     */
+    public function getUnescapedQuery()
+    {
+        return Utilities::unescape($this->getQuery());
+    }
+
+    /**
+     * Returns the current Grammar instance.
+     *
+     * @return Grammar
+     */
+    public function getGrammar()
+    {
+        return $this->grammar;
+    }
+
+    /**
+     * Returns the current Cache instance.
+     *
+     * @return Cache|null
+     */
+    public function getCache()
+    {
+        return $this->cache;
+    }
+
+    /**
+     * Returns the current Connection instance.
+     *
+     * @return Connection
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * Returns the query type.
+     *
+     * @return string
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Set the base distinguished name of the query.
+     *
+     * @param Model|string $dn
+     *
+     * @return $this
+     */
+    public function setBaseDn($dn)
+    {
+        $this->baseDn = $this->substituteBaseInDn($dn);
+
+        return $this;
+    }
+
+    /**
+     * Get the base distinguished name of the query.
+     *
+     * @return string|null
+     */
+    public function getBaseDn()
+    {
+        return $this->baseDn;
+    }
+
+    /**
+     * Get the distinguished name of the query.
+     *
+     * @return string
+     */
+    public function getDn()
+    {
+        return $this->dn;
+    }
+
+    /**
+     * Set the distinguished name for the query.
+     *
+     * @param string|Model|null $dn
+     *
+     * @return $this
+     */
+    public function setDn($dn = null)
+    {
+        $this->dn = $this->substituteBaseInDn($dn);
+
+        return $this;
+    }
+
+    /**
+     * Substitute the base DN string template for the current base.
+     *
+     * @param Model|string $dn
+     *
+     * @return string
+     */
+    protected function substituteBaseInDn($dn)
+    {
+        return str_replace(
+            '{base}',
+            $this->baseDn,
+            $dn instanceof Model ? $dn->getDn() : $dn
+        );
+    }
+
+    /**
+     * Alias for setting the distinguished name for the query.
+     *
+     * @param string|Model|null $dn
+     *
+     * @return $this
+     */
+    public function in($dn = null)
+    {
+        return $this->setDn($dn);
+    }
+
+    /**
+     * Set the size limit of the current query.
+     *
+     * @param int $limit
+     *
+     * @return $this
+     */
+    public function limit($limit = 0)
+    {
+        $this->limit = $limit;
+
+        return $this;
+    }
+
+    /**
+     * Returns a new query for the given model.
+     *
+     * @param Model $model
+     *
+     * @return ModelBuilder
+     */
+    public function model(Model $model)
+    {
+        return $model->newQueryBuilder($this->connection)
+            ->setCache($this->connection->getCache())
+            ->setBaseDn($this->baseDn)
+            ->setModel($model);
+    }
+
+    /**
+     * Performs the specified query on the current LDAP connection.
+     *
+     * @param string $query
+     *
+     * @return Collection|array
+     */
+    public function query($query)
+    {
+        $start = microtime(true);
+
+        // Here we will create the execution callback. This allows us
+        // to only execute an LDAP request if caching is disabled
+        // or if no cache of the given query exists yet.
+        $callback = function () use ($query) {
+            return $this->parse($this->run($query));
+        };
+
+        $results = $this->getCachedResponse($query, $callback);
+
+        $this->logQuery($this, $this->type, $this->getElapsedTime($start));
+
+        return $this->process($results);
+    }
+
+    /**
+     * Paginates the current LDAP query.
+     *
+     * @param int  $pageSize
+     * @param bool $isCritical
+     *
+     * @return Collection|array
+     */
+    public function paginate($pageSize = 1000, $isCritical = false)
+    {
+        $this->paginated = true;
+
+        $start = microtime(true);
+
+        $query = $this->getQuery();
+
+        // Here we will create the pagination callback. This allows us
+        // to only execute an LDAP request if caching is disabled
+        // or if no cache of the given query exists yet.
+        $callback = function () use ($query, $pageSize, $isCritical) {
+            return $this->runPaginate($query, $pageSize, $isCritical);
+        };
+
+        $pages = $this->getCachedResponse($query, $callback);
+
+        $this->logQuery($this, 'paginate', $this->getElapsedTime($start));
+
+        return $this->process($pages);
+    }
+
+    /**
+     * Runs the paginate operation with the given filter.
+     *
+     * @param string $filter
+     * @param int    $perPage
+     * @param bool   $isCritical
+     *
+     * @return array
+     */
+    protected function runPaginate($filter, $perPage, $isCritical)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
+            return (new Paginator($this, $filter, $perPage, $isCritical))->execute($ldap);
+        });
+    }
+
+    /**
+     * Chunk the results of a paginated LDAP query.
+     *
+     * @param int     $pageSize
+     * @param Closure $callback
+     * @param bool    $isCritical
+     *
+     * @return void
+     */
+    public function chunk($pageSize, Closure $callback, $isCritical = false)
+    {
+        $start = microtime(true);
+
+        $query = $this->getQuery();
+
+        foreach ($this->runChunk($query, $pageSize, $isCritical) as $chunk) {
+            $callback($this->process($chunk));
+        }
+
+        $this->logQuery($this, 'chunk', $this->getElapsedTime($start));
+    }
+
+    /**
+     * Runs the chunk operation with the given filter.
+     *
+     * @param string $filter
+     * @param int    $perPage
+     * @param bool   $isCritical
+     *
+     * @return array
+     */
+    protected function runChunk($filter, $perPage, $isCritical)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
+            return (new LazyPaginator($this, $filter, $perPage, $isCritical))->execute($ldap);
+        });
+    }
+
+    /**
+     * Processes and converts the given LDAP results into models.
+     *
+     * @param array $results
+     *
+     * @return array
+     */
+    protected function process(array $results)
+    {
+        unset($results['count']);
+
+        return $this->paginated ? $this->flattenPages($results) : $results;
+    }
+
+    /**
+     * Flattens LDAP paged results into a single array.
+     *
+     * @param array $pages
+     *
+     * @return array
+     */
+    protected function flattenPages(array $pages)
+    {
+        $records = [];
+
+        foreach ($pages as $page) {
+            unset($page['count']);
+
+            $records = array_merge($records, $page);
+        }
+
+        return $records;
+    }
+
+    /**
+     * Get the cached response or execute and cache the callback value.
+     *
+     * @param string  $query
+     * @param Closure $callback
+     *
+     * @return mixed
+     */
+    protected function getCachedResponse($query, Closure $callback)
+    {
+        // If caching is enabled and we have a cache instance available,
+        // we will try to retrieve the cached results instead.
+        if ($this->caching && $this->cache) {
+            $key = $this->getCacheKey($query);
+
+            if ($this->flushCache) {
+                $this->cache->delete($key);
+            }
+
+            return $this->cache->remember($key, $this->cacheUntil, $callback);
+        }
+
+        // Otherwise, we will simply execute the callback.
+        return $callback();
+    }
+
+    /**
+     * Runs the query operation with the given filter.
+     *
+     * @param string $filter
+     *
+     * @return resource
+     */
+    public function run($filter)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($filter) {
+            // We will avoid setting the controls during any pagination
+            // requests as it will clear the cookie we need to send
+            // to the server upon retrieving every page.
+            if (! $this->paginated) {
+                // Before running the query, we will set the LDAP server controls. This
+                // allows the controls to be automatically reset upon each new query
+                // that is conducted on the same connection during each request.
+                $ldap->setOption(LDAP_OPT_SERVER_CONTROLS, $this->controls);
+            }
+
+            return $ldap->{$this->type}(
+                $this->dn ?? $this->baseDn,
+                $filter,
+                $this->getSelects(),
+                $onlyAttributes = false,
+                $this->limit
+            );
+        });
+    }
+
+    /**
+     * Parses the given LDAP resource by retrieving its entries.
+     *
+     * @param resource $resource
+     *
+     * @return array
+     */
+    public function parse($resource)
+    {
+        if (! $resource) {
+            return [];
+        }
+
+        return $this->connection->run(function (LdapInterface $ldap) use ($resource) {
+            $entries = $ldap->getEntries($resource);
+
+            // Free up memory.
+            if (is_resource($resource)) {
+                $ldap->freeResult($resource);
+            }
+
+            return $entries;
+        });
+    }
+
+    /**
+     * Returns the cache key.
+     *
+     * @param string $query
+     *
+     * @return string
+     */
+    protected function getCacheKey($query)
+    {
+        $host = $this->connection->getLdapConnection()->getHost();
+
+        $key = $host
+            .$this->type
+            .$this->getDn()
+            .$query
+            .implode($this->getSelects())
+            .$this->limit
+            .$this->paginated;
+
+        return md5($key);
+    }
+
+    /**
+     * Returns the first entry in a search result.
+     *
+     * @param array|string $columns
+     *
+     * @return Model|null
+     */
+    public function first($columns = ['*'])
+    {
+        return Arr::get($this->limit(1)->get($columns), 0);
+    }
+
+    /**
+     * Returns the first entry in a search result.
+     *
+     * If no entry is found, an exception is thrown.
+     *
+     * @param array|string $columns
+     *
+     * @throws ObjectNotFoundException
+     *
+     * @return Model|static
+     */
+    public function firstOrFail($columns = ['*'])
+    {
+        if (! $record = $this->first($columns)) {
+            $this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
+        }
+
+        return $record;
+    }
+
+    /**
+     * Throws a not found exception.
+     *
+     * @param string $query
+     * @param string $dn
+     *
+     * @throws ObjectNotFoundException
+     */
+    protected function throwNotFoundException($query, $dn)
+    {
+        throw ObjectNotFoundException::forQuery($query, $dn);
+    }
+
+    /**
+     * Finds a record by the specified attribute and value.
+     *
+     * @param string       $attribute
+     * @param string       $value
+     * @param array|string $columns
+     *
+     * @return Model|static|null
+     */
+    public function findBy($attribute, $value, $columns = ['*'])
+    {
+        try {
+            return $this->findByOrFail($attribute, $value, $columns);
+        } catch (ObjectNotFoundException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Finds a record by the specified attribute and value.
+     *
+     * If no record is found an exception is thrown.
+     *
+     * @param string       $attribute
+     * @param string       $value
+     * @param array|string $columns
+     *
+     * @throws ObjectNotFoundException
+     *
+     * @return Model
+     */
+    public function findByOrFail($attribute, $value, $columns = ['*'])
+    {
+        return $this->whereEquals($attribute, $value)->firstOrFail($columns);
+    }
+
+    /**
+     * Find many records by distinguished name.
+     *
+     * @param array $dns
+     * @param array $columns
+     *
+     * @return array|Collection
+     */
+    public function findMany($dns, $columns = ['*'])
+    {
+        if (empty($dns)) {
+            return $this->process([]);
+        }
+
+        $objects = [];
+
+        foreach ($dns as $dn) {
+            if (! is_null($object = $this->find($dn, $columns))) {
+                $objects[] = $object;
+            }
+        }
+
+        return $this->process($objects);
+    }
+
+    /**
+     * Finds many records by the specified attribute.
+     *
+     * @param string $attribute
+     * @param array  $values
+     * @param array  $columns
+     *
+     * @return Collection
+     */
+    public function findManyBy($attribute, array $values = [], $columns = ['*'])
+    {
+        $query = $this->select($columns);
+
+        foreach ($values as $value) {
+            $query->orWhere([$attribute => $value]);
+        }
+
+        return $query->get();
+    }
+
+    /**
+     * Finds a record by its distinguished name.
+     *
+     * @param string|array $dn
+     * @param array|string $columns
+     *
+     * @return Model|static|array|Collection|null
+     */
+    public function find($dn, $columns = ['*'])
+    {
+        if (is_array($dn)) {
+            return $this->findMany($dn, $columns);
+        }
+
+        try {
+            return $this->findOrFail($dn, $columns);
+        } catch (ObjectNotFoundException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Finds a record by its distinguished name.
+     *
+     * Fails upon no records returned.
+     *
+     * @param string       $dn
+     * @param array|string $columns
+     *
+     * @throws ObjectNotFoundException
+     *
+     * @return Model|static
+     */
+    public function findOrFail($dn, $columns = ['*'])
+    {
+        return $this->setDn($dn)
+            ->read()
+            ->whereHas('objectclass')
+            ->firstOrFail($columns);
+    }
+
+    /**
+     * Adds the inserted fields to query on the current LDAP connection.
+     *
+     * @param array|string $columns
+     *
+     * @return $this
+     */
+    public function select($columns = ['*'])
+    {
+        $columns = is_array($columns) ? $columns : func_get_args();
+
+        if (! empty($columns)) {
+            $this->columns = $columns;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add a new select column to the query.
+     *
+     * @param array|mixed $column
+     *
+     * @return $this
+     */
+    public function addSelect($column)
+    {
+        $column = is_array($column) ? $column : func_get_args();
+
+        $this->columns = array_merge((array) $this->columns, $column);
+
+        return $this;
+    }
+
+    /**
+     * Adds a raw filter to the current query.
+     *
+     * @param array|string $filters
+     *
+     * @return $this
+     */
+    public function rawFilter($filters = [])
+    {
+        $filters = is_array($filters) ? $filters : func_get_args();
+
+        foreach ($filters as $filter) {
+            $this->filters['raw'][] = $filter;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Adds a nested 'and' filter to the current query.
+     *
+     * @param Closure $closure
+     *
+     * @return $this
+     */
+    public function andFilter(Closure $closure)
+    {
+        $query = $this->newNestedInstance($closure);
+
+        return $this->rawFilter(
+            $this->grammar->compileAnd($query->getQuery())
+        );
+    }
+
+    /**
+     * Adds a nested 'or' filter to the current query.
+     *
+     * @param Closure $closure
+     *
+     * @return $this
+     */
+    public function orFilter(Closure $closure)
+    {
+        $query = $this->newNestedInstance($closure);
+
+        return $this->rawFilter(
+            $this->grammar->compileOr($query->getQuery())
+        );
+    }
+
+    /**
+     * Adds a nested 'not' filter to the current query.
+     *
+     * @param Closure $closure
+     *
+     * @return $this
+     */
+    public function notFilter(Closure $closure)
+    {
+        $query = $this->newNestedInstance($closure);
+
+        return $this->rawFilter(
+            $this->grammar->compileNot($query->getQuery())
+        );
+    }
+
+    /**
+     * Adds a where clause to the current query.
+     *
+     * @param string|array $field
+     * @param string       $operator
+     * @param string       $value
+     * @param string       $boolean
+     * @param bool         $raw
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return $this
+     */
+    public function where($field, $operator = null, $value = null, $boolean = 'and', $raw = false)
+    {
+        if (is_array($field)) {
+            // If the field is an array, we will assume we have been
+            // provided with an array of key-value pairs and can
+            // add them each as their own seperate where clause.
+            return $this->addArrayOfWheres($field, $boolean, $raw);
+        }
+
+        // If we have been provided with two arguments not a "has" or
+        // "not has" operator, we'll assume the developer is creating
+        // an "equals" clause and set the proper operator in place.
+        if (func_num_args() === 2 && ! in_array($operator, ['*', '!*'])) {
+            [$value, $operator] = [$operator, '='];
+        }
+
+        if (! in_array($operator, $this->grammar->getOperators())) {
+            throw new InvalidArgumentException("Invalid LDAP filter operator [$operator]");
+        }
+
+        // We'll escape the value if raw isn't requested.
+        $value = $this->prepareWhereValue($field, $value, $raw);
+
+        $field = $this->escape($field)->both()->get();
+
+        $this->addFilter($boolean, compact('field', 'operator', 'value'));
+
+        return $this;
+    }
+
+    /**
+     * Prepare the value for being queried.
+     *
+     * @param string $field
+     * @param string $value
+     * @param bool   $raw
+     *
+     * @return string
+     */
+    protected function prepareWhereValue($field, $value, $raw = false)
+    {
+        return $raw ? $value : $this->escape($value);
+    }
+
+    /**
+     * Adds a raw where clause to the current query.
+     *
+     * Values given to this method are not escaped.
+     *
+     * @param string|array $field
+     * @param string       $operator
+     * @param string       $value
+     *
+     * @return $this
+     */
+    public function whereRaw($field, $operator = null, $value = null)
+    {
+        return $this->where($field, $operator, $value, 'and', true);
+    }
+
+    /**
+     * Adds a 'where equals' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereEquals($field, $value)
+    {
+        return $this->where($field, '=', $value);
+    }
+
+    /**
+     * Adds a 'where not equals' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereNotEquals($field, $value)
+    {
+        return $this->where($field, '!', $value);
+    }
+
+    /**
+     * Adds a 'where approximately equals' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereApproximatelyEquals($field, $value)
+    {
+        return $this->where($field, '~=', $value);
+    }
+
+    /**
+     * Adds a 'where has' clause to the current query.
+     *
+     * @param string $field
+     *
+     * @return $this
+     */
+    public function whereHas($field)
+    {
+        return $this->where($field, '*');
+    }
+
+    /**
+     * Adds a 'where not has' clause to the current query.
+     *
+     * @param string $field
+     *
+     * @return $this
+     */
+    public function whereNotHas($field)
+    {
+        return $this->where($field, '!*');
+    }
+
+    /**
+     * Adds a 'where contains' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereContains($field, $value)
+    {
+        return $this->where($field, 'contains', $value);
+    }
+
+    /**
+     * Adds a 'where contains' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereNotContains($field, $value)
+    {
+        return $this->where($field, 'not_contains', $value);
+    }
+
+    /**
+     * Query for entries that match any of the values provided for the given field.
+     *
+     * @param string $field
+     * @param array  $values
+     *
+     * @return $this
+     */
+    public function whereIn($field, array $values)
+    {
+        return $this->orFilter(function (self $query) use ($field, $values) {
+            foreach ($values as $value) {
+                $query->whereEquals($field, $value);
+            }
+        });
+    }
+
+    /**
+     * Adds a 'between' clause to the current query.
+     *
+     * @param string $field
+     * @param array  $values
+     *
+     * @return $this
+     */
+    public function whereBetween($field, array $values)
+    {
+        return $this->where([
+            [$field, '>=', $values[0]],
+            [$field, '<=', $values[1]],
+        ]);
+    }
+
+    /**
+     * Adds a 'where starts with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereStartsWith($field, $value)
+    {
+        return $this->where($field, 'starts_with', $value);
+    }
+
+    /**
+     * Adds a 'where *not* starts with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereNotStartsWith($field, $value)
+    {
+        return $this->where($field, 'not_starts_with', $value);
+    }
+
+    /**
+     * Adds a 'where ends with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereEndsWith($field, $value)
+    {
+        return $this->where($field, 'ends_with', $value);
+    }
+
+    /**
+     * Adds a 'where *not* ends with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function whereNotEndsWith($field, $value)
+    {
+        return $this->where($field, 'not_ends_with', $value);
+    }
+
+    /**
+     * Only include deleted models in the results.
+     *
+     * @return $this
+     */
+    public function whereDeleted()
+    {
+        return $this->withDeleted()->whereEquals('isDeleted', 'TRUE');
+    }
+
+    /**
+     * Set the LDAP control option to include deleted LDAP models.
+     *
+     * @return $this
+     */
+    public function withDeleted()
+    {
+        return $this->addControl(LdapInterface::OID_SERVER_SHOW_DELETED, $isCritical = true);
+    }
+
+    /**
+     * Add a server control to the query.
+     *
+     * @param string $oid
+     * @param bool   $isCritical
+     * @param mixed  $value
+     *
+     * @return $this
+     */
+    public function addControl($oid, $isCritical = false, $value = null)
+    {
+        $this->controls[$oid] = compact('oid', 'isCritical', 'value');
+
+        return $this;
+    }
+
+    /**
+     * Determine if the server control exists on the query.
+     *
+     * @param string $oid
+     *
+     * @return bool
+     */
+    public function hasControl($oid)
+    {
+        return array_key_exists($oid, $this->controls);
+    }
+
+    /**
+     * Adds an 'or where' clause to the current query.
+     *
+     * @param array|string $field
+     * @param string|null  $operator
+     * @param string|null  $value
+     *
+     * @return $this
+     */
+    public function orWhere($field, $operator = null, $value = null)
+    {
+        return $this->where($field, $operator, $value, 'or');
+    }
+
+    /**
+     * Adds a raw or where clause to the current query.
+     *
+     * Values given to this method are not escaped.
+     *
+     * @param string $field
+     * @param string $operator
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereRaw($field, $operator = null, $value = null)
+    {
+        return $this->where($field, $operator, $value, 'or', true);
+    }
+
+    /**
+     * Adds an 'or where has' clause to the current query.
+     *
+     * @param string $field
+     *
+     * @return $this
+     */
+    public function orWhereHas($field)
+    {
+        return $this->orWhere($field, '*');
+    }
+
+    /**
+     * Adds a 'where not has' clause to the current query.
+     *
+     * @param string $field
+     *
+     * @return $this
+     */
+    public function orWhereNotHas($field)
+    {
+        return $this->orWhere($field, '!*');
+    }
+
+    /**
+     * Adds an 'or where equals' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereEquals($field, $value)
+    {
+        return $this->orWhere($field, '=', $value);
+    }
+
+    /**
+     * Adds an 'or where not equals' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereNotEquals($field, $value)
+    {
+        return $this->orWhere($field, '!', $value);
+    }
+
+    /**
+     * Adds a 'or where approximately equals' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereApproximatelyEquals($field, $value)
+    {
+        return $this->orWhere($field, '~=', $value);
+    }
+
+    /**
+     * Adds an 'or where contains' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereContains($field, $value)
+    {
+        return $this->orWhere($field, 'contains', $value);
+    }
+
+    /**
+     * Adds an 'or where *not* contains' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereNotContains($field, $value)
+    {
+        return $this->orWhere($field, 'not_contains', $value);
+    }
+
+    /**
+     * Adds an 'or where starts with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereStartsWith($field, $value)
+    {
+        return $this->orWhere($field, 'starts_with', $value);
+    }
+
+    /**
+     * Adds an 'or where *not* starts with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereNotStartsWith($field, $value)
+    {
+        return $this->orWhere($field, 'not_starts_with', $value);
+    }
+
+    /**
+     * Adds an 'or where ends with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereEndsWith($field, $value)
+    {
+        return $this->orWhere($field, 'ends_with', $value);
+    }
+
+    /**
+     * Adds an 'or where *not* ends with' clause to the current query.
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return $this
+     */
+    public function orWhereNotEndsWith($field, $value)
+    {
+        return $this->orWhere($field, 'not_ends_with', $value);
+    }
+
+    /**
+     * Adds a filter binding onto the current query.
+     *
+     * @param string $type     The type of filter to add.
+     * @param array  $bindings The bindings of the filter.
+     *
+     * @throws InvalidArgumentException
+     *
+     * @return $this
+     */
+    public function addFilter($type, array $bindings)
+    {
+        if (! array_key_exists($type, $this->filters)) {
+            throw new InvalidArgumentException("Filter type: [$type] is invalid.");
+        }
+
+        // Each filter clause require key bindings to be set. We
+        // will validate this here to ensure all of them have
+        // been provided, or throw an exception otherwise.
+        if ($missing = $this->missingBindingKeys($bindings)) {
+            $keys = implode(', ', $missing);
+
+            throw new InvalidArgumentException("Invalid filter bindings. Missing: [$keys] keys.");
+        }
+
+        $this->filters[$type][] = $bindings;
+
+        return $this;
+    }
+
+    /**
+     * Extract any missing required binding keys.
+     *
+     * @param array $bindings
+     *
+     * @return array
+     */
+    protected function missingBindingKeys($bindings)
+    {
+        $required = array_flip(['field', 'operator', 'value']);
+
+        $existing = array_intersect_key($required, $bindings);
+
+        return array_keys(array_diff_key($required, $existing));
+    }
+
+    /**
+     * Get all the filters on the query.
+     *
+     * @return array
+     */
+    public function getFilters()
+    {
+        return $this->filters;
+    }
+
+    /**
+     * Clear the query filters.
+     *
+     * @return $this
+     */
+    public function clearFilters()
+    {
+        foreach (array_keys($this->filters) as $type) {
+            $this->filters[$type] = [];
+        }
+
+        return $this;
+    }
+
+    /**
+     * Determine if the query has attributes selected.
+     *
+     * @return bool
+     */
+    public function hasSelects()
+    {
+        return count($this->columns) > 0;
+    }
+
+    /**
+     * Get the attributes to select on the search.
+     *
+     * @return array
+     */
+    public function getSelects()
+    {
+        $selects = $this->columns ?? ['*'];
+
+        if (in_array('*', $selects)) {
+            return $selects;
+        }
+
+        if (in_array('objectclass', $selects)) {
+            return $selects;
+        }
+
+        // If the * character is not provided in the selected columns,
+        // we need to ensure we always select the object class, as
+        // this is used for constructing models properly.
+        $selects[] = 'objectclass';
+
+        return $selects;
+    }
+
+    /**
+     * Set the query to search on the base distinguished name.
+     *
+     * This will result in one record being returned.
+     *
+     * @return $this
+     */
+    public function read()
+    {
+        $this->type = 'read';
+
+        return $this;
+    }
+
+    /**
+     * Set the query to search one level on the base distinguished name.
+     *
+     * @return $this
+     */
+    public function listing()
+    {
+        $this->type = 'listing';
+
+        return $this;
+    }
+
+    /**
+     * Set the query to search the entire directory on the base distinguished name.
+     *
+     * @return $this
+     */
+    public function recursive()
+    {
+        $this->type = 'search';
+
+        return $this;
+    }
+
+    /**
+     * Whether to mark the current query as nested.
+     *
+     * @param bool $nested
+     *
+     * @return $this
+     */
+    public function nested($nested = true)
+    {
+        $this->nested = (bool) $nested;
+
+        return $this;
+    }
+
+    /**
+     * Enables caching on the current query until the given date.
+     *
+     * If flushing is enabled, the query cache will be flushed and then re-cached.
+     *
+     * @param DateTimeInterface $until When to expire the query cache.
+     * @param bool              $flush Whether to force-flush the query cache.
+     *
+     * @return $this
+     */
+    public function cache(DateTimeInterface $until = null, $flush = false)
+    {
+        $this->caching = true;
+        $this->cacheUntil = $until;
+        $this->flushCache = $flush;
+
+        return $this;
+    }
+
+    /**
+     * Determine if the query is nested.
+     *
+     * @return bool
+     */
+    public function isNested()
+    {
+        return $this->nested === true;
+    }
+
+    /**
+     * Determine whether the query is paginated.
+     *
+     * @return bool
+     */
+    public function isPaginated()
+    {
+        return $this->paginated;
+    }
+
+    /**
+     * Insert an entry into the directory.
+     *
+     * @param string $dn
+     * @param array  $attributes
+     *
+     * @throws LdapRecordException
+     *
+     * @return bool
+     */
+    public function insert($dn, array $attributes)
+    {
+        if (empty($dn)) {
+            throw new LdapRecordException('A new LDAP object must have a distinguished name (dn).');
+        }
+
+        if (! array_key_exists('objectclass', $attributes)) {
+            throw new LdapRecordException(
+                'A new LDAP object must contain at least one object class (objectclass) to be created.'
+            );
+        }
+
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+            return $ldap->add($dn, $attributes);
+        });
+    }
+
+    /**
+     * Create attributes on the entry in the directory.
+     *
+     * @param string $dn
+     * @param array  $attributes
+     *
+     * @return bool
+     */
+    public function insertAttributes($dn, array $attributes)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+            return $ldap->modAdd($dn, $attributes);
+        });
+    }
+
+    /**
+     * Update the entry with the given modifications.
+     *
+     * @param string $dn
+     * @param array  $modifications
+     *
+     * @return bool
+     */
+    public function update($dn, array $modifications)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn, $modifications) {
+            return $ldap->modifyBatch($dn, $modifications);
+        });
+    }
+
+    /**
+     * Update an entries attribute in the directory.
+     *
+     * @param string $dn
+     * @param array  $attributes
+     *
+     * @return bool
+     */
+    public function updateAttributes($dn, array $attributes)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+            return $ldap->modReplace($dn, $attributes);
+        });
+    }
+
+    /**
+     * Delete an entry from the directory.
+     *
+     * @param string $dn
+     *
+     * @return bool
+     */
+    public function delete($dn)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn) {
+            return $ldap->delete($dn);
+        });
+    }
+
+    /**
+     * Delete attributes on the entry in the directory.
+     *
+     * @param string $dn
+     * @param array  $attributes
+     *
+     * @return bool
+     */
+    public function deleteAttributes($dn, array $attributes)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+            return $ldap->modDelete($dn, $attributes);
+        });
+    }
+
+    /**
+     * Rename an entry in the directory.
+     *
+     * @param string $dn
+     * @param string $rdn
+     * @param string $newParentDn
+     * @param bool   $deleteOldRdn
+     *
+     * @return bool
+     */
+    public function rename($dn, $rdn, $newParentDn, $deleteOldRdn = true)
+    {
+        return $this->connection->run(function (LdapInterface $ldap) use ($dn, $rdn, $newParentDn, $deleteOldRdn) {
+            return $ldap->rename($dn, $rdn, $newParentDn, $deleteOldRdn);
+        });
+    }
+
+    /**
+     * Handle dynamic method calls on the query builder.
+     *
+     * @param string $method
+     * @param array  $parameters
+     *
+     * @throws BadMethodCallException
+     *
+     * @return mixed
+     */
+    public function __call($method, $parameters)
+    {
+        // If the beginning of the method being called contains
+        // 'where', we will assume a dynamic 'where' clause is
+        // being performed and pass the parameters to it.
+        if (substr($method, 0, 5) === 'where') {
+            return $this->dynamicWhere($method, $parameters);
+        }
+
+        throw new BadMethodCallException(sprintf(
+            'Call to undefined method %s::%s()',
+            static::class,
+            $method
+        ));
+    }
+
+    /**
+     * Handles dynamic "where" clauses to the query.
+     *
+     * @param string $method
+     * @param array  $parameters
+     *
+     * @return $this
+     */
+    public function dynamicWhere($method, $parameters)
+    {
+        $finder = substr($method, 5);
+
+        $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+        // The connector variable will determine which connector will be used for the
+        // query condition. We will change it as we come across new boolean values
+        // in the dynamic method strings, which could contain a number of these.
+        $connector = 'and';
+
+        $index = 0;
+
+        foreach ($segments as $segment) {
+            // If the segment is not a boolean connector, we can assume it is a column's name
+            // and we will add it to the query as a new constraint as a where clause, then
+            // we can keep iterating through the dynamic method string's segments again.
+            if ($segment != 'And' && $segment != 'Or') {
+                $this->addDynamic($segment, $connector, $parameters, $index);
+
+                $index++;
+            }
+
+            // Otherwise, we will store the connector so we know how the next where clause we
+            // find in the query should be connected to the previous ones, meaning we will
+            // have the proper boolean connector to connect the next where clause found.
+            else {
+                $connector = $segment;
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Adds an array of wheres to the current query.
+     *
+     * @param array  $wheres
+     * @param string $boolean
+     * @param bool   $raw
+     *
+     * @return $this
+     */
+    protected function addArrayOfWheres($wheres, $boolean, $raw)
+    {
+        foreach ($wheres as $key => $value) {
+            if (is_numeric($key) && is_array($value)) {
+                // If the key is numeric and the value is an array, we'll
+                // assume we've been given an array with conditionals.
+                [$field, $condition] = $value;
+
+                // Since a value is optional for some conditionals, we will
+                // try and retrieve the third parameter from the array,
+                // but is entirely optional.
+                $value = Arr::get($value, 2);
+
+                $this->where($field, $condition, $value, $boolean);
+            } else {
+                // If the value is not an array, we will assume an equals clause.
+                $this->where($key, '=', $value, $boolean, $raw);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add a single dynamic where clause statement to the query.
+     *
+     * @param string $segment
+     * @param string $connector
+     * @param array  $parameters
+     * @param int    $index
+     *
+     * @return void
+     */
+    protected function addDynamic($segment, $connector, $parameters, $index)
+    {
+        // If no parameters were given to the dynamic where clause,
+        // we can assume a "has" attribute filter is being added.
+        if (count($parameters) === 0) {
+            $this->where(strtolower($segment), '*', null, strtolower($connector));
+        } else {
+            $this->where(strtolower($segment), '=', $parameters[$index], strtolower($connector));
+        }
+    }
+
+    /**
+     * Logs the given executed query information by firing its query event.
+     *
+     * @param Builder    $query
+     * @param string     $type
+     * @param null|float $time
+     *
+     * @return void
+     */
+    protected function logQuery($query, $type, $time = null)
+    {
+        $args = [$query, $time];
+
+        switch ($type) {
+            case 'listing':
+                $event = new Events\Listing(...$args);
+                break;
+            case 'read':
+                $event = new Events\Read(...$args);
+                break;
+            case 'chunk':
+                $event = new Events\Chunk(...$args);
+                break;
+            case 'paginate':
+                $event = new Events\Paginate(...$args);
+                break;
+            default:
+                $event = new Events\Search(...$args);
+                break;
+        }
+
+        $this->fireQueryEvent($event);
+    }
+
+    /**
+     * Fires the given query event.
+     *
+     * @param QueryExecuted $event
+     *
+     * @return void
+     */
+    protected function fireQueryEvent(QueryExecuted $event)
+    {
+        Container::getInstance()->getEventDispatcher()->fire($event);
+    }
+
+    /**
+     * Get the elapsed time since a given starting point.
+     *
+     * @param int $start
+     *
+     * @return float
+     */
+    protected function getElapsedTime($start)
+    {
+        return round((microtime(true) - $start) * 1000, 2);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Cache.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Cache.php
new file mode 100644
index 0000000..dfbf8cd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Cache.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use Closure;
+use DateInterval;
+use DateTimeInterface;
+use Psr\SimpleCache\CacheInterface;
+
+class Cache
+{
+    use InteractsWithTime;
+
+    /**
+     * The cache driver.
+     *
+     * @var CacheInterface
+     */
+    protected $store;
+
+    /**
+     * Constructor.
+     *
+     * @param CacheInterface $store
+     */
+    public function __construct(CacheInterface $store)
+    {
+        $this->store = $store;
+    }
+
+    /**
+     * Get an item from the cache.
+     *
+     * @param string $key
+     *
+     * @return mixed
+     */
+    public function get($key)
+    {
+        return $this->store->get($key);
+    }
+
+    /**
+     * Store an item in the cache.
+     *
+     * @param string                                  $key
+     * @param mixed                                   $value
+     * @param DateTimeInterface|DateInterval|int|null $ttl
+     *
+     * @return bool
+     */
+    public function put($key, $value, $ttl = null)
+    {
+        $seconds = $this->secondsUntil($ttl);
+
+        if ($seconds <= 0) {
+            return $this->delete($key);
+        }
+
+        return $this->store->set($key, $value, $seconds);
+    }
+
+    /**
+     * Get an item from the cache, or execute the given Closure and store the result.
+     *
+     * @param string                                  $key
+     * @param DateTimeInterface|DateInterval|int|null $ttl
+     * @param Closure                                 $callback
+     *
+     * @return mixed
+     */
+    public function remember($key, $ttl, Closure $callback)
+    {
+        $value = $this->get($key);
+
+        if (! is_null($value)) {
+            return $value;
+        }
+
+        $this->put($key, $value = $callback(), $ttl);
+
+        return $value;
+    }
+
+    /**
+     * Delete an item from the cache.
+     *
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function delete($key)
+    {
+        return $this->store->delete($key);
+    }
+
+    /**
+     * Get the underlying cache store.
+     *
+     * @return CacheInterface
+     */
+    public function store()
+    {
+        return $this->store;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Collection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Collection.php
new file mode 100644
index 0000000..a02146d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Collection.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use LdapRecord\Models\Model;
+use Tightenco\Collect\Support\Collection as BaseCollection;
+
+class Collection extends BaseCollection
+{
+    /**
+     * @inheritdoc
+     */
+    protected function valueRetriever($value)
+    {
+        if ($this->useAsCallable($value)) {
+            return $value;
+        }
+
+        return function ($item) use ($value) {
+            return $item instanceof Model
+                ? $item->getFirstAttribute($value)
+                : data_get($item, $value);
+        };
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Chunk.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Chunk.php
new file mode 100644
index 0000000..3cd36d8
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Chunk.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Chunk extends QueryExecuted
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Listing.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Listing.php
new file mode 100644
index 0000000..2b88ad9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Listing.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Listing extends QueryExecuted
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Paginate.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Paginate.php
new file mode 100644
index 0000000..6f8f262
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Paginate.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Paginate extends QueryExecuted
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/QueryExecuted.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/QueryExecuted.php
new file mode 100644
index 0000000..f13ddeb
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/QueryExecuted.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+use LdapRecord\Query\Builder;
+
+class QueryExecuted
+{
+    /**
+     * The LDAP filter that was used for the query.
+     *
+     * @var string
+     */
+    protected $query;
+
+    /**
+     * The number of milliseconds it took to execute the query.
+     *
+     * @var float
+     */
+    protected $time;
+
+    /**
+     * Constructor.
+     *
+     * @param Builder    $query
+     * @param null|float $time
+     */
+    public function __construct(Builder $query, $time = null)
+    {
+        $this->query = $query;
+        $this->time = $time;
+    }
+
+    /**
+     * Returns the LDAP filter that was used for the query.
+     *
+     * @return Builder
+     */
+    public function getQuery()
+    {
+        return $this->query;
+    }
+
+    /**
+     * Returns the number of milliseconds it took to execute the query.
+     *
+     * @return float|null
+     */
+    public function getTime()
+    {
+        return $this->time;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Read.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Read.php
new file mode 100644
index 0000000..510c4ca
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Read.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Read extends QueryExecuted
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Search.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Search.php
new file mode 100644
index 0000000..5132316
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Search.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Search extends QueryExecuted
+{
+    //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Grammar.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Grammar.php
new file mode 100644
index 0000000..3217173
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Grammar.php
@@ -0,0 +1,549 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use UnexpectedValueException;
+
+class Grammar
+{
+    /**
+     * The query operators and their method names.
+     *
+     * @var array
+     */
+    public $operators = [
+        '*' => 'has',
+        '!*' => 'notHas',
+        '=' => 'equals',
+        '!' => 'doesNotEqual',
+        '!=' => 'doesNotEqual',
+        '>=' => 'greaterThanOrEquals',
+        '<=' => 'lessThanOrEquals',
+        '~=' => 'approximatelyEquals',
+        'starts_with' => 'startsWith',
+        'not_starts_with' => 'notStartsWith',
+        'ends_with' => 'endsWith',
+        'not_ends_with' => 'notEndsWith',
+        'contains' => 'contains',
+        'not_contains' => 'notContains',
+    ];
+
+    /**
+     * The query wrapper.
+     *
+     * @var string|null
+     */
+    protected $wrapper;
+
+    /**
+     * Get all the available operators.
+     *
+     * @return array
+     */
+    public function getOperators()
+    {
+        return array_keys($this->operators);
+    }
+
+    /**
+     * Wraps a query string in brackets.
+     *
+     * Produces: (query)
+     *
+     * @param string $query
+     * @param string $prefix
+     * @param string $suffix
+     *
+     * @return string
+     */
+    public function wrap($query, $prefix = '(', $suffix = ')')
+    {
+        return $prefix.$query.$suffix;
+    }
+
+    /**
+     * Compiles the Builder instance into an LDAP query string.
+     *
+     * @param Builder $query
+     *
+     * @return string
+     */
+    public function compile(Builder $query)
+    {
+        if ($this->queryMustBeWrapped($query)) {
+            $this->wrapper = 'and';
+        }
+
+        $filter = $this->compileRaws($query)
+            .$this->compileWheres($query)
+            .$this->compileOrWheres($query);
+
+        switch ($this->wrapper) {
+            case 'and':
+                return $this->compileAnd($filter);
+            case 'or':
+                return $this->compileOr($filter);
+            default:
+                return $filter;
+        }
+    }
+
+    /**
+     * Determine if the query must be wrapped in an encapsulating statement.
+     *
+     * @param Builder $query
+     *
+     * @return bool
+     */
+    protected function queryMustBeWrapped(Builder $query)
+    {
+        return ! $query->isNested() && $this->hasMultipleFilters($query);
+    }
+
+    /**
+     * Assembles all of the "raw" filters on the query.
+     *
+     * @param Builder $builder
+     *
+     * @return string
+     */
+    protected function compileRaws(Builder $builder)
+    {
+        return $this->concatenate($builder->filters['raw']);
+    }
+
+    /**
+     * Assembles all where clauses in the current wheres property.
+     *
+     * @param Builder $builder
+     * @param string  $type
+     *
+     * @return string
+     */
+    protected function compileWheres(Builder $builder, $type = 'and')
+    {
+        $filter = '';
+
+        foreach ($builder->filters[$type] as $where) {
+            $filter .= $this->compileWhere($where);
+        }
+
+        return $filter;
+    }
+
+    /**
+     * Assembles all or where clauses in the current orWheres property.
+     *
+     * @param Builder $query
+     *
+     * @return string
+     */
+    protected function compileOrWheres(Builder $query)
+    {
+        $filter = $this->compileWheres($query, 'or');
+
+        if (! $this->hasMultipleFilters($query)) {
+            return $filter;
+        }
+
+        // Here we will detect whether the entire query can be
+        // wrapped inside of an "or" statement by checking
+        // how many filter statements exist for each type.
+        if ($this->queryCanBeWrappedInSingleOrStatement($query)) {
+            $this->wrapper = 'or';
+        } else {
+            $filter = $this->compileOr($filter);
+        }
+
+        return $filter;
+    }
+
+    /**
+     * Determine if the query can be wrapped in a single or statement.
+     *
+     * @param Builder $query
+     *
+     * @return bool
+     */
+    protected function queryCanBeWrappedInSingleOrStatement(Builder $query)
+    {
+        return $this->has($query, 'or', '>=', 1) &&
+            $this->has($query, 'and', '<=', 1) &&
+            $this->has($query, 'raw', '=', 0);
+    }
+
+    /**
+     * Concatenates filters into a single string.
+     *
+     * @param array $bindings
+     *
+     * @return string
+     */
+    public function concatenate(array $bindings = [])
+    {
+        // Filter out empty query segments.
+        return implode(
+            array_filter($bindings, [$this, 'bindingValueIsNotEmpty'])
+        );
+    }
+
+    /**
+     * Determine if the binding value is not empty.
+     *
+     * @param string $value
+     *
+     * @return bool
+     */
+    protected function bindingValueIsNotEmpty($value)
+    {
+        return ! empty($value);
+    }
+
+    /**
+     * Determine if the query is using multiple filters.
+     *
+     * @param Builder $query
+     *
+     * @return bool
+     */
+    protected function hasMultipleFilters(Builder $query)
+    {
+        return $this->has($query, ['and', 'or', 'raw'], '>', 1);
+    }
+
+    /**
+     * Determine if the query contains the given filter statement type.
+     *
+     * @param Builder      $query
+     * @param string|array $type
+     * @param string       $operator
+     * @param int          $count
+     *
+     * @return bool
+     */
+    protected function has(Builder $query, $type, $operator = '>=', $count = 1)
+    {
+        $types = (array) $type;
+
+        $filters = 0;
+
+        foreach ($types as $type) {
+            $filters += count($query->filters[$type]);
+        }
+
+        switch ($operator) {
+            case '>':
+                return $filters > $count;
+            case '>=':
+                return $filters >= $count;
+            case '<':
+                return $filters < $count;
+            case '<=':
+                return $filters <= $count;
+            default:
+                return $filters == $count;
+        }
+    }
+
+    /**
+     * Returns a query string for equals.
+     *
+     * Produces: (field=value)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileEquals($field, $value)
+    {
+        return $this->wrap($field.'='.$value);
+    }
+
+    /**
+     * Returns a query string for does not equal.
+     *
+     * Produces: (!(field=value))
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileDoesNotEqual($field, $value)
+    {
+        return $this->compileNot(
+            $this->compileEquals($field, $value)
+        );
+    }
+
+    /**
+     * Alias for does not equal operator (!=) operator.
+     *
+     * Produces: (!(field=value))
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileDoesNotEqualAlias($field, $value)
+    {
+        return $this->compileDoesNotEqual($field, $value);
+    }
+
+    /**
+     * Returns a query string for greater than or equals.
+     *
+     * Produces: (field>=value)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileGreaterThanOrEquals($field, $value)
+    {
+        return $this->wrap("$field>=$value");
+    }
+
+    /**
+     * Returns a query string for less than or equals.
+     *
+     * Produces: (field<=value)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileLessThanOrEquals($field, $value)
+    {
+        return $this->wrap("$field<=$value");
+    }
+
+    /**
+     * Returns a query string for approximately equals.
+     *
+     * Produces: (field~=value)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileApproximatelyEquals($field, $value)
+    {
+        return $this->wrap("$field~=$value");
+    }
+
+    /**
+     * Returns a query string for starts with.
+     *
+     * Produces: (field=value*)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileStartsWith($field, $value)
+    {
+        return $this->wrap("$field=$value*");
+    }
+
+    /**
+     * Returns a query string for does not start with.
+     *
+     * Produces: (!(field=*value))
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileNotStartsWith($field, $value)
+    {
+        return $this->compileNot(
+            $this->compileStartsWith($field, $value)
+        );
+    }
+
+    /**
+     * Returns a query string for ends with.
+     *
+     * Produces: (field=*value)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileEndsWith($field, $value)
+    {
+        return $this->wrap("$field=*$value");
+    }
+
+    /**
+     * Returns a query string for does not end with.
+     *
+     * Produces: (!(field=value*))
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileNotEndsWith($field, $value)
+    {
+        return $this->compileNot($this->compileEndsWith($field, $value));
+    }
+
+    /**
+     * Returns a query string for contains.
+     *
+     * Produces: (field=*value*)
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileContains($field, $value)
+    {
+        return $this->wrap("$field=*$value*");
+    }
+
+    /**
+     * Returns a query string for does not contain.
+     *
+     * Produces: (!(field=*value*))
+     *
+     * @param string $field
+     * @param string $value
+     *
+     * @return string
+     */
+    public function compileNotContains($field, $value)
+    {
+        return $this->compileNot(
+            $this->compileContains($field, $value)
+        );
+    }
+
+    /**
+     * Returns a query string for a where has.
+     *
+     * Produces: (field=*)
+     *
+     * @param string $field
+     *
+     * @return string
+     */
+    public function compileHas($field)
+    {
+        return $this->wrap("$field=*");
+    }
+
+    /**
+     * Returns a query string for a where does not have.
+     *
+     * Produces: (!(field=*))
+     *
+     * @param string $field
+     *
+     * @return string
+     */
+    public function compileNotHas($field)
+    {
+        return $this->compileNot(
+            $this->compileHas($field)
+        );
+    }
+
+    /**
+     * Wraps the inserted query inside an AND operator.
+     *
+     * Produces: (&query)
+     *
+     * @param string $query
+     *
+     * @return string
+     */
+    public function compileAnd($query)
+    {
+        return $query ? $this->wrap($query, '(&') : '';
+    }
+
+    /**
+     * Wraps the inserted query inside an OR operator.
+     *
+     * Produces: (|query)
+     *
+     * @param string $query
+     *
+     * @return string
+     */
+    public function compileOr($query)
+    {
+        return $query ? $this->wrap($query, '(|') : '';
+    }
+
+    /**
+     * Wraps the inserted query inside an NOT operator.
+     *
+     * @param string $query
+     *
+     * @return string
+     */
+    public function compileNot($query)
+    {
+        return $query ? $this->wrap($query, '(!') : '';
+    }
+
+    /**
+     * Assembles a single where query.
+     *
+     * @param array $where
+     *
+     * @throws UnexpectedValueException
+     *
+     * @return string
+     */
+    protected function compileWhere(array $where)
+    {
+        $method = $this->makeCompileMethod($where['operator']);
+
+        return $this->{$method}($where['field'], $where['value']);
+    }
+
+    /**
+     * Make the compile method name for the operator.
+     *
+     * @param string $operator
+     *
+     * @throws UnexpectedValueException
+     *
+     * @return string
+     */
+    protected function makeCompileMethod($operator)
+    {
+        if (! $this->operatorExists($operator)) {
+            throw new UnexpectedValueException("Invalid LDAP filter operator ['$operator']");
+        }
+
+        return 'compile'.ucfirst($this->operators[$operator]);
+    }
+
+    /**
+     * Determine if the operator exists.
+     *
+     * @param string $operator
+     *
+     * @return bool
+     */
+    protected function operatorExists($operator)
+    {
+        return array_key_exists($operator, $this->operators);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/InteractsWithTime.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/InteractsWithTime.php
new file mode 100644
index 0000000..1562ec0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/InteractsWithTime.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use Carbon\Carbon;
+use DateInterval;
+use DateTimeInterface;
+
+/**
+ * @author Taylor Otwell
+ *
+ * @see https://laravel.com
+ */
+trait InteractsWithTime
+{
+    /**
+     * Get the number of seconds until the given DateTime.
+     *
+     * @param DateTimeInterface|DateInterval|int $delay
+     *
+     * @return int
+     */
+    protected function secondsUntil($delay)
+    {
+        $delay = $this->parseDateInterval($delay);
+
+        return $delay instanceof DateTimeInterface
+            ? max(0, $delay->getTimestamp() - $this->currentTime())
+            : (int) $delay;
+    }
+
+    /**
+     * Get the "available at" UNIX timestamp.
+     *
+     * @param DateTimeInterface|DateInterval|int $delay
+     *
+     * @return int
+     */
+    protected function availableAt($delay = 0)
+    {
+        $delay = $this->parseDateInterval($delay);
+
+        return $delay instanceof DateTimeInterface
+            ? $delay->getTimestamp()
+            : Carbon::now()->addRealSeconds($delay)->getTimestamp();
+    }
+
+    /**
+     * If the given value is an interval, convert it to a DateTime instance.
+     *
+     * @param DateTimeInterface|DateInterval|int $delay
+     *
+     * @return DateTimeInterface|int
+     */
+    protected function parseDateInterval($delay)
+    {
+        if ($delay instanceof DateInterval) {
+            $delay = Carbon::now()->add($delay);
+        }
+
+        return $delay;
+    }
+
+    /**
+     * Get the current system time as a UNIX timestamp.
+     *
+     * @return int
+     */
+    protected function currentTime()
+    {
+        return Carbon::now()->getTimestamp();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/ActiveDirectoryBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/ActiveDirectoryBuilder.php
new file mode 100644
index 0000000..8923015
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/ActiveDirectoryBuilder.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+use Closure;
+use LdapRecord\LdapInterface;
+use LdapRecord\Models\Attributes\AccountControl;
+use LdapRecord\Models\ModelNotFoundException;
+
+class ActiveDirectoryBuilder extends Builder
+{
+    /**
+     * Finds a record by its Object SID.
+     *
+     * @param string       $sid
+     * @param array|string $columns
+     *
+     * @return \LdapRecord\Models\ActiveDirectory\Entry|static|null
+     */
+    public function findBySid($sid, $columns = [])
+    {
+        try {
+            return $this->findBySidOrFail($sid, $columns);
+        } catch (ModelNotFoundException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Finds a record by its Object SID.
+     *
+     * Fails upon no records returned.
+     *
+     * @param string       $sid
+     * @param array|string $columns
+     *
+     * @throws ModelNotFoundException
+     *
+     * @return \LdapRecord\Models\ActiveDirectory\Entry|static
+     */
+    public function findBySidOrFail($sid, $columns = [])
+    {
+        return $this->findByOrFail('objectsid', $sid, $columns);
+    }
+
+    /**
+     * Adds a enabled filter to the current query.
+     *
+     * @return $this
+     */
+    public function whereEnabled()
+    {
+        return $this->notFilter(function ($query) {
+            return $query->whereDisabled();
+        });
+    }
+
+    /**
+     * Adds a disabled filter to the current query.
+     *
+     * @return $this
+     */
+    public function whereDisabled()
+    {
+        return $this->rawFilter(
+            (new AccountControl())->accountIsDisabled()->filter()
+        );
+    }
+
+    /**
+     * Adds a 'where member' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function whereMember($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->whereEquals($attribute, $dn);
+        }, 'member', $nested);
+    }
+
+    /**
+     * Adds an 'or where member' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function orWhereMember($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->orWhereEquals($attribute, $dn);
+        }, 'member', $nested);
+    }
+
+    /**
+     * Adds a 'where member of' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function whereMemberOf($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->whereEquals($attribute, $dn);
+        }, 'memberof', $nested);
+    }
+
+    /**
+     * Adds a 'where not member of' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function whereNotMemberof($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->whereNotEquals($attribute, $dn);
+        }, 'memberof', $nested);
+    }
+
+    /**
+     * Adds an 'or where member of' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function orWhereMemberOf($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->orWhereEquals($attribute, $dn);
+        }, 'memberof', $nested);
+    }
+
+    /**
+     * Adds a 'or where not member of' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function orWhereNotMemberof($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->orWhereNotEquals($attribute, $dn);
+        }, 'memberof', $nested);
+    }
+
+    /**
+     * Adds a 'where manager' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function whereManager($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->whereEquals($attribute, $dn);
+        }, 'manager', $nested);
+    }
+
+    /**
+     * Adds a 'where not manager' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function whereNotManager($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->whereNotEquals($attribute, $dn);
+        }, 'manager', $nested);
+    }
+
+    /**
+     * Adds an 'or where manager' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function orWhereManager($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->orWhereEquals($attribute, $dn);
+        }, 'manager', $nested);
+    }
+
+    /**
+     * Adds an 'or where not manager' filter to the current query.
+     *
+     * @param string $dn
+     * @param bool   $nested
+     *
+     * @return $this
+     */
+    public function orWhereNotManager($dn, $nested = false)
+    {
+        return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+            return $this->orWhereNotEquals($attribute, $dn);
+        }, 'manager', $nested);
+    }
+
+    /**
+     * Execute the callback with a nested match attribute.
+     *
+     * @param Closure $callback
+     * @param string  $attribute
+     * @param bool    $nested
+     *
+     * @return $this
+     */
+    protected function nestedMatchQuery(Closure $callback, $attribute, $nested = false)
+    {
+        return $callback(
+            $nested ? $this->makeNestedMatchAttribute($attribute) : $attribute
+        );
+    }
+
+    /**
+     * Make a "nested match" filter attribute for querying descendants.
+     *
+     * @param string $attribute
+     *
+     * @return string
+     */
+    protected function makeNestedMatchAttribute($attribute)
+    {
+        return sprintf('%s:%s:', $attribute, LdapInterface::OID_MATCHING_RULE_IN_CHAIN);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/Builder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/Builder.php
new file mode 100644
index 0000000..eed5e91
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/Builder.php
@@ -0,0 +1,446 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+use Closure;
+use DateTime;
+use LdapRecord\Models\Model;
+use LdapRecord\Models\ModelNotFoundException;
+use LdapRecord\Models\Scope;
+use LdapRecord\Models\Types\ActiveDirectory;
+use LdapRecord\Query\Builder as BaseBuilder;
+use LdapRecord\Utilities;
+
+class Builder extends BaseBuilder
+{
+    /**
+     * The model being queried.
+     *
+     * @var Model
+     */
+    protected $model;
+
+    /**
+     * The global scopes to be applied.
+     *
+     * @var array
+     */
+    protected $scopes = [];
+
+    /**
+     * The removed global scopes.
+     *
+     * @var array
+     */
+    protected $removedScopes = [];
+
+    /**
+     * The applied global scopes.
+     *
+     * @var array
+     */
+    protected $appliedScopes = [];
+
+    /**
+     * Dynamically handle calls into the query instance.
+     *
+     * @param string $method
+     * @param array  $parameters
+     *
+     * @return mixed
+     */
+    public function __call($method, $parameters)
+    {
+        if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
+            return $this->callScope([$this->model, $scope], $parameters);
+        }
+
+        return parent::__call($method, $parameters);
+    }
+
+    /**
+     * Apply the given scope on the current builder instance.
+     *
+     * @param callable $scope
+     * @param array    $parameters
+     *
+     * @return mixed
+     */
+    protected function callScope(callable $scope, $parameters = [])
+    {
+        array_unshift($parameters, $this);
+
+        return $scope(...array_values($parameters)) ?? $this;
+    }
+
+    /**
+     * Get the attributes to select on the search.
+     *
+     * @return array
+     */
+    public function getSelects()
+    {
+        // Here we will ensure the models GUID attribute is always
+        // selected. In some LDAP directories, the attribute is
+        // virtual and must be requested for specifically.
+        return array_values(array_unique(
+            array_merge([$this->model->getGuidKey()], parent::getSelects())
+        ));
+    }
+
+    /**
+     * Set the model instance for the model being queried.
+     *
+     * @param Model $model
+     *
+     * @return $this
+     */
+    public function setModel(Model $model)
+    {
+        $this->model = $model;
+
+        return $this;
+    }
+
+    /**
+     * Returns the model being queried for.
+     *
+     * @return Model
+     */
+    public function getModel()
+    {
+        return $this->model;
+    }
+
+    /**
+     * Get a new model query builder instance.
+     *
+     * @param string|null $baseDn
+     *
+     * @return static
+     */
+    public function newInstance($baseDn = null)
+    {
+        return parent::newInstance($baseDn)->model($this->model);
+    }
+
+    /**
+     * Finds a model by its distinguished name.
+     *
+     * @param array|string          $dn
+     * @param array|string|string[] $columns
+     *
+     * @return Model|\LdapRecord\Query\Collection|static|null
+     */
+    public function find($dn, $columns = ['*'])
+    {
+        return $this->afterScopes(function () use ($dn, $columns) {
+            return parent::find($dn, $columns);
+        });
+    }
+
+    /**
+     * Finds a record using ambiguous name resolution.
+     *
+     * @param string|array $value
+     * @param array|string $columns
+     *
+     * @return Model|\LdapRecord\Query\Collection|static|null
+     */
+    public function findByAnr($value, $columns = ['*'])
+    {
+        if (is_array($value)) {
+            return $this->findManyByAnr($value, $columns);
+        }
+
+        // If the model is not compatible with ANR filters,
+        // we must construct an equivalent filter that
+        // the current LDAP server does support.
+        if (! $this->modelIsCompatibleWithAnr()) {
+            return $this->prepareAnrEquivalentQuery($value)->first($columns);
+        }
+
+        return $this->findBy('anr', $value, $columns);
+    }
+
+    /**
+     * Determine if the current model is compatible with ANR filters.
+     *
+     * @return bool
+     */
+    protected function modelIsCompatibleWithAnr()
+    {
+        return $this->model instanceof ActiveDirectory;
+    }
+
+    /**
+     * Finds a record using ambiguous name resolution.
+     *
+     * If a record is not found, an exception is thrown.
+     *
+     * @param string       $value
+     * @param array|string $columns
+     *
+     * @throws ModelNotFoundException
+     *
+     * @return Model
+     */
+    public function findByAnrOrFail($value, $columns = ['*'])
+    {
+        if (! $entry = $this->findByAnr($value, $columns)) {
+            $this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
+        }
+
+        return $entry;
+    }
+
+    /**
+     * Throws a not found exception.
+     *
+     * @param string $query
+     * @param string $dn
+     *
+     * @throws ModelNotFoundException
+     */
+    protected function throwNotFoundException($query, $dn)
+    {
+        throw ModelNotFoundException::forQuery($query, $dn);
+    }
+
+    /**
+     * Finds multiple records using ambiguous name resolution.
+     *
+     * @param array $values
+     * @param array $columns
+     *
+     * @return \LdapRecord\Query\Collection
+     */
+    public function findManyByAnr(array $values = [], $columns = ['*'])
+    {
+        $this->select($columns);
+
+        if (! $this->modelIsCompatibleWithAnr()) {
+            foreach ($values as $value) {
+                $this->prepareAnrEquivalentQuery($value);
+            }
+
+            return $this->get($columns);
+        }
+
+        return $this->findManyBy('anr', $values);
+    }
+
+    /**
+     * Creates an ANR equivalent query for LDAP distributions that do not support ANR.
+     *
+     * @param string $value
+     *
+     * @return $this
+     */
+    protected function prepareAnrEquivalentQuery($value)
+    {
+        return $this->orFilter(function (self $query) use ($value) {
+            foreach ($this->model->getAnrAttributes() as $attribute) {
+                $query->whereEquals($attribute, $value);
+            }
+        });
+    }
+
+    /**
+     * Finds a record by its string GUID.
+     *
+     * @param string       $guid
+     * @param array|string $columns
+     *
+     * @return Model|static|null
+     */
+    public function findByGuid($guid, $columns = ['*'])
+    {
+        try {
+            return $this->findByGuidOrFail($guid, $columns);
+        } catch (ModelNotFoundException $e) {
+            return;
+        }
+    }
+
+    /**
+     * Finds a record by its string GUID.
+     *
+     * Fails upon no records returned.
+     *
+     * @param string       $guid
+     * @param array|string $columns
+     *
+     * @throws ModelNotFoundException
+     *
+     * @return Model|static
+     */
+    public function findByGuidOrFail($guid, $columns = ['*'])
+    {
+        if ($this->model instanceof ActiveDirectory) {
+            $guid = Utilities::stringGuidToHex($guid);
+        }
+
+        return $this->whereRaw([
+            $this->model->getGuidKey() => $guid,
+        ])->firstOrFail($columns);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getQuery()
+    {
+        return $this->afterScopes(function () {
+            return parent::getQuery();
+        });
+    }
+
+    /**
+     * Apply the query scopes and execute the callback.
+     *
+     * @param Closure $callback
+     *
+     * @return mixed
+     */
+    protected function afterScopes(Closure $callback)
+    {
+        $this->applyScopes();
+
+        return $callback();
+    }
+
+    /**
+     * Apply the global query scopes.
+     *
+     * @return $this
+     */
+    public function applyScopes()
+    {
+        if (! $this->scopes) {
+            return $this;
+        }
+
+        foreach ($this->scopes as $identifier => $scope) {
+            if (isset($this->appliedScopes[$identifier])) {
+                continue;
+            }
+
+            $scope instanceof Scope
+                ? $scope->apply($this, $this->getModel())
+                : $scope($this);
+
+            $this->appliedScopes[$identifier] = $scope;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Register a new global scope.
+     *
+     * @param string         $identifier
+     * @param Scope|\Closure $scope
+     *
+     * @return $this
+     */
+    public function withGlobalScope($identifier, $scope)
+    {
+        $this->scopes[$identifier] = $scope;
+
+        return $this;
+    }
+
+    /**
+     * Remove a registered global scope.
+     *
+     * @param Scope|string $scope
+     *
+     * @return $this
+     */
+    public function withoutGlobalScope($scope)
+    {
+        if (! is_string($scope)) {
+            $scope = get_class($scope);
+        }
+
+        unset($this->scopes[$scope]);
+
+        $this->removedScopes[] = $scope;
+
+        return $this;
+    }
+
+    /**
+     * Remove all or passed registered global scopes.
+     *
+     * @param array|null $scopes
+     *
+     * @return $this
+     */
+    public function withoutGlobalScopes(array $scopes = null)
+    {
+        if (! is_array($scopes)) {
+            $scopes = array_keys($this->scopes);
+        }
+
+        foreach ($scopes as $scope) {
+            $this->withoutGlobalScope($scope);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get an array of global scopes that were removed from the query.
+     *
+     * @return array
+     */
+    public function removedScopes()
+    {
+        return $this->removedScopes;
+    }
+
+    /**
+     * Get an array of the global scopes that were applied to the query.
+     *
+     * @return array
+     */
+    public function appliedScopes()
+    {
+        return $this->appliedScopes;
+    }
+
+    /**
+     * Processes and converts the given LDAP results into models.
+     *
+     * @param array $results
+     *
+     * @return \LdapRecord\Query\Collection
+     */
+    protected function process(array $results)
+    {
+        return $this->model->hydrate(parent::process($results));
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function prepareWhereValue($field, $value, $raw = false)
+    {
+        if ($value instanceof DateTime) {
+            $field = $this->model->normalizeAttributeKey($field);
+
+            if (! $this->model->isDateAttribute($field)) {
+                throw new \UnexpectedValueException(
+                    "Cannot convert field [$field] to an LDAP timestamp. You must add this field as a model date."
+                    .' Refer to https://ldaprecord.com/docs/model-mutators/#date-mutators'
+                );
+            }
+
+            $value = $this->model->fromDateTime($this->model->getDates()[$field], $value);
+        }
+
+        return parent::prepareWhereValue($field, $value, $raw);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/FreeIpaBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/FreeIpaBuilder.php
new file mode 100644
index 0000000..5e3d43f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/FreeIpaBuilder.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+class FreeIpaBuilder extends Builder
+{
+    /**
+     * Adds a enabled filter to the current query.
+     *
+     * @return $this
+     */
+    public function whereEnabled()
+    {
+        return $this->rawFilter('(!(pwdAccountLockedTime=*))');
+    }
+
+    /**
+     * Adds a disabled filter to the current query.
+     *
+     * @return $this
+     */
+    public function whereDisabled()
+    {
+        return $this->rawFilter('(pwdAccountLockedTime=*)');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/OpenLdapBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/OpenLdapBuilder.php
new file mode 100644
index 0000000..dd41344
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/OpenLdapBuilder.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+class OpenLdapBuilder extends Builder
+{
+    /**
+     * Adds a enabled filter to the current query.
+     *
+     * @return $this
+     */
+    public function whereEnabled()
+    {
+        return $this->rawFilter('(!(pwdAccountLockedTime=*))');
+    }
+
+    /**
+     * Adds a disabled filter to the current query.
+     *
+     * @return $this
+     */
+    public function whereDisabled()
+    {
+        return $this->rawFilter('(pwdAccountLockedTime=*)');
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ObjectNotFoundException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ObjectNotFoundException.php
new file mode 100644
index 0000000..b2dec28
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ObjectNotFoundException.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use LdapRecord\LdapRecordException;
+
+class ObjectNotFoundException extends LdapRecordException
+{
+    /**
+     * The query filter that was used.
+     *
+     * @var string
+     */
+    protected $query;
+
+    /**
+     * The base DN of the query that was used.
+     *
+     * @var string
+     */
+    protected $baseDn;
+
+    /**
+     * Create a new exception for the executed filter.
+     *
+     * @param string $query
+     * @param null   $baseDn
+     *
+     * @return static
+     */
+    public static function forQuery($query, $baseDn = null)
+    {
+        return (new static())->setQuery($query, $baseDn);
+    }
+
+    /**
+     * Set the query that was used.
+     *
+     * @param string      $query
+     * @param string|null $baseDn
+     *
+     * @return $this
+     */
+    public function setQuery($query, $baseDn = null)
+    {
+        $this->query = $query;
+        $this->baseDn = $baseDn;
+        $this->message = "No LDAP query results for filter: [$query] in: [$baseDn]";
+
+        return $this;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/AbstractPaginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/AbstractPaginator.php
new file mode 100644
index 0000000..3dfd3f1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/AbstractPaginator.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+use LdapRecord\Query\Builder;
+
+abstract class AbstractPaginator
+{
+    /**
+     * The query builder instance.
+     *
+     * @var Builder
+     */
+    protected $query;
+
+    /**
+     * The filter to execute.
+     *
+     * @var string
+     */
+    protected $filter;
+
+    /**
+     * The amount of objects to fetch per page.
+     *
+     * @var int
+     */
+    protected $perPage;
+
+    /**
+     * Whether the operation is critical.
+     *
+     * @var bool
+     */
+    protected $isCritical;
+
+    /**
+     * Constructor.
+     *
+     * @param Builder $query
+     */
+    public function __construct(Builder $query, $filter, $perPage, $isCritical)
+    {
+        $this->query = $query;
+        $this->filter = $filter;
+        $this->perPage = $perPage;
+        $this->isCritical = $isCritical;
+    }
+
+    /**
+     * Execute the pagination request.
+     *
+     * @param LdapInterface $ldap
+     *
+     * @return array
+     */
+    public function execute(LdapInterface $ldap)
+    {
+        $pages = [];
+
+        $this->prepareServerControls();
+
+        do {
+            $this->applyServerControls($ldap);
+
+            if (! $resource = $this->query->run($this->filter)) {
+                break;
+            }
+
+            $this->updateServerControls($ldap, $resource);
+
+            $pages[] = $this->query->parse($resource);
+        } while (! empty($this->fetchCookie()));
+
+        $this->resetServerControls($ldap);
+
+        return $pages;
+    }
+
+    /**
+     * Fetch the pagination cookie.
+     *
+     * @return string
+     */
+    abstract protected function fetchCookie();
+
+    /**
+     * Prepare the server controls before executing the pagination request.
+     *
+     * @return void
+     */
+    abstract protected function prepareServerControls();
+
+    /**
+     * Apply the server controls.
+     *
+     * @param LdapInterface $ldap
+     *
+     * @return void
+     */
+    abstract protected function applyServerControls(LdapInterface $ldap);
+
+    /**
+     * Reset the server controls.
+     *
+     * @param LdapInterface $ldap
+     *
+     * @return mixed
+     */
+    abstract protected function resetServerControls(LdapInterface $ldap);
+
+    /**
+     * Update the server controls.
+     *
+     * @param LdapInterface $ldap
+     * @param resource      $resource
+     *
+     * @return void
+     */
+    abstract protected function updateServerControls(LdapInterface $ldap, $resource);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/DeprecatedPaginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/DeprecatedPaginator.php
new file mode 100644
index 0000000..b4a7f8d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/DeprecatedPaginator.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+
+/**
+ * @deprecated since v2.5.0
+ */
+class DeprecatedPaginator extends AbstractPaginator
+{
+    /**
+     * The pagination cookie.
+     *
+     * @var string
+     */
+    protected $cookie = '';
+
+    /**
+     * @inheritdoc
+     */
+    protected function fetchCookie()
+    {
+        return $this->cookie;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function prepareServerControls()
+    {
+        $this->cookie = '';
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function applyServerControls(LdapInterface $ldap)
+    {
+        $ldap->controlPagedResult($this->perPage, $this->isCritical, $this->cookie);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function updateServerControls(LdapInterface $ldap, $resource)
+    {
+        $ldap->controlPagedResultResponse($resource, $this->cookie);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function resetServerControls(LdapInterface $ldap)
+    {
+        $ldap->controlPagedResult();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/LazyPaginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/LazyPaginator.php
new file mode 100644
index 0000000..2974b8f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/LazyPaginator.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+
+class LazyPaginator extends Paginator
+{
+    /**
+     * Execute the pagination request.
+     *
+     * @param LdapInterface $ldap
+     *
+     * @return Generator
+     */
+    public function execute(LdapInterface $ldap)
+    {
+        $this->prepareServerControls();
+
+        do {
+            $this->applyServerControls($ldap);
+
+            if (! $resource = $this->query->run($this->filter)) {
+                break;
+            }
+
+            $this->updateServerControls($ldap, $resource);
+
+            yield $this->query->parse($resource);
+        } while (! empty($this->fetchCookie()));
+
+        $this->resetServerControls($ldap);
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/Paginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/Paginator.php
new file mode 100644
index 0000000..9ab6e67
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/Paginator.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+
+class Paginator extends AbstractPaginator
+{
+    /**
+     * @inheritdoc
+     */
+    protected function fetchCookie()
+    {
+        return $this->query->controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? null;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function prepareServerControls()
+    {
+        $this->query->addControl(LDAP_CONTROL_PAGEDRESULTS, $this->isCritical, [
+            'size' => $this->perPage, 'cookie' => '',
+        ]);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function applyServerControls(LdapInterface $ldap)
+    {
+        $ldap->setOption(LDAP_OPT_SERVER_CONTROLS, $this->query->controls);
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function updateServerControls(LdapInterface $ldap, $resource)
+    {
+        $errorCode = $dn = $errorMessage = $refs = null;
+
+        $ldap->parseResult(
+            $resource,
+            $errorCode,
+            $dn,
+            $errorMessage,
+            $refs,
+            $this->query->controls
+        );
+
+        $this->resetPageSize();
+    }
+
+    /**
+     * Reset the page control page size.
+     *
+     * @return void
+     */
+    protected function resetPageSize()
+    {
+        $this->query->controls[LDAP_CONTROL_PAGEDRESULTS]['value']['size'] = $this->perPage;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function resetServerControls(LdapInterface $ldap)
+    {
+        $this->query->controls = [];
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Arr.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Arr.php
new file mode 100644
index 0000000..8fa87a2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Arr.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace LdapRecord\Support;
+
+use ArrayAccess;
+
+class Arr
+{
+    /**
+     * Determine whether the given value is array accessible.
+     *
+     * @param mixed $value
+     *
+     * @return bool
+     */
+    public static function accessible($value)
+    {
+        return is_array($value) || $value instanceof ArrayAccess;
+    }
+
+    /**
+     * Determine if the given key exists in the provided array.
+     *
+     * @param \ArrayAccess|array $array
+     * @param string|int         $key
+     *
+     * @return bool
+     */
+    public static function exists($array, $key)
+    {
+        if ($array instanceof ArrayAccess) {
+            return $array->offsetExists($key);
+        }
+
+        return array_key_exists($key, $array);
+    }
+
+    /**
+     * If the given value is not an array and not null, wrap it in one.
+     *
+     * @param mixed $value
+     *
+     * @return array
+     */
+    public static function wrap($value)
+    {
+        if (is_null($value)) {
+            return [];
+        }
+
+        return is_array($value) ? $value : [$value];
+    }
+
+    /**
+     * Return the first element in an array passing a given truth test.
+     *
+     * @param iterable      $array
+     * @param callable|null $callback
+     * @param mixed         $default
+     *
+     * @return mixed
+     */
+    public static function first($array, callable $callback = null, $default = null)
+    {
+        if (is_null($callback)) {
+            if (empty($array)) {
+                return Helpers::value($default);
+            }
+
+            foreach ($array as $item) {
+                return $item;
+            }
+        }
+
+        foreach ($array as $key => $value) {
+            if ($callback($value, $key)) {
+                return $value;
+            }
+        }
+
+        return Helpers::value($default);
+    }
+
+    /**
+     * Return the last element in an array passing a given truth test.
+     *
+     * @param array         $array
+     * @param callable|null $callback
+     * @param mixed         $default
+     *
+     * @return mixed
+     */
+    public static function last($array, callable $callback = null, $default = null)
+    {
+        if (is_null($callback)) {
+            return empty($array) ? Helpers::value($default) : end($array);
+        }
+
+        return static::first(array_reverse($array, true), $callback, $default);
+    }
+
+    /**
+     * Get an item from an array using "dot" notation.
+     *
+     * @param ArrayAccess|array $array
+     * @param string|int|null   $key
+     * @param mixed             $default
+     *
+     * @return mixed
+     */
+    public static function get($array, $key, $default = null)
+    {
+        if (! static::accessible($array)) {
+            return Helpers::value($default);
+        }
+
+        if (is_null($key)) {
+            return $array;
+        }
+
+        if (static::exists($array, $key)) {
+            return $array[$key];
+        }
+
+        if (strpos($key, '.') === false) {
+            return $array[$key] ?? Helpers::value($default);
+        }
+
+        foreach (explode('.', $key) as $segment) {
+            if (static::accessible($array) && static::exists($array, $segment)) {
+                $array = $array[$segment];
+            } else {
+                return Helpers::value($default);
+            }
+        }
+
+        return $array;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Helpers.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Helpers.php
new file mode 100644
index 0000000..a55d1d2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Helpers.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace LdapRecord\Support;
+
+use Closure;
+
+class Helpers
+{
+    /**
+     * Return the default value of the given value.
+     *
+     * @param mixed $value
+     *
+     * @return mixed
+     */
+    public static function value($value)
+    {
+        return $value instanceof Closure ? $value() : $value;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/AuthGuardFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/AuthGuardFake.php
new file mode 100644
index 0000000..4a69150
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/AuthGuardFake.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\Auth\Guard;
+
+class AuthGuardFake extends Guard
+{
+    /**
+     * Always allow binding as configured user.
+     *
+     * @return bool
+     */
+    public function bindAsConfiguredUser()
+    {
+        return true;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/ConnectionFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/ConnectionFake.php
new file mode 100644
index 0000000..0aa12a1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/ConnectionFake.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\Auth\Guard;
+use LdapRecord\Connection;
+use LdapRecord\Models\Model;
+
+class ConnectionFake extends Connection
+{
+    /**
+     * The underlying fake LDAP connection.
+     *
+     * @var LdapFake
+     */
+    protected $ldap;
+
+    /**
+     * Whether the fake is connected.
+     *
+     * @var bool
+     */
+    protected $connected = false;
+
+    /**
+     * Make a new fake LDAP connection instance.
+     *
+     * @param array  $config
+     * @param string $ldap
+     *
+     * @return static
+     */
+    public static function make(array $config = [], $ldap = LdapFake::class)
+    {
+        $connection = new static($config, new $ldap());
+
+        $connection->configure();
+
+        return $connection;
+    }
+
+    /**
+     * Set the user to authenticate as.
+     *
+     * @param Model|string $user
+     *
+     * @return $this
+     */
+    public function actingAs($user)
+    {
+        $this->ldap->shouldAuthenticateWith(
+            $user instanceof Model ? $user->getDn() : $user
+        );
+
+        return $this;
+    }
+
+    /**
+     * Set the connection to bypass bind attempts as the configured user.
+     *
+     * @return $this
+     */
+    public function shouldBeConnected()
+    {
+        $this->connected = true;
+
+        $this->authGuardResolver = function () {
+            return new AuthGuardFake($this->ldap, $this->configuration);
+        };
+
+        return $this;
+    }
+
+    /**
+     * Set the connection to attempt binding as the configured user.
+     *
+     * @return $this
+     */
+    public function shouldNotBeConnected()
+    {
+        $this->connected = false;
+
+        $this->authGuardResolver = function () {
+            return new Guard($this->ldap, $this->configuration);
+        };
+
+        return $this;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isConnected()
+    {
+        return $this->connected ?: parent::isConnected();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/DirectoryFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/DirectoryFake.php
new file mode 100644
index 0000000..70640af
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/DirectoryFake.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\Container;
+
+class DirectoryFake
+{
+    /**
+     * Setup the fake connection.
+     *
+     * @param string|null $name
+     *
+     * @throws \LdapRecord\ContainerException
+     *
+     * @return ConnectionFake
+     */
+    public static function setup($name = null)
+    {
+        $connection = Container::getConnection($name);
+
+        $fake = static::makeConnectionFake(
+            $connection->getConfiguration()->all()
+        );
+
+        // Replace the connection with a fake.
+        Container::addConnection($fake, $name);
+
+        return $fake;
+    }
+
+    /**
+     * Reset the container.
+     *
+     * @return void
+     */
+    public static function tearDown()
+    {
+        Container::reset();
+    }
+
+    /**
+     * Make a connection fake.
+     *
+     * @param array $config
+     *
+     * @return ConnectionFake
+     */
+    public static function makeConnectionFake(array $config = [])
+    {
+        return ConnectionFake::make($config)->shouldBeConnected();
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapExpectation.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapExpectation.php
new file mode 100644
index 0000000..90a5fa2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapExpectation.php
@@ -0,0 +1,303 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\LdapRecordException;
+use PHPUnit\Framework\Constraint\Constraint;
+use PHPUnit\Framework\Constraint\IsEqual;
+use UnexpectedValueException;
+
+class LdapExpectation
+{
+    /**
+     * The value to return from the expectation.
+     *
+     * @var mixed
+     */
+    protected $value;
+
+    /**
+     * The exception to throw from the expectation.
+     *
+     * @var null|LdapRecordException|\Exception
+     */
+    protected $exception;
+
+    /**
+     * The amount of times the expectation should be called.
+     *
+     * @var int
+     */
+    protected $count = 1;
+
+    /**
+     * The method that the expectation belongs to.
+     *
+     * @var string
+     */
+    protected $method;
+
+    /**
+     * The methods argument's.
+     *
+     * @var array
+     */
+    protected $args = [];
+
+    /**
+     * Whether the same expectation should be returned indefinitely.
+     *
+     * @var bool
+     */
+    protected $indefinitely = true;
+
+    /**
+     * Whether the expectation should return errors.
+     *
+     * @var bool
+     */
+    protected $errors = false;
+
+    /**
+     * The error number to return.
+     *
+     * @var int
+     */
+    protected $errorCode = 1;
+
+    /**
+     * The last error string to return.
+     *
+     * @var string
+     */
+    protected $errorMessage = '';
+
+    /**
+     * The diagnostic message string to return.
+     *
+     * @var string
+     */
+    protected $errorDiagnosticMessage = '';
+
+    /**
+     * Constructor.
+     *
+     * @param string $method
+     */
+    public function __construct($method)
+    {
+        $this->method = $method;
+    }
+
+    /**
+     * Set the arguments that the operation should receive.
+     *
+     * @param mixed $args
+     *
+     * @return $this
+     */
+    public function with($args)
+    {
+        $args = is_array($args) ? $args : func_get_args();
+
+        foreach ($args as $key => $arg) {
+            if (! $arg instanceof Constraint) {
+                $args[$key] = new IsEqual($arg);
+            }
+        }
+
+        $this->args = $args;
+
+        return $this;
+    }
+
+    /**
+     * Set the expected value to return.
+     *
+     * @param mixed $value
+     *
+     * @return $this
+     */
+    public function andReturn($value)
+    {
+        $this->value = $value;
+
+        return $this;
+    }
+
+    /**
+     * The error message to return from the expectation.
+     *
+     * @param int    $code
+     * @param string $error
+     * @param string $diagnosticMessage
+     *
+     * @return $this
+     */
+    public function andReturnError($code = 1, $error = '', $diagnosticMessage = '')
+    {
+        $this->errors = true;
+
+        $this->errorCode = $code;
+        $this->errorMessage = $error;
+        $this->errorDiagnosticMessage = $diagnosticMessage;
+
+        return $this;
+    }
+
+    /**
+     * Set the expected exception to throw.
+     *
+     * @param string|\Exception|LdapRecordException $exception
+     *
+     * @return $this
+     */
+    public function andThrow($exception)
+    {
+        if (is_string($exception)) {
+            $exception = new LdapRecordException($exception);
+        }
+
+        $this->exception = $exception;
+
+        return $this;
+    }
+
+    /**
+     * Set the expectation to be only called once.
+     *
+     * @return $this
+     */
+    public function once()
+    {
+        return $this->times(1);
+    }
+
+    /**
+     * Set the expectation to be only called twice.
+     *
+     * @return $this
+     */
+    public function twice()
+    {
+        return $this->times(2);
+    }
+
+    /**
+     * Set the expectation to be called the given number of times.
+     *
+     * @param int $count
+     *
+     * @return $this
+     */
+    public function times($count = 1)
+    {
+        $this->indefinitely = false;
+
+        $this->count = $count;
+
+        return $this;
+    }
+
+    /**
+     * Get the method the expectation belongs to.
+     *
+     * @return string
+     */
+    public function getMethod()
+    {
+        if (is_null($this->method)) {
+            throw new UnexpectedValueException('An expectation must have a method.');
+        }
+
+        return $this->method;
+    }
+
+    /**
+     * Get the expected call count.
+     *
+     * @return int
+     */
+    public function getExpectedCount()
+    {
+        return $this->count;
+    }
+
+    /**
+     * Get the expected arguments.
+     *
+     * @return Constraint[]
+     */
+    public function getExpectedArgs()
+    {
+        return $this->args;
+    }
+
+    /**
+     * Get the expected exception.
+     *
+     * @return null|\Exception|LdapRecordException
+     */
+    public function getExpectedException()
+    {
+        return $this->exception;
+    }
+
+    /**
+     * Get the expected value.
+     *
+     * @return mixed
+     */
+    public function getExpectedValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Determine whether the expectation is returning an error.
+     *
+     * @return bool
+     */
+    public function isReturningError()
+    {
+        return $this->errors;
+    }
+
+    /**
+     * @return int
+     */
+    public function getExpectedErrorCode()
+    {
+        return $this->errorCode;
+    }
+
+    /**
+     * @return string
+     */
+    public function getExpectedErrorMessage()
+    {
+        return $this->errorMessage;
+    }
+
+    /**
+     * @return string
+     */
+    public function getExpectedErrorDiagnosticMessage()
+    {
+        return $this->errorDiagnosticMessage;
+    }
+
+    /**
+     * Decrement the call count of the expectation.
+     *
+     * @return $this
+     */
+    public function decrementCallCount()
+    {
+        if (! $this->indefinitely) {
+            $this->count -= 1;
+        }
+
+        return $this;
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapFake.php
new file mode 100644
index 0000000..7ba0e15
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapFake.php
@@ -0,0 +1,525 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use Exception;
+use LdapRecord\DetailedError;
+use LdapRecord\DetectsErrors;
+use LdapRecord\HandlesConnection;
+use LdapRecord\LdapInterface;
+use LdapRecord\Support\Arr;
+use PHPUnit\Framework\Assert as PHPUnit;
+use PHPUnit\Framework\Constraint\Constraint;
+
+class LdapFake implements LdapInterface
+{
+    use HandlesConnection, DetectsErrors;
+
+    /**
+     * The expectations of the LDAP fake.
+     *
+     * @var array
+     */
+    protected $expectations = [];
+
+    /**
+     * The default fake error number.
+     *
+     * @var int
+     */
+    protected $errNo = 1;
+
+    /**
+     * The default fake last error string.
+     *
+     * @var string
+     */
+    protected $lastError = '';
+
+    /**
+     * The default fake diagnostic message string.
+     *
+     * @var string
+     */
+    protected $diagnosticMessage = '';
+
+    /**
+     * Create a new expected operation.
+     *
+     * @param string $method
+     *
+     * @return LdapExpectation
+     */
+    public static function operation($method)
+    {
+        return new LdapExpectation($method);
+    }
+
+    /**
+     * Set the user that will pass binding.
+     *
+     * @param string $dn
+     *
+     * @return $this
+     */
+    public function shouldAuthenticateWith($dn)
+    {
+        return $this->expect(
+            static::operation('bind')->with($dn, PHPUnit::anything())->andReturn(true)
+        );
+    }
+
+    /**
+     * Add an LDAP method expectation.
+     *
+     * @param LdapExpectation|array $expectations
+     *
+     * @return $this
+     */
+    public function expect($expectations = [])
+    {
+        $expectations = Arr::wrap($expectations);
+
+        foreach ($expectations as $key => $expectation) {
+            // If the key is non-numeric, we will assume
+            // that the string is the method name and
+            // the expectation is the return value.
+            if (! is_numeric($key)) {
+                $expectation = static::operation($key)->andReturn($expectation);
+            }
+
+            if (! $expectation instanceof LdapExpectation) {
+                $expectation = static::operation($expectation);
+            }
+
+            $this->expectations[$expectation->getMethod()][] = $expectation;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Determine if the method has any expectations.
+     *
+     * @param string $method
+     *
+     * @return bool
+     */
+    public function hasExpectations($method)
+    {
+        return count($this->getExpectations($method)) > 0;
+    }
+
+    /**
+     * Get expectations by method.
+     *
+     * @param string $method
+     *
+     * @return LdapExpectation[]|mixed
+     */
+    public function getExpectations($method)
+    {
+        return $this->expectations[$method] ?? [];
+    }
+
+    /**
+     * Remove an expectation by method and key.
+     *
+     * @param string $method
+     * @param int    $key
+     *
+     * @return void
+     */
+    public function removeExpectation($method, $key)
+    {
+        unset($this->expectations[$method][$key]);
+    }
+
+    /**
+     * Set the error number of a failed bind attempt.
+     *
+     * @param int $number
+     *
+     * @return $this
+     */
+    public function shouldReturnErrorNumber($number = 1)
+    {
+        $this->errNo = $number;
+
+        return $this;
+    }
+
+    /**
+     * Set the last error of a failed bind attempt.
+     *
+     * @param string $message
+     *
+     * @return $this
+     */
+    public function shouldReturnError($message = '')
+    {
+        $this->lastError = $message;
+
+        return $this;
+    }
+
+    /**
+     * Set the diagnostic message of a failed bind attempt.
+     *
+     * @param string $message
+     *
+     * @return $this
+     */
+    public function shouldReturnDiagnosticMessage($message = '')
+    {
+        $this->diagnosticMessage = $message;
+
+        return $this;
+    }
+
+    /**
+     * Return a fake error number.
+     *
+     * @return int
+     */
+    public function errNo()
+    {
+        return $this->errNo;
+    }
+
+    /**
+     * Return a fake error.
+     *
+     * @return string
+     */
+    public function getLastError()
+    {
+        return $this->lastError;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getDiagnosticMessage()
+    {
+        return $this->diagnosticMessage;
+    }
+
+    /**
+     * Return a fake detailed error.
+     *
+     * @return DetailedError
+     */
+    public function getDetailedError()
+    {
+        return new DetailedError(
+            $this->errNo(),
+            $this->getLastError(),
+            $this->getDiagnosticMessage()
+        );
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getEntries($searchResults)
+    {
+        return $searchResults;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isUsingSSL()
+    {
+        return $this->hasExpectations('isUsingSSL')
+            ? $this->resolveExpectation('isUsingSSL')
+            : $this->useSSL;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isUsingTLS()
+    {
+        return $this->hasExpectations('isUsingTLS')
+            ? $this->resolveExpectation('isUsingTLS')
+            : $this->useTLS;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function isBound()
+    {
+        return $this->hasExpectations('isBound')
+            ? $this->resolveExpectation('isBound')
+            : $this->bound;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function setOption($option, $value)
+    {
+        return $this->hasExpectations('setOption')
+            ? $this->resolveExpectation('setOption', func_get_args())
+            : true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getOption($option, &$value = null)
+    {
+        return $this->resolveExpectation('getOption', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function startTLS()
+    {
+        return $this->resolveExpectation('startTLS', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function connect($hosts = [], $port = 389)
+    {
+        $this->bound = false;
+
+        $this->host = $this->makeConnectionUris($hosts, $port);
+
+        return $this->connection = $this->hasExpectations('connect')
+            ? $this->resolveExpectation('connect', func_get_args())
+            : true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function close()
+    {
+        $this->connection = null;
+        $this->bound = false;
+        $this->host = null;
+
+        return $this->hasExpectations('close')
+            ? $this->resolveExpectation('close')
+            : true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function bind($username, $password)
+    {
+        return $this->bound = $this->resolveExpectation('bind', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+    {
+        return $this->resolveExpectation('search', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+    {
+        return $this->resolveExpectation('listing', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+    {
+        return $this->resolveExpectation('read', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
+    {
+        return $this->resolveExpectation('parseResult', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function add($dn, array $entry)
+    {
+        return $this->resolveExpectation('add', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function delete($dn)
+    {
+        return $this->resolveExpectation('delete', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
+    {
+        return $this->resolveExpectation('rename', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modify($dn, array $entry)
+    {
+        return $this->resolveExpectation('modify', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modifyBatch($dn, array $values)
+    {
+        return $this->resolveExpectation('modifyBatch', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modAdd($dn, array $entry)
+    {
+        return $this->resolveExpectation('modAdd', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modReplace($dn, array $entry)
+    {
+        return $this->resolveExpectation('modReplace', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function modDelete($dn, array $entry)
+    {
+        return $this->resolveExpectation('modDelete', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
+    {
+        return $this->resolveExpectation('controlPagedResult', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function controlPagedResultResponse($result, &$cookie)
+    {
+        return $this->resolveExpectation('controlPagedResultResponse', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function freeResult($result)
+    {
+        return $this->resolveExpectation('freeResult', func_get_args());
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function err2Str($number)
+    {
+        return $this->resolveExpectation('err2Str', func_get_args());
+    }
+
+    /**
+     * Resolve the methods expectations.
+     *
+     * @param string $method
+     * @param array  $args
+     *
+     * @throws Exception
+     *
+     * @return mixed
+     */
+    protected function resolveExpectation($method, array $args = [])
+    {
+        foreach ($this->getExpectations($method) as $key => $expectation) {
+            $this->assertMethodArgumentsMatch($method, $expectation->getExpectedArgs(), $args);
+
+            $expectation->decrementCallCount();
+
+            if ($expectation->getExpectedCount() === 0) {
+                $this->removeExpectation($method, $key);
+            }
+
+            if (! is_null($exception = $expectation->getExpectedException())) {
+                throw $exception;
+            }
+
+            if ($expectation->isReturningError()) {
+                $this->applyExpectationError($expectation);
+            }
+
+            return $expectation->getExpectedValue();
+        }
+
+        throw new Exception("LDAP method [$method] was unexpected.");
+    }
+
+    /**
+     * Apply the expectation error to the fake.
+     *
+     * @param LdapExpectation $expectation
+     *
+     * @return void
+     */
+    protected function applyExpectationError(LdapExpectation $expectation)
+    {
+        $this->shouldReturnError($expectation->getExpectedErrorMessage());
+        $this->shouldReturnErrorNumber($expectation->getExpectedErrorCode());
+        $this->shouldReturnDiagnosticMessage($expectation->getExpectedErrorDiagnosticMessage());
+    }
+
+    /**
+     * Assert that the expected arguments match the operations arguments.
+     *
+     * @param string       $method
+     * @param Constraint[] $expectedArgs
+     * @param array        $methodArgs
+     *
+     * @return void
+     */
+    protected function assertMethodArgumentsMatch($method, array $expectedArgs = [], array $methodArgs = [])
+    {
+        foreach ($expectedArgs as $key => $constraint) {
+            $argNumber = $key + 1;
+
+            PHPUnit::assertArrayHasKey(
+                $key,
+                $methodArgs,
+                "LDAP method [$method] argument #{$argNumber} does not exist."
+            );
+
+            $constraint->evaluate(
+                $methodArgs[$key],
+                "LDAP method [$method] expectation failed."
+            );
+        }
+    }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Utilities.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Utilities.php
new file mode 100644
index 0000000..0f0ca3c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Utilities.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace LdapRecord;
+
+class Utilities
+{
+    /**
+     * Converts a DN string into an array of RDNs.
+     *
+     * This will also decode hex characters into their true
+     * UTF-8 representation embedded inside the DN as well.
+     *
+     * @param string $dn
+     * @param bool   $removeAttributePrefixes
+     *
+     * @return array|false
+     */
+    public static function explodeDn($dn, $removeAttributePrefixes = true)
+    {
+        $dn = ldap_explode_dn($dn, ($removeAttributePrefixes ? 1 : 0));
+
+        if (! is_array($dn)) {
+            return false;
+        }
+
+        if (! array_key_exists('count', $dn)) {
+            return false;
+        }
+
+        unset($dn['count']);
+
+        foreach ($dn as $rdn => $value) {
+            $dn[$rdn] = static::unescape($value);
+        }
+
+        return $dn;
+    }
+
+    /**
+     * Un-escapes a hexadecimal string into its original string representation.
+     *
+     * @param string $value
+     *
+     * @return string
+     */
+    public static function unescape($value)
+    {
+        return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
+            return chr(hexdec($matches[1]));
+        }, $value);
+    }
+
+    /**
+     * Convert a binary SID to a string SID.
+     *
+     * @author Chad Sikorra
+     *
+     * @see https://github.com/ChadSikorra
+     * @see https://stackoverflow.com/questions/39533560/php-ldap-get-user-sid
+     *
+     * @param string $value The Binary SID
+     *
+     * @return string|null
+     */
+    public static function binarySidToString($value)
+    {
+        // Revision - 8bit unsigned int (C1)
+        // Count - 8bit unsigned int (C1)
+        // 2 null bytes
+        // ID - 32bit unsigned long, big-endian order
+        $sid = @unpack('C1rev/C1count/x2/N1id', $value);
+
+        if (! isset($sid['id']) || ! isset($sid['rev'])) {
+            return;
+        }
+
+        $revisionLevel = $sid['rev'];
+
+        $identifierAuthority = $sid['id'];
+
+        $subs = isset($sid['count']) ? $sid['count'] : 0;
+
+        $sidHex = $subs ? bin2hex($value) : '';
+
+        $subAuthorities = [];
+
+        // The sub-authorities depend on the count, so only get as
+        // many as the count, regardless of data beyond it.
+        for ($i = 0; $i < $subs; $i++) {
+            $data = implode(array_reverse(
+                str_split(
+                    substr($sidHex, 16 + ($i * 8), 8),
+                    2
+                )
+            ));
+
+            $subAuthorities[] = hexdec($data);
+        }
+
+        // Tack on the 'S-' and glue it all together...
+        return 'S-'.$revisionLevel.'-'.$identifierAuthority.implode(
+            preg_filter('/^/', '-', $subAuthorities)
+        );
+    }
+
+    /**
+     * Convert a binary GUID to a string GUID.
+     *
+     * @param string $binGuid
+     *
+     * @return string|null
+     */
+    public static function binaryGuidToString($binGuid)
+    {
+        if (trim($binGuid) == '' || is_null($binGuid)) {
+            return;
+        }
+
+        $hex = unpack('H*hex', $binGuid)['hex'];
+
+        $hex1 = substr($hex, -26, 2).substr($hex, -28, 2).substr($hex, -30, 2).substr($hex, -32, 2);
+        $hex2 = substr($hex, -22, 2).substr($hex, -24, 2);
+        $hex3 = substr($hex, -18, 2).substr($hex, -20, 2);
+        $hex4 = substr($hex, -16, 4);
+        $hex5 = substr($hex, -12, 12);
+
+        return sprintf('%s-%s-%s-%s-%s', $hex1, $hex2, $hex3, $hex4, $hex5);
+    }
+
+    /**
+     * Converts a string GUID to it's hex variant.
+     *
+     * @param string $string
+     *
+     * @return string
+     */
+    public static function stringGuidToHex($string)
+    {
+        $hex = '\\'.substr($string, 6, 2).'\\'.substr($string, 4, 2).'\\'.substr($string, 2, 2).'\\'.substr($string, 0, 2);
+        $hex = $hex.'\\'.substr($string, 11, 2).'\\'.substr($string, 9, 2);
+        $hex = $hex.'\\'.substr($string, 16, 2).'\\'.substr($string, 14, 2);
+        $hex = $hex.'\\'.substr($string, 19, 2).'\\'.substr($string, 21, 2);
+        $hex = $hex.'\\'.substr($string, 24, 2).'\\'.substr($string, 26, 2).'\\'.substr($string, 28, 2).'\\'.substr($string, 30, 2).'\\'.substr($string, 32, 2).'\\'.substr($string, 34, 2);
+
+        return $hex;
+    }
+
+    /**
+     * Round a Windows timestamp down to seconds and remove
+     * the seconds between 1601-01-01 and 1970-01-01.
+     *
+     * @param float $windowsTime
+     *
+     * @return float
+     */
+    public static function convertWindowsTimeToUnixTime($windowsTime)
+    {
+        return round($windowsTime / 10000000) - 11644473600;
+    }
+
+    /**
+     * Convert a Unix timestamp to Windows timestamp.
+     *
+     * @param float $unixTime
+     *
+     * @return float
+     */
+    public static function convertUnixTimeToWindowsTime($unixTime)
+    {
+        return ($unixTime + 11644473600) * 10000000;
+    }
+
+    /**
+     * Validates that the inserted string is an object SID.
+     *
+     * @param string $sid
+     *
+     * @return bool
+     */
+    public static function isValidSid($sid)
+    {
+        return (bool) preg_match("/^S-\d(-\d{1,10}){1,16}$/i", $sid);
+    }
+
+    /**
+     * Validates that the inserted string is an object GUID.
+     *
+     * @param string $guid
+     *
+     * @return bool
+     */
+    public static function isValidGuid($guid)
+    {
+        return (bool) preg_match('/^([0-9a-fA-F]){8}(-([0-9a-fA-F]){4}){3}-([0-9a-fA-F]){12}$/', $guid);
+    }
+}