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/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);
+    }
+}