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/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 = [];
+    }
+}