git subrepo commit (merge) mailcow/src/mailcow-dockerized
subrepo: subdir: "mailcow/src/mailcow-dockerized"
merged: "02ae5285"
upstream: origin: "https://github.com/mailcow/mailcow-dockerized.git"
branch: "master"
commit: "649a5c01"
git-subrepo: version: "0.4.3"
origin: "???"
commit: "???"
Change-Id: I870ad468fba026cc5abf3c5699ed1e12ff28b32b
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/BindException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/BindException.php
new file mode 100644
index 0000000..d87abc1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/BindException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use LdapRecord\LdapRecordException;
+
+class BindException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Attempting.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Attempting.php
new file mode 100644
index 0000000..3776401
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Attempting.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Attempting extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Binding.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Binding.php
new file mode 100644
index 0000000..faffd85
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Binding.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Binding extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Bound.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Bound.php
new file mode 100644
index 0000000..65a3fae
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Bound.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Bound extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php
new file mode 100644
index 0000000..83716b4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+use LdapRecord\LdapInterface;
+
+abstract class Event
+{
+ /**
+ * The connection that the username and password is being bound on.
+ *
+ * @var LdapInterface
+ */
+ protected $connection;
+
+ /**
+ * The username that is being used for binding.
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * The password that is being used for binding.
+ *
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * Constructor.
+ *
+ * @param LdapInterface $connection
+ * @param string $username
+ * @param string $password
+ */
+ public function __construct(LdapInterface $connection, $username, $password)
+ {
+ $this->connection = $connection;
+ $this->username = $username;
+ $this->password = $password;
+ }
+
+ /**
+ * Returns the events connection.
+ *
+ * @return LdapInterface
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Returns the authentication events username.
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Returns the authentication events password.
+ *
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Failed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Failed.php
new file mode 100644
index 0000000..7133e43
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Failed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Failed extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Passed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Passed.php
new file mode 100644
index 0000000..2442f3e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Passed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Auth\Events;
+
+class Passed extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php
new file mode 100644
index 0000000..696cc40
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use Exception;
+use LdapRecord\Auth\Events\Attempting;
+use LdapRecord\Auth\Events\Binding;
+use LdapRecord\Auth\Events\Bound;
+use LdapRecord\Auth\Events\Failed;
+use LdapRecord\Auth\Events\Passed;
+use LdapRecord\Configuration\DomainConfiguration;
+use LdapRecord\Events\DispatcherInterface;
+use LdapRecord\LdapInterface;
+
+class Guard
+{
+ /**
+ * The connection to bind to.
+ *
+ * @var LdapInterface
+ */
+ protected $connection;
+
+ /**
+ * The domain configuration to utilize.
+ *
+ * @var DomainConfiguration
+ */
+ protected $configuration;
+
+ /**
+ * The event dispatcher.
+ *
+ * @var DispatcherInterface
+ */
+ protected $events;
+
+ /**
+ * Constructor.
+ *
+ * @param LdapInterface $connection
+ * @param DomainConfiguration $configuration
+ */
+ public function __construct(LdapInterface $connection, DomainConfiguration $configuration)
+ {
+ $this->connection = $connection;
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * Attempt binding a user to the LDAP server.
+ *
+ * @param string $username
+ * @param string $password
+ * @param bool $stayBound
+ *
+ * @throws UsernameRequiredException
+ * @throws PasswordRequiredException
+ *
+ * @return bool
+ */
+ public function attempt($username, $password, $stayBound = false)
+ {
+ switch (true) {
+ case empty($username):
+ throw new UsernameRequiredException('A username must be specified.');
+ case empty($password):
+ throw new PasswordRequiredException('A password must be specified.');
+ }
+
+ $this->fireAttemptingEvent($username, $password);
+
+ try {
+ $this->bind($username, $password);
+
+ $authenticated = true;
+
+ $this->firePassedEvent($username, $password);
+ } catch (BindException $e) {
+ $authenticated = false;
+ }
+
+ if (! $stayBound) {
+ $this->bindAsConfiguredUser();
+ }
+
+ return $authenticated;
+ }
+
+ /**
+ * Attempt binding a user to the LDAP server. Supports anonymous binding.
+ *
+ * @param string|null $username
+ * @param string|null $password
+ *
+ * @throws BindException
+ * @throws \LdapRecord\ConnectionException
+ */
+ public function bind($username = null, $password = null)
+ {
+ $this->fireBindingEvent($username, $password);
+
+ // Prior to binding, we will upgrade our connectivity to TLS on our current
+ // connection and ensure we are not already bound before upgrading.
+ // This is to prevent subsequent upgrading on several binds.
+ if ($this->connection->isUsingTLS() && ! $this->connection->isBound()) {
+ $this->connection->startTLS();
+ }
+
+ try {
+ if (! $this->connection->bind($username, $password)) {
+ throw new Exception($this->connection->getLastError(), $this->connection->errNo());
+ }
+
+ $this->fireBoundEvent($username, $password);
+ } catch (Exception $e) {
+ $this->fireFailedEvent($username, $password);
+
+ throw BindException::withDetailedError($e, $this->connection->getDetailedError());
+ }
+ }
+
+ /**
+ * Bind to the LDAP server using the configured username and password.
+ *
+ * @throws BindException
+ * @throws \LdapRecord\ConnectionException
+ * @throws \LdapRecord\Configuration\ConfigurationException
+ */
+ public function bindAsConfiguredUser()
+ {
+ $this->bind(
+ $this->configuration->get('username'),
+ $this->configuration->get('password')
+ );
+ }
+
+ /**
+ * Get the event dispatcher instance.
+ *
+ * @return DispatcherInterface
+ */
+ public function getDispatcher()
+ {
+ return $this->events;
+ }
+
+ /**
+ * Set the event dispatcher instance.
+ *
+ * @param DispatcherInterface $dispatcher
+ *
+ * @return void
+ */
+ public function setDispatcher(DispatcherInterface $dispatcher)
+ {
+ $this->events = $dispatcher;
+ }
+
+ /**
+ * Fire the attempting event.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return void
+ */
+ protected function fireAttemptingEvent($username, $password)
+ {
+ if (isset($this->events)) {
+ $this->events->fire(new Attempting($this->connection, $username, $password));
+ }
+ }
+
+ /**
+ * Fire the passed event.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return void
+ */
+ protected function firePassedEvent($username, $password)
+ {
+ if (isset($this->events)) {
+ $this->events->fire(new Passed($this->connection, $username, $password));
+ }
+ }
+
+ /**
+ * Fire the failed event.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return void
+ */
+ protected function fireFailedEvent($username, $password)
+ {
+ if (isset($this->events)) {
+ $this->events->fire(new Failed($this->connection, $username, $password));
+ }
+ }
+
+ /**
+ * Fire the binding event.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return void
+ */
+ protected function fireBindingEvent($username, $password)
+ {
+ if (isset($this->events)) {
+ $this->events->fire(new Binding($this->connection, $username, $password));
+ }
+ }
+
+ /**
+ * Fire the bound event.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return void
+ */
+ protected function fireBoundEvent($username, $password)
+ {
+ if (isset($this->events)) {
+ $this->events->fire(new Bound($this->connection, $username, $password));
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/PasswordRequiredException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/PasswordRequiredException.php
new file mode 100644
index 0000000..7b2bbd1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/PasswordRequiredException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use LdapRecord\LdapRecordException;
+
+class PasswordRequiredException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/UsernameRequiredException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/UsernameRequiredException.php
new file mode 100644
index 0000000..838ae58
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/UsernameRequiredException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Auth;
+
+use LdapRecord\LdapRecordException;
+
+class UsernameRequiredException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/ConfigurationException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/ConfigurationException.php
new file mode 100644
index 0000000..6a93b12
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/ConfigurationException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Configuration;
+
+use LdapRecord\LdapRecordException;
+
+class ConfigurationException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php
new file mode 100644
index 0000000..1dcdd1a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace LdapRecord\Configuration;
+
+use LdapRecord\LdapInterface;
+
+class DomainConfiguration
+{
+ /**
+ * The extended configuration options.
+ *
+ * @var array
+ */
+ protected static $extended = [];
+
+ /**
+ * The configuration options array.
+ *
+ * The default values for each key indicate the type of value it requires.
+ *
+ * @var array
+ */
+ protected $options = [
+ // An array of LDAP hosts.
+ 'hosts' => [],
+
+ // The global LDAP operation timeout limit in seconds.
+ 'timeout' => 5,
+
+ // The LDAP version to utilize.
+ 'version' => 3,
+
+ // The port to use for connecting to your hosts.
+ 'port' => LdapInterface::PORT,
+
+ // The base distinguished name of your domain.
+ 'base_dn' => '',
+
+ // The username to use for binding.
+ 'username' => '',
+
+ // The password to use for binding.
+ 'password' => '',
+
+ // Whether or not to use SSL when connecting.
+ 'use_ssl' => false,
+
+ // Whether or not to use TLS when connecting.
+ 'use_tls' => false,
+
+ // Whether or not follow referrals is enabled when performing LDAP operations.
+ 'follow_referrals' => false,
+
+ // Custom LDAP options.
+ 'options' => [],
+ ];
+
+ /**
+ * Constructor.
+ *
+ * @param array $options
+ *
+ * @throws ConfigurationException When an option value given is an invalid type.
+ */
+ public function __construct(array $options = [])
+ {
+ $this->options = array_merge($this->options, static::$extended);
+
+ foreach ($options as $key => $value) {
+ $this->set($key, $value);
+ }
+ }
+
+ /**
+ * Extend the configuration with a custom option, or override an existing.
+ *
+ * @param string $option
+ * @param mixed $default
+ *
+ * @return void
+ */
+ public static function extend($option, $default = null)
+ {
+ static::$extended[$option] = $default;
+ }
+
+ /**
+ * Flush the extended configuration options.
+ *
+ * @return void
+ */
+ public static function flushExtended()
+ {
+ static::$extended = [];
+ }
+
+ /**
+ * Get all configuration options.
+ *
+ * @return array
+ */
+ public function all()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Set a configuration option.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @throws ConfigurationException When an option value given is an invalid type.
+ */
+ public function set($key, $value)
+ {
+ if ($this->validate($key, $value)) {
+ $this->options[$key] = $value;
+ }
+ }
+
+ /**
+ * Returns the value for the specified configuration options.
+ *
+ * @param string $key
+ *
+ * @throws ConfigurationException When the option specified does not exist.
+ *
+ * @return mixed
+ */
+ public function get($key)
+ {
+ if (! $this->has($key)) {
+ throw new ConfigurationException("Option {$key} does not exist.");
+ }
+
+ return $this->options[$key];
+ }
+
+ /**
+ * Checks if a configuration option exists.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function has($key)
+ {
+ return array_key_exists($key, $this->options);
+ }
+
+ /**
+ * Validate the configuration option.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @throws ConfigurationException When an option value given is an invalid type.
+ *
+ * @return bool
+ */
+ protected function validate($key, $value)
+ {
+ $default = $this->get($key);
+
+ if (is_array($default)) {
+ $validator = new Validators\ArrayValidator($key, $value);
+ } elseif (is_int($default)) {
+ $validator = new Validators\IntegerValidator($key, $value);
+ } elseif (is_bool($default)) {
+ $validator = new Validators\BooleanValidator($key, $value);
+ } else {
+ $validator = new Validators\StringOrNullValidator($key, $value);
+ }
+
+ return $validator->validate();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/ArrayValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/ArrayValidator.php
new file mode 100644
index 0000000..4aa43ed
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/ArrayValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class ArrayValidator extends Validator
+{
+ /**
+ * The validation exception message.
+ *
+ * @var string
+ */
+ protected $message = 'Option [:option] must be an array.';
+
+ /**
+ * @inheritdoc
+ */
+ public function passes()
+ {
+ return is_array($this->value);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/BooleanValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/BooleanValidator.php
new file mode 100644
index 0000000..1d25a4b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/BooleanValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class BooleanValidator extends Validator
+{
+ /**
+ * The validation exception message.
+ *
+ * @var string
+ */
+ protected $message = 'Option [:option] must be a boolean.';
+
+ /**
+ * @inheritdoc
+ */
+ public function passes()
+ {
+ return is_bool($this->value);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/IntegerValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/IntegerValidator.php
new file mode 100644
index 0000000..5c4f0f9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/IntegerValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class IntegerValidator extends Validator
+{
+ /**
+ * The validation exception message.
+ *
+ * @var string
+ */
+ protected $message = 'Option [:option] must be an integer.';
+
+ /**
+ * @inheritdoc
+ */
+ public function passes()
+ {
+ return is_numeric($this->value);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/StringOrNullValidator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/StringOrNullValidator.php
new file mode 100644
index 0000000..bc23372
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/StringOrNullValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+class StringOrNullValidator extends Validator
+{
+ /**
+ * The validation exception message.
+ *
+ * @var string
+ */
+ protected $message = 'Option [:option] must be a string or null.';
+
+ /**
+ * @inheritdoc
+ */
+ public function passes()
+ {
+ return is_string($this->value) || is_null($this->value);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php
new file mode 100644
index 0000000..908a639
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace LdapRecord\Configuration\Validators;
+
+use LdapRecord\Configuration\ConfigurationException;
+
+abstract class Validator
+{
+ /**
+ * The configuration key under validation.
+ *
+ * @var string
+ */
+ protected $key;
+
+ /**
+ * The configuration value under validation.
+ *
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * The validation exception message.
+ *
+ * @var string
+ */
+ protected $message;
+
+ /**
+ * Constructor.
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function __construct($key, $value)
+ {
+ $this->key = $key;
+ $this->value = $value;
+ }
+
+ /**
+ * Determine if the validation rule passes.
+ *
+ * @return bool
+ */
+ abstract public function passes();
+
+ /**
+ * Validate the configuration value.
+ *
+ * @throws ConfigurationException
+ *
+ * @return bool
+ */
+ public function validate()
+ {
+ if (! $this->passes()) {
+ $this->fail();
+ }
+
+ return true;
+ }
+
+ /**
+ * Throw a configuration exception.
+ *
+ * @throws ConfigurationException
+ *
+ * @return void
+ */
+ protected function fail()
+ {
+ throw new ConfigurationException(
+ str_replace(':option', $this->key, $this->message)
+ );
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php
new file mode 100644
index 0000000..8ba0ef1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php
@@ -0,0 +1,511 @@
+<?php
+
+namespace LdapRecord;
+
+use Carbon\Carbon;
+use Closure;
+use LdapRecord\Auth\Guard;
+use LdapRecord\Configuration\DomainConfiguration;
+use LdapRecord\Events\DispatcherInterface;
+use LdapRecord\Query\Builder;
+use LdapRecord\Query\Cache;
+use Psr\SimpleCache\CacheInterface;
+
+class Connection
+{
+ use DetectsErrors;
+
+ /**
+ * The underlying LDAP connection.
+ *
+ * @var Ldap
+ */
+ protected $ldap;
+
+ /**
+ * The cache driver.
+ *
+ * @var Cache|null
+ */
+ protected $cache;
+
+ /**
+ * The domain configuration.
+ *
+ * @var DomainConfiguration
+ */
+ protected $configuration;
+
+ /**
+ * The event dispatcher;.
+ *
+ * @var DispatcherInterface|null
+ */
+ protected $dispatcher;
+
+ /**
+ * The current host connected to.
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * The configured domain hosts.
+ *
+ * @var array
+ */
+ protected $hosts = [];
+
+ /**
+ * The attempted hosts that failed connecting to.
+ *
+ * @var array
+ */
+ protected $attempted = [];
+
+ /**
+ * The callback to execute upon total connection failure.
+ *
+ * @var Closure
+ */
+ protected $failed;
+
+ /**
+ * The authentication guard resolver.
+ *
+ * @var Closure
+ */
+ protected $authGuardResolver;
+
+ /**
+ * Whether the connection is retrying the initial connection attempt.
+ *
+ * @var bool
+ */
+ protected $retryingInitialConnection = false;
+
+ /**
+ * Constructor.
+ *
+ * @param array $config
+ * @param LdapInterface|null $ldap
+ */
+ public function __construct($config = [], LdapInterface $ldap = null)
+ {
+ $this->setConfiguration($config);
+
+ $this->setLdapConnection($ldap ?? new Ldap());
+
+ $this->failed = function () {
+ $this->dispatch(new Events\ConnectionFailed($this));
+ };
+
+ $this->authGuardResolver = function () {
+ return new Guard($this->ldap, $this->configuration);
+ };
+ }
+
+ /**
+ * Set the connection configuration.
+ *
+ * @param array $config
+ *
+ * @throws Configuration\ConfigurationException
+ *
+ * @return $this
+ */
+ public function setConfiguration($config = [])
+ {
+ $this->configuration = new DomainConfiguration($config);
+
+ $this->hosts = $this->configuration->get('hosts');
+
+ $this->host = reset($this->hosts);
+
+ return $this;
+ }
+
+ /**
+ * Set the LDAP connection.
+ *
+ * @param LdapInterface $ldap
+ *
+ * @return $this
+ */
+ public function setLdapConnection(LdapInterface $ldap)
+ {
+ $this->ldap = $ldap;
+
+ return $this;
+ }
+
+ /**
+ * Set the event dispatcher.
+ *
+ * @param DispatcherInterface $dispatcher
+ *
+ * @return $this
+ */
+ public function setDispatcher(DispatcherInterface $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+
+ return $this;
+ }
+
+ /**
+ * Initializes the LDAP connection.
+ *
+ * @return void
+ */
+ public function initialize()
+ {
+ $this->configure();
+
+ $this->ldap->connect($this->host, $this->configuration->get('port'));
+ }
+
+ /**
+ * Configure the LDAP connection.
+ *
+ * @return void
+ */
+ protected function configure()
+ {
+ if ($this->configuration->get('use_ssl')) {
+ $this->ldap->ssl();
+ } elseif ($this->configuration->get('use_tls')) {
+ $this->ldap->tls();
+ }
+
+ $this->ldap->setOptions(array_replace(
+ $this->configuration->get('options'),
+ [
+ LDAP_OPT_PROTOCOL_VERSION => $this->configuration->get('version'),
+ LDAP_OPT_NETWORK_TIMEOUT => $this->configuration->get('timeout'),
+ LDAP_OPT_REFERRALS => $this->configuration->get('follow_referrals'),
+ ]
+ ));
+ }
+
+ /**
+ * Set the cache store.
+ *
+ * @param CacheInterface $store
+ *
+ * @return $this
+ */
+ public function setCache(CacheInterface $store)
+ {
+ $this->cache = new Cache($store);
+
+ return $this;
+ }
+
+ /**
+ * Get the cache store.
+ *
+ * @return Cache|null
+ */
+ public function getCache()
+ {
+ return $this->cache;
+ }
+
+ /**
+ * Get the LDAP configuration instance.
+ *
+ * @return DomainConfiguration
+ */
+ public function getConfiguration()
+ {
+ return $this->configuration;
+ }
+
+ /**
+ * Get the LDAP connection instance.
+ *
+ * @return Ldap
+ */
+ public function getLdapConnection()
+ {
+ return $this->ldap;
+ }
+
+ /**
+ * Bind to the LDAP server.
+ *
+ * If no username or password is specified, then the configured credentials are used.
+ *
+ * @param string|null $username
+ * @param string|null $password
+ *
+ * @throws Auth\BindException
+ * @throws LdapRecordException
+ *
+ * @return Connection
+ */
+ public function connect($username = null, $password = null)
+ {
+ $attempt = function () use ($username, $password) {
+ $this->dispatch(new Events\Connecting($this));
+
+ is_null($username) && is_null($password)
+ ? $this->auth()->bindAsConfiguredUser()
+ : $this->auth()->bind($username, $password);
+
+ $this->dispatch(new Events\Connected($this));
+
+ $this->retryingInitialConnection = false;
+ };
+
+ try {
+ $this->runOperationCallback($attempt);
+ } catch (LdapRecordException $e) {
+ $this->retryingInitialConnection = true;
+
+ $this->retryOnNextHost($e, $attempt);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Reconnect to the LDAP server.
+ *
+ * @throws Auth\BindException
+ * @throws ConnectionException
+ *
+ * @return void
+ */
+ public function reconnect()
+ {
+ $this->reinitialize();
+
+ $this->connect();
+ }
+
+ /**
+ * Reinitialize the connection.
+ *
+ * @return void
+ */
+ protected function reinitialize()
+ {
+ $this->disconnect();
+
+ $this->initialize();
+ }
+
+ /**
+ * Disconnect from the LDAP server.
+ *
+ * @return void
+ */
+ public function disconnect()
+ {
+ $this->ldap->close();
+ }
+
+ /**
+ * Dispatch an event.
+ *
+ * @param object $event
+ *
+ * @return void
+ */
+ public function dispatch($event)
+ {
+ if (isset($this->dispatcher)) {
+ $this->dispatcher->dispatch($event);
+ }
+ }
+
+ /**
+ * Get the attempted hosts that failed connecting to.
+ *
+ * @return array
+ */
+ public function attempted()
+ {
+ return $this->attempted;
+ }
+
+ /**
+ * Perform the operation on the LDAP connection.
+ *
+ * @param Closure $operation
+ *
+ * @return mixed
+ */
+ public function run(Closure $operation)
+ {
+ try {
+ // Before running the operation, we will check if the current
+ // connection is bound and connect if necessary. Otherwise
+ // some LDAP operations will not be executed properly.
+ if (! $this->isConnected()) {
+ $this->connect();
+ }
+
+ return $this->runOperationCallback($operation);
+ } catch (LdapRecordException $e) {
+ if ($exception = $this->getExceptionForCauseOfFailure($e)) {
+ throw $exception;
+ }
+
+ return $this->tryAgainIfCausedByLostConnection($e, $operation);
+ }
+ }
+
+ /**
+ * Attempt to get an exception for the cause of failure.
+ *
+ * @param LdapRecordException $e
+ *
+ * @return mixed
+ */
+ protected function getExceptionForCauseOfFailure(LdapRecordException $e)
+ {
+ switch (true) {
+ case $this->errorContainsMessage($e->getMessage(), 'Already exists'):
+ return Exceptions\AlreadyExistsException::withDetailedError($e, $e->getDetailedError());
+ case $this->errorContainsMessage($e->getMessage(), 'Insufficient access'):
+ return Exceptions\InsufficientAccessException::withDetailedError($e, $e->getDetailedError());
+ case $this->errorContainsMessage($e->getMessage(), 'Constraint violation'):
+ return Exceptions\ConstraintViolationException::withDetailedError($e, $e->getDetailedError());
+ default:
+ return;
+ }
+ }
+
+ /**
+ * Run the operation callback on the current LDAP connection.
+ *
+ * @param Closure $operation
+ *
+ * @throws LdapRecordException
+ *
+ * @return mixed
+ */
+ protected function runOperationCallback(Closure $operation)
+ {
+ return $operation($this->ldap);
+ }
+
+ /**
+ * Get a new auth guard instance.
+ *
+ * @return Auth\Guard
+ */
+ public function auth()
+ {
+ if (! $this->ldap->isConnected()) {
+ $this->initialize();
+ }
+
+ $guard = call_user_func($this->authGuardResolver);
+
+ $guard->setDispatcher(
+ Container::getInstance()->getEventDispatcher()
+ );
+
+ return $guard;
+ }
+
+ /**
+ * Get a new query builder for the connection.
+ *
+ * @return Query\Builder
+ */
+ public function query()
+ {
+ return (new Builder($this))
+ ->setCache($this->cache)
+ ->setBaseDn($this->configuration->get('base_dn'));
+ }
+
+ /**
+ * Determine if the LDAP connection is bound.
+ *
+ * @return bool
+ */
+ public function isConnected()
+ {
+ return $this->ldap->isBound();
+ }
+
+ /**
+ * Attempt to retry an LDAP operation if due to a lost connection.
+ *
+ * @param LdapRecordException $e
+ * @param Closure $operation
+ *
+ * @throws LdapRecordException
+ *
+ * @return mixed
+ */
+ protected function tryAgainIfCausedByLostConnection(LdapRecordException $e, Closure $operation)
+ {
+ // If the operation failed due to a lost or failed connection,
+ // we'll attempt reconnecting and running the operation again
+ // underneath the same host, and then move onto the next.
+ if ($this->causedByLostConnection($e->getMessage())) {
+ return $this->retry($operation);
+ }
+
+ throw $e;
+ }
+
+ /**
+ * Retry the operation on the current host.
+ *
+ * @param Closure $operation
+ *
+ * @throws LdapRecordException
+ *
+ * @return mixed
+ */
+ protected function retry(Closure $operation)
+ {
+ try {
+ $this->retryingInitialConnection
+ ? $this->reinitialize()
+ : $this->reconnect();
+
+ return $this->runOperationCallback($operation);
+ } catch (LdapRecordException $e) {
+ return $this->retryOnNextHost($e, $operation);
+ }
+ }
+
+ /**
+ * Attempt the operation again on the next host.
+ *
+ * @param LdapRecordException $e
+ * @param Closure $operation
+ *
+ * @throws LdapRecordException
+ *
+ * @return mixed
+ */
+ protected function retryOnNextHost(LdapRecordException $e, Closure $operation)
+ {
+ $this->attempted[$this->host] = Carbon::now();
+
+ if (($key = array_search($this->host, $this->hosts)) !== false) {
+ unset($this->hosts[$key]);
+ }
+
+ if ($next = reset($this->hosts)) {
+ $this->host = $next;
+
+ return $this->tryAgainIfCausedByLostConnection($e, $operation);
+ }
+
+ call_user_func($this->failed, $this->ldap);
+
+ throw $e;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionException.php
new file mode 100644
index 0000000..81691bb
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord;
+
+class ConnectionException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php
new file mode 100644
index 0000000..0eacbc3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php
@@ -0,0 +1,320 @@
+<?php
+
+namespace LdapRecord;
+
+use BadMethodCallException;
+use LdapRecord\Events\Dispatcher;
+use LdapRecord\Events\DispatcherInterface;
+use LdapRecord\Events\Logger;
+use Psr\Log\LoggerInterface;
+
+class ConnectionManager
+{
+ /**
+ * The logger instance.
+ *
+ * @var LoggerInterface|null
+ */
+ protected $logger;
+
+ /**
+ * The event dispatcher instance.
+ *
+ * @var DispatcherInterface|null
+ */
+ protected $dispatcher;
+
+ /**
+ * The added LDAP connections.
+ *
+ * @var Connection[]
+ */
+ protected $connections = [];
+
+ /**
+ * The name of the default connection.
+ *
+ * @var string
+ */
+ protected $default = 'default';
+
+ /**
+ * The events to register listeners for during initialization.
+ *
+ * @var array
+ */
+ protected $listen = [
+ 'LdapRecord\Auth\Events\*',
+ 'LdapRecord\Query\Events\*',
+ 'LdapRecord\Models\Events\*',
+ ];
+
+ /**
+ * The method calls to proxy for compatibility.
+ *
+ * To be removed in the next major version.
+ *
+ * @var array
+ */
+ protected $proxy = [
+ 'reset' => 'flush',
+ 'addConnection' => 'add',
+ 'getConnection' => 'get',
+ 'allConnections' => 'all',
+ 'removeConnection' => 'remove',
+ 'getDefaultConnection' => 'getDefault',
+ 'setDefaultConnection' => 'setDefault',
+ 'getEventDispatcher' => 'dispatcher',
+ 'setEventDispatcher' => 'setDispatcher',
+ ];
+
+ /**
+ * Constructor.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ $this->dispatcher = new Dispatcher();
+ }
+
+ /**
+ * Forward missing method calls onto the instance.
+ *
+ * @param string $method
+ * @param mixed $args
+ *
+ * @return mixed
+ */
+ public function __call($method, $args)
+ {
+ $method = $this->proxy[$method] ?? $method;
+
+ if (! method_exists($this, $method)) {
+ throw new BadMethodCallException(sprintf(
+ 'Call to undefined method %s::%s()',
+ static::class,
+ $method
+ ));
+ }
+
+ return $this->{$method}(...$args);
+ }
+
+ /**
+ * Add a new connection.
+ *
+ * @param Connection $connection
+ * @param string|null $name
+ *
+ * @return $this
+ */
+ public function add(Connection $connection, $name = null)
+ {
+ $this->connections[$name ?? $this->default] = $connection;
+
+ if ($this->dispatcher) {
+ $connection->setDispatcher($this->dispatcher);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove a connection.
+ *
+ * @param $name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ unset($this->connections[$name]);
+
+ return $this;
+ }
+
+ /**
+ * Get all of the connections.
+ *
+ * @return Connection[]
+ */
+ public function all()
+ {
+ return $this->connections;
+ }
+
+ /**
+ * Get a connection by name or return the default.
+ *
+ * @param string|null $name
+ *
+ * @throws ContainerException If the given connection does not exist.
+ *
+ * @return Connection
+ */
+ public function get($name = null)
+ {
+ if ($this->exists($name = $name ?? $this->default)) {
+ return $this->connections[$name];
+ }
+
+ throw new ContainerException("The LDAP connection [$name] does not exist.");
+ }
+
+ /**
+ * Return the default connection.
+ *
+ * @return Connection
+ */
+ public function getDefault()
+ {
+ return $this->get($this->default);
+ }
+
+ /**
+ * Get the default connection name.
+ *
+ * @return string
+ */
+ public function getDefaultConnectionName()
+ {
+ return $this->default;
+ }
+
+ /**
+ * Checks if the connection exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function exists($name)
+ {
+ return array_key_exists($name, $this->connections);
+ }
+
+ /**
+ * Set the default connection name.
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setDefault($name = null)
+ {
+ $this->default = $name;
+
+ return $this;
+ }
+
+ /**
+ * Flush the manager of all instances and connections.
+ *
+ * @return $this
+ */
+ public function flush()
+ {
+ $this->logger = null;
+
+ $this->connections = [];
+
+ $this->dispatcher = new Dispatcher();
+
+ return $this;
+ }
+
+ /**
+ * Get the logger instance.
+ *
+ * @return LoggerInterface|null
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Set the event logger to use.
+ *
+ * @param LoggerInterface $logger
+ *
+ * @return void
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+
+ $this->initEventLogger();
+ }
+
+ /**
+ * Initialize the event logger.
+ *
+ * @return void
+ */
+ public function initEventLogger()
+ {
+ $logger = $this->newEventLogger();
+
+ foreach ($this->listen as $event) {
+ $this->dispatcher->listen($event, function ($eventName, $events) use ($logger) {
+ foreach ($events as $event) {
+ $logger->log($event);
+ }
+ });
+ }
+ }
+
+ /**
+ * Make a new event logger instance.
+ *
+ * @return Logger
+ */
+ protected function newEventLogger()
+ {
+ return new Logger($this->logger);
+ }
+
+ /**
+ * Unset the logger instance.
+ *
+ * @return void
+ */
+ public function unsetLogger()
+ {
+ $this->logger = null;
+ }
+
+ /**
+ * Get the event dispatcher.
+ *
+ * @return DispatcherInterface|null
+ */
+ public function dispatcher()
+ {
+ return $this->dispatcher;
+ }
+
+ /**
+ * Set the event dispatcher.
+ *
+ * @param DispatcherInterface $dispatcher
+ *
+ * @return void
+ */
+ public function setDispatcher(DispatcherInterface $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * Unset the event dispatcher.
+ *
+ * @return void
+ */
+ public function unsetEventDispatcher()
+ {
+ $this->dispatcher = null;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php
new file mode 100644
index 0000000..f458951
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace LdapRecord;
+
+/**
+ * @method static $this reset()
+ * @method static Connection[] all()
+ * @method static Connection[] allConnections()
+ * @method static Connection getDefaultConnection()
+ * @method static Connection get(string|null $name = null)
+ * @method static Connection getConnection(string|null $name = null)
+ * @method static bool exists(string $name)
+ * @method static $this remove(string|null $name = null)
+ * @method static $this removeConnection(string|null $name = null)
+ * @method static $this setDefault(string|null $name = null)
+ * @method static $this setDefaultConnection(string|null $name = null)
+ * @method static $this add(Connection $connection, string|null $name = null)
+ * @method static $this addConnection(Connection $connection, string|null $name = null)
+ */
+class Container
+{
+ /**
+ * The current container instance.
+ *
+ * @var Container
+ */
+ protected static $instance;
+
+ /**
+ * The connection manager instance.
+ *
+ * @var ConnectionManager
+ */
+ protected $manager;
+
+ /**
+ * The methods to passthru, for compatibility.
+ *
+ * @var array
+ */
+ protected $passthru = [
+ 'reset', 'flush',
+ 'add', 'addConnection',
+ 'remove', 'removeConnection',
+ 'setDefault', 'setDefaultConnection',
+ ];
+
+ /**
+ * Forward missing static calls onto the current instance.
+ *
+ * @param string $method
+ * @param mixed $args
+ *
+ * @return mixed
+ */
+ public static function __callStatic($method, $args)
+ {
+ return static::getInstance()->{$method}(...$args);
+ }
+
+ /**
+ * Get or set the current instance of the container.
+ *
+ * @return Container
+ */
+ public static function getInstance()
+ {
+ return static::$instance ?? static::getNewInstance();
+ }
+
+ /**
+ * Set the container instance.
+ *
+ * @param Container|null $container
+ *
+ * @return Container|null
+ */
+ public static function setInstance(self $container = null)
+ {
+ return static::$instance = $container;
+ }
+
+ /**
+ * Set and get a new instance of the container.
+ *
+ * @return Container
+ */
+ public static function getNewInstance()
+ {
+ return static::setInstance(new static());
+ }
+
+ /**
+ * Constructor.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ $this->manager = new ConnectionManager();
+ }
+
+ /**
+ * Forward missing method calls onto the connection manager.
+ *
+ * @param string $method
+ * @param mixed $args
+ *
+ * @return mixed
+ */
+ public function __call($method, $args)
+ {
+ $value = $this->manager->{$method}(...$args);
+
+ return in_array($method, $this->passthru) ? $this : $value;
+ }
+
+ /**
+ * Get the connection manager.
+ *
+ * @return ConnectionManager
+ */
+ public function manager()
+ {
+ return $this->manager;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ContainerException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ContainerException.php
new file mode 100644
index 0000000..0ab29cf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/ContainerException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord;
+
+class ContainerException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php
new file mode 100644
index 0000000..d61159e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace LdapRecord;
+
+class DetailedError
+{
+ /**
+ * The error code from ldap_errno.
+ *
+ * @var int|null
+ */
+ protected $errorCode;
+
+ /**
+ * The error message from ldap_error.
+ *
+ * @var string|null
+ */
+ protected $errorMessage;
+
+ /**
+ * The diagnostic message when retrieved after an ldap_error.
+ *
+ * @var string|null
+ */
+ protected $diagnosticMessage;
+
+ /**
+ * Constructor.
+ *
+ * @param int $errorCode
+ * @param string $errorMessage
+ * @param string $diagnosticMessage
+ */
+ public function __construct($errorCode, $errorMessage, $diagnosticMessage)
+ {
+ $this->errorCode = $errorCode;
+ $this->errorMessage = $errorMessage;
+ $this->diagnosticMessage = $diagnosticMessage;
+ }
+
+ /**
+ * Returns the LDAP error code.
+ *
+ * @return int
+ */
+ public function getErrorCode()
+ {
+ return $this->errorCode;
+ }
+
+ /**
+ * Returns the LDAP error message.
+ *
+ * @return string
+ */
+ public function getErrorMessage()
+ {
+ return $this->errorMessage;
+ }
+
+ /**
+ * Returns the LDAP diagnostic message.
+ *
+ * @return string
+ */
+ public function getDiagnosticMessage()
+ {
+ return $this->diagnosticMessage;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php
new file mode 100644
index 0000000..e8997a9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace LdapRecord;
+
+trait DetectsErrors
+{
+ /**
+ * Determine if the error was caused by a lost connection.
+ *
+ * @param string $error
+ *
+ * @return bool
+ */
+ protected function causedByLostConnection($error)
+ {
+ return $this->errorContainsMessage($error, ["Can't contact LDAP server", 'Operations error']);
+ }
+
+ /**
+ * Determine if the error was caused by lack of pagination support.
+ *
+ * @param string $error
+ *
+ * @return bool
+ */
+ protected function causedByPaginationSupport($error)
+ {
+ return $this->errorContainsMessage($error, 'No server controls in result');
+ }
+
+ /**
+ * Determine if the error was caused by a size limit warning.
+ *
+ * @param $error
+ *
+ * @return bool
+ */
+ protected function causedBySizeLimit($error)
+ {
+ return $this->errorContainsMessage($error, ['Partial search results returned', 'Size limit exceeded']);
+ }
+
+ /**
+ * Determine if the error was caused by a "No such object" warning.
+ *
+ * @param string $error
+ *
+ * @return bool
+ */
+ protected function causedByNoSuchObject($error)
+ {
+ return $this->errorContainsMessage($error, ['No such object']);
+ }
+
+ /**
+ * Determine if the error contains the any of the messages.
+ *
+ * @param string $error
+ * @param string|array $messages
+ *
+ * @return bool
+ */
+ protected function errorContainsMessage($error, $messages = [])
+ {
+ foreach ((array) $messages as $message) {
+ if (strpos($error, $message) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php
new file mode 100644
index 0000000..acfc020
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord;
+
+use LdapRecord\Models\Attributes\EscapedValue;
+
+trait EscapesValues
+{
+ /**
+ * Prepare a value to be escaped.
+ *
+ * @param string $value
+ * @param string $ignore
+ * @param int $flags
+ *
+ * @return EscapedValue
+ */
+ public function escape($value, $ignore = '', $flags = 0)
+ {
+ return new EscapedValue($value, $ignore, $flags);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connected.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connected.php
new file mode 100644
index 0000000..d9505da
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connected.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Events;
+
+class Connected extends ConnectionEvent
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connecting.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connecting.php
new file mode 100644
index 0000000..d2922ad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Connecting.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Events;
+
+class Connecting extends ConnectionEvent
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php
new file mode 100644
index 0000000..e9c2c35
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace LdapRecord\Events;
+
+use LdapRecord\Connection;
+
+abstract class ConnectionEvent
+{
+ /**
+ * The LDAP connection.
+ *
+ * @var Connection
+ */
+ protected $connection;
+
+ /**
+ * Constructor.
+ *
+ * @param Connection $connection
+ */
+ public function __construct(Connection $connection)
+ {
+ $this->connection = $connection;
+ }
+
+ /**
+ * Get the connection pertaining to the event.
+ *
+ * @return Connection
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionFailed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionFailed.php
new file mode 100644
index 0000000..7e110c1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionFailed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Events;
+
+class ConnectionFailed extends ConnectionEvent
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php
new file mode 100644
index 0000000..a4ae3de
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php
@@ -0,0 +1,334 @@
+<?php
+
+namespace LdapRecord\Events;
+
+use LdapRecord\Support\Arr;
+
+/**
+ * Class Dispatcher.
+ *
+ * Handles event listening and dispatching.
+ *
+ * This code was taken out of the Laravel Framework core
+ * with broadcasting and queuing omitted to remove
+ * an extra dependency that would be required.
+ *
+ * @author Taylor Otwell
+ *
+ * @see https://github.com/laravel/framework
+ */
+class Dispatcher implements DispatcherInterface
+{
+ /**
+ * The registered event listeners.
+ *
+ * @var array
+ */
+ protected $listeners = [];
+
+ /**
+ * The wildcard listeners.
+ *
+ * @var array
+ */
+ protected $wildcards = [];
+
+ /**
+ * The cached wildcard listeners.
+ *
+ * @var array
+ */
+ protected $wildcardsCache = [];
+
+ /**
+ * @inheritdoc
+ */
+ public function listen($events, $listener)
+ {
+ foreach ((array) $events as $event) {
+ if (strpos($event, '*') !== false) {
+ $this->setupWildcardListen($event, $listener);
+ } else {
+ $this->listeners[$event][] = $this->makeListener($listener);
+ }
+ }
+ }
+
+ /**
+ * Setup a wildcard listener callback.
+ *
+ * @param string $event
+ * @param mixed $listener
+ *
+ * @return void
+ */
+ protected function setupWildcardListen($event, $listener)
+ {
+ $this->wildcards[$event][] = $this->makeListener($listener, true);
+
+ $this->wildcardsCache = [];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hasListeners($eventName)
+ {
+ return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function until($event, $payload = [])
+ {
+ return $this->dispatch($event, $payload, true);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function fire($event, $payload = [], $halt = false)
+ {
+ return $this->dispatch($event, $payload, $halt);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function dispatch($event, $payload = [], $halt = false)
+ {
+ // When the given "event" is actually an object we will assume it is an event
+ // object and use the class as the event name and this event itself as the
+ // payload to the handler, which makes object based events quite simple.
+ [$event, $payload] = $this->parseEventAndPayload(
+ $event,
+ $payload
+ );
+
+ $responses = [];
+
+ foreach ($this->getListeners($event) as $listener) {
+ $response = $listener($event, $payload);
+
+ // If a response is returned from the listener and event halting is enabled
+ // we will just return this response, and not call the rest of the event
+ // listeners. Otherwise we will add the response on the response list.
+ if ($halt && ! is_null($response)) {
+ return $response;
+ }
+
+ // If a boolean false is returned from a listener, we will stop propagating
+ // the event to any further listeners down in the chain, else we keep on
+ // looping through the listeners and firing every one in our sequence.
+ if ($response === false) {
+ break;
+ }
+
+ $responses[] = $response;
+ }
+
+ return $halt ? null : $responses;
+ }
+
+ /**
+ * Parse the given event and payload and prepare them for dispatching.
+ *
+ * @param mixed $event
+ * @param mixed $payload
+ *
+ * @return array
+ */
+ protected function parseEventAndPayload($event, $payload)
+ {
+ if (is_object($event)) {
+ [$payload, $event] = [[$event], get_class($event)];
+ }
+
+ return [$event, Arr::wrap($payload)];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getListeners($eventName)
+ {
+ $listeners = $this->listeners[$eventName] ?? [];
+
+ $listeners = array_merge(
+ $listeners,
+ $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
+ );
+
+ return class_exists($eventName, false)
+ ? $this->addInterfaceListeners($eventName, $listeners)
+ : $listeners;
+ }
+
+ /**
+ * Get the wildcard listeners for the event.
+ *
+ * @param string $eventName
+ *
+ * @return array
+ */
+ protected function getWildcardListeners($eventName)
+ {
+ $wildcards = [];
+
+ foreach ($this->wildcards as $key => $listeners) {
+ if ($this->wildcardContainsEvent($key, $eventName)) {
+ $wildcards = array_merge($wildcards, $listeners);
+ }
+ }
+
+ return $this->wildcardsCache[$eventName] = $wildcards;
+ }
+
+ /**
+ * Determine if the wildcard matches or contains the given event.
+ *
+ * This function is a direct excerpt from Laravel's Str::is().
+ *
+ * @param string $wildcard
+ * @param string $eventName
+ *
+ * @return bool
+ */
+ protected function wildcardContainsEvent($wildcard, $eventName)
+ {
+ $patterns = Arr::wrap($wildcard);
+
+ if (empty($patterns)) {
+ return false;
+ }
+
+ foreach ($patterns as $pattern) {
+ // If the given event is an exact match we can of course return true right
+ // from the beginning. Otherwise, we will translate asterisks and do an
+ // actual pattern match against the two strings to see if they match.
+ if ($pattern == $eventName) {
+ return true;
+ }
+
+ $pattern = preg_quote($pattern, '#');
+
+ // Asterisks are translated into zero-or-more regular expression wildcards
+ // to make it convenient to check if the strings starts with the given
+ // pattern such as "library/*", making any string check convenient.
+ $pattern = str_replace('\*', '.*', $pattern);
+
+ if (preg_match('#^'.$pattern.'\z#u', $eventName) === 1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add the listeners for the event's interfaces to the given array.
+ *
+ * @param string $eventName
+ * @param array $listeners
+ *
+ * @return array
+ */
+ protected function addInterfaceListeners($eventName, array $listeners = [])
+ {
+ foreach (class_implements($eventName) as $interface) {
+ if (isset($this->listeners[$interface])) {
+ foreach ($this->listeners[$interface] as $names) {
+ $listeners = array_merge($listeners, (array) $names);
+ }
+ }
+ }
+
+ return $listeners;
+ }
+
+ /**
+ * Register an event listener with the dispatcher.
+ *
+ * @param \Closure|string $listener
+ * @param bool $wildcard
+ *
+ * @return \Closure
+ */
+ public function makeListener($listener, $wildcard = false)
+ {
+ if (is_string($listener)) {
+ return $this->createClassListener($listener, $wildcard);
+ }
+
+ return function ($event, $payload) use ($listener, $wildcard) {
+ if ($wildcard) {
+ return $listener($event, $payload);
+ }
+
+ return $listener(...array_values($payload));
+ };
+ }
+
+ /**
+ * Create a class based listener.
+ *
+ * @param string $listener
+ * @param bool $wildcard
+ *
+ * @return \Closure
+ */
+ protected function createClassListener($listener, $wildcard = false)
+ {
+ return function ($event, $payload) use ($listener, $wildcard) {
+ if ($wildcard) {
+ return call_user_func($this->createClassCallable($listener), $event, $payload);
+ }
+
+ return call_user_func_array(
+ $this->createClassCallable($listener),
+ $payload
+ );
+ };
+ }
+
+ /**
+ * Create the class based event callable.
+ *
+ * @param string $listener
+ *
+ * @return callable
+ */
+ protected function createClassCallable($listener)
+ {
+ [$class, $method] = $this->parseListenerCallback($listener);
+
+ return [new $class(), $method];
+ }
+
+ /**
+ * Parse the class listener into class and method.
+ *
+ * @param string $listener
+ *
+ * @return array
+ */
+ protected function parseListenerCallback($listener)
+ {
+ return strpos($listener, '@') !== false
+ ? explode('@', $listener, 2)
+ : [$listener, 'handle'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function forget($event)
+ {
+ if (strpos($event, '*') !== false) {
+ unset($this->wildcards[$event]);
+ } else {
+ unset($this->listeners[$event]);
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php
new file mode 100644
index 0000000..6b7cb10
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace LdapRecord\Events;
+
+interface DispatcherInterface
+{
+ /**
+ * Register an event listener with the dispatcher.
+ *
+ * @param string|array $events
+ * @param mixed $listener
+ *
+ * @return void
+ */
+ public function listen($events, $listener);
+
+ /**
+ * Determine if a given event has listeners.
+ *
+ * @param string $eventName
+ *
+ * @return bool
+ */
+ public function hasListeners($eventName);
+
+ /**
+ * Fire an event until the first non-null response is returned.
+ *
+ * @param string|object $event
+ * @param mixed $payload
+ *
+ * @return array|null
+ */
+ public function until($event, $payload = []);
+
+ /**
+ * Fire an event and call the listeners.
+ *
+ * @param string|object $event
+ * @param mixed $payload
+ * @param bool $halt
+ *
+ * @return mixed
+ */
+ public function fire($event, $payload = [], $halt = false);
+
+ /**
+ * Fire an event and call the listeners.
+ *
+ * @param string|object $event
+ * @param mixed $payload
+ * @param bool $halt
+ *
+ * @return array|null
+ */
+ public function dispatch($event, $payload = [], $halt = false);
+
+ /**
+ * Get all of the listeners for a given event name.
+ *
+ * @param string $eventName
+ *
+ * @return array
+ */
+ public function getListeners($eventName);
+
+ /**
+ * Remove a set of listeners from the dispatcher.
+ *
+ * @param string $event
+ *
+ * @return void
+ */
+ public function forget($event);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Logger.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Logger.php
new file mode 100644
index 0000000..f3840c2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Logger.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace LdapRecord\Events;
+
+use LdapRecord\Auth\Events\Event as AuthEvent;
+use LdapRecord\Auth\Events\Failed;
+use LdapRecord\Models\Events\Event as ModelEvent;
+use LdapRecord\Query\Events\QueryExecuted as QueryEvent;
+use Psr\Log\LoggerInterface;
+use ReflectionClass;
+
+class Logger
+{
+ /**
+ * The logger instance.
+ *
+ * @var LoggerInterface|null
+ */
+ protected $logger;
+
+ /**
+ * Constructor.
+ *
+ * @param LoggerInterface|null $logger
+ */
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger;
+ }
+
+ /**
+ * Logs the given event.
+ *
+ * @param mixed $event
+ *
+ * @return void
+ */
+ public function log($event)
+ {
+ switch (true) {
+ case $event instanceof AuthEvent:
+ return $this->auth($event);
+ case $event instanceof ModelEvent:
+ return $this->model($event);
+ case $event instanceof QueryEvent:
+ return $this->query($event);
+ }
+ }
+
+ /**
+ * Logs an authentication event.
+ *
+ * @param AuthEvent $event
+ *
+ * @return void
+ */
+ public function auth(AuthEvent $event)
+ {
+ if (isset($this->logger)) {
+ $connection = $event->getConnection();
+
+ $message = "LDAP ({$connection->getHost()})"
+ ." - Operation: {$this->getOperationName($event)}"
+ ." - Username: {$event->getUsername()}";
+
+ $result = null;
+ $type = 'info';
+
+ if (is_a($event, Failed::class)) {
+ $type = 'warning';
+ $result = " - Reason: {$connection->getLastError()}";
+ }
+
+ $this->logger->$type($message.$result);
+ }
+ }
+
+ /**
+ * Logs a model event.
+ *
+ * @param ModelEvent $event
+ *
+ * @return void
+ */
+ public function model(ModelEvent $event)
+ {
+ if (isset($this->logger)) {
+ $model = $event->getModel();
+
+ $on = get_class($model);
+
+ $connection = $model->getConnection()->getLdapConnection();
+
+ $message = "LDAP ({$connection->getHost()})"
+ ." - Operation: {$this->getOperationName($event)}"
+ ." - On: {$on}"
+ ." - Distinguished Name: {$model->getDn()}";
+
+ $this->logger->info($message);
+ }
+ }
+
+ /**
+ * Logs a query event.
+ *
+ * @param QueryEvent $event
+ *
+ * @return void
+ */
+ public function query(QueryEvent $event)
+ {
+ if (isset($this->logger)) {
+ $query = $event->getQuery();
+
+ $connection = $query->getConnection()->getLdapConnection();
+
+ $selected = implode(',', $query->getSelects());
+
+ $message = "LDAP ({$connection->getHost()})"
+ ." - Operation: {$this->getOperationName($event)}"
+ ." - Base DN: {$query->getBaseDn()}"
+ ." - Filter: {$query->getQuery()}"
+ ." - Selected: ({$selected})"
+ ." - Time Elapsed: {$event->getTime()}";
+
+ $this->logger->info($message);
+ }
+ }
+
+ /**
+ * Returns the operational name of the given event.
+ *
+ * @param mixed $event
+ *
+ * @return string
+ */
+ protected function getOperationName($event)
+ {
+ return (new ReflectionClass($event))->getShortName();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/AlreadyExistsException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/AlreadyExistsException.php
new file mode 100644
index 0000000..2298caf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/AlreadyExistsException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Exceptions;
+
+use LdapRecord\LdapRecordException;
+
+class AlreadyExistsException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/ConstraintViolationException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/ConstraintViolationException.php
new file mode 100644
index 0000000..641843a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/ConstraintViolationException.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace LdapRecord\Exceptions;
+
+use LdapRecord\DetectsErrors;
+use LdapRecord\LdapRecordException;
+
+class ConstraintViolationException extends LdapRecordException
+{
+ use DetectsErrors;
+
+ /**
+ * Determine if the exception was generated due to the password policy.
+ *
+ * @return bool
+ */
+ public function causedByPasswordPolicy()
+ {
+ return isset($this->detailedError)
+ ? $this->errorContainsMessage($this->detailedError->getDiagnosticMessage(), '0000052D')
+ : false;
+ }
+
+ /**
+ * Determine if the exception was generated due to an incorrect password.
+ *
+ * @return bool
+ */
+ public function causedByIncorrectPassword()
+ {
+ return isset($this->detailedError)
+ ? $this->errorContainsMessage($this->detailedError->getDiagnosticMessage(), '00000056')
+ : false;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/InsufficientAccessException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/InsufficientAccessException.php
new file mode 100644
index 0000000..89c55fd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Exceptions/InsufficientAccessException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Exceptions;
+
+use LdapRecord\LdapRecordException;
+
+class InsufficientAccessException extends LdapRecordException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/HandlesConnection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/HandlesConnection.php
new file mode 100644
index 0000000..41334b6
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/HandlesConnection.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace LdapRecord;
+
+use Closure;
+use ErrorException;
+use Exception;
+
+trait HandlesConnection
+{
+ /**
+ * The LDAP host that is currently connected.
+ *
+ * @var string|null
+ */
+ protected $host;
+
+ /**
+ * The LDAP connection resource.
+ *
+ * @var resource|null
+ */
+ protected $connection;
+
+ /**
+ * The bound status of the connection.
+ *
+ * @var bool
+ */
+ protected $bound = false;
+
+ /**
+ * Whether the connection must be bound over SSL.
+ *
+ * @var bool
+ */
+ protected $useSSL = false;
+
+ /**
+ * Whether the connection must be bound over TLS.
+ *
+ * @var bool
+ */
+ protected $useTLS = false;
+
+ /**
+ * @inheritdoc
+ */
+ public function isUsingSSL()
+ {
+ return $this->useSSL;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isUsingTLS()
+ {
+ return $this->useTLS;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isBound()
+ {
+ return $this->bound;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isConnected()
+ {
+ return ! is_null($this->connection);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canChangePasswords()
+ {
+ return $this->isUsingSSL() || $this->isUsingTLS();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function ssl($enabled = true)
+ {
+ $this->useSSL = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function tls($enabled = true)
+ {
+ $this->useTLS = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setOptions(array $options = [])
+ {
+ foreach ($options as $option => $value) {
+ $this->setOption($option, $value);
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getProtocol()
+ {
+ return $this->isUsingSSL() ? LdapInterface::PROTOCOL_SSL : LdapInterface::PROTOCOL;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getExtendedError()
+ {
+ return $this->getDiagnosticMessage();
+ }
+
+ /**
+ * Convert warnings to exceptions for the given operation.
+ *
+ * @param Closure $operation
+ *
+ * @throws LdapRecordException
+ *
+ * @return mixed
+ */
+ protected function executeFailableOperation(Closure $operation)
+ {
+ // If some older versions of PHP, errors are reported instead of throwing
+ // exceptions, which could be a signifcant detriment to our application.
+ // Here, we will enforce these operations to throw exceptions instead.
+ set_error_handler(function ($severity, $message, $file, $line) {
+ if (! $this->shouldBypassError($message)) {
+ throw new ErrorException($message, $severity, $severity, $file, $line);
+ }
+ });
+
+ try {
+ if (($result = $operation()) !== false) {
+ return $result;
+ }
+
+ // If the failed query operation was a based on a query being executed
+ // -- such as a search, read, or listing, then we can safely return
+ // the failed response here and prevent throwning an exception.
+ if ($this->shouldBypassFailure($method = debug_backtrace()[1]['function'])) {
+ return $result;
+ }
+
+ throw new Exception("LDAP operation [$method] failed.");
+ } catch (ErrorException $e) {
+ throw LdapRecordException::withDetailedError($e, $this->getDetailedError());
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * Determine if the failed operation should be bypassed.
+ *
+ * @param string $method
+ *
+ * @return bool
+ */
+ protected function shouldBypassFailure($method)
+ {
+ return in_array($method, ['search', 'read', 'listing']);
+ }
+
+ /**
+ * Determine if the error should be bypassed.
+ *
+ * @param string $error
+ *
+ * @return bool
+ */
+ protected function shouldBypassError($error)
+ {
+ return $this->causedByPaginationSupport($error) || $this->causedBySizeLimit($error) || $this->causedByNoSuchObject($error);
+ }
+
+ /**
+ * Determine if the current PHP version supports server controls.
+ *
+ * @deprecated since v2.5.0
+ *
+ * @return bool
+ */
+ public function supportsServerControlsInMethods()
+ {
+ return version_compare(PHP_VERSION, '7.3.0') >= 0;
+ }
+
+ /**
+ * Generates an LDAP connection string for each host given.
+ *
+ * @param string|array $hosts
+ * @param string $port
+ *
+ * @return string
+ */
+ protected function makeConnectionUris($hosts, $port)
+ {
+ // If an attempt to connect via SSL protocol is being performed,
+ // and we are still using the default port, we will swap it
+ // for the default SSL port, for developer convenience.
+ if ($this->isUsingSSL() && $port == LdapInterface::PORT) {
+ $port = LdapInterface::PORT_SSL;
+ }
+
+ // The blank space here is intentional. PHP's LDAP extension
+ // requires additional hosts to be seperated by a blank
+ // space, so that it can parse each individually.
+ return implode(' ', $this->assembleHostUris($hosts, $port));
+ }
+
+ /**
+ * Assemble the host URI strings.
+ *
+ * @param array|string $hosts
+ * @param string $port
+ *
+ * @return array
+ */
+ protected function assembleHostUris($hosts, $port)
+ {
+ return array_map(function ($host) use ($port) {
+ return "{$this->getProtocol()}{$host}:{$port}";
+ }, (array) $hosts);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Ldap.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Ldap.php
new file mode 100644
index 0000000..6503cea
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Ldap.php
@@ -0,0 +1,480 @@
+<?php
+
+namespace LdapRecord;
+
+class Ldap implements LdapInterface
+{
+ use HandlesConnection, DetectsErrors;
+
+ /**
+ * @inheritdoc
+ */
+ public function getEntries($searchResults)
+ {
+ return $this->executeFailableOperation(function () use ($searchResults) {
+ return ldap_get_entries($this->connection, $searchResults);
+ });
+ }
+
+ /**
+ * Retrieves the first entry from a search result.
+ *
+ * @see http://php.net/manual/en/function.ldap-first-entry.php
+ *
+ * @param resource $searchResults
+ *
+ * @return resource
+ */
+ public function getFirstEntry($searchResults)
+ {
+ return $this->executeFailableOperation(function () use ($searchResults) {
+ return ldap_first_entry($this->connection, $searchResults);
+ });
+ }
+
+ /**
+ * Retrieves the next entry from a search result.
+ *
+ * @see http://php.net/manual/en/function.ldap-next-entry.php
+ *
+ * @param resource $entry
+ *
+ * @return resource
+ */
+ public function getNextEntry($entry)
+ {
+ return $this->executeFailableOperation(function () use ($entry) {
+ return ldap_next_entry($this->connection, $entry);
+ });
+ }
+
+ /**
+ * Retrieves the ldap entry's attributes.
+ *
+ * @see http://php.net/manual/en/function.ldap-get-attributes.php
+ *
+ * @param resource $entry
+ *
+ * @return array|false
+ */
+ public function getAttributes($entry)
+ {
+ return $this->executeFailableOperation(function () use ($entry) {
+ return ldap_get_attributes($this->connection, $entry);
+ });
+ }
+
+ /**
+ * Returns the number of entries from a search result.
+ *
+ * @see http://php.net/manual/en/function.ldap-count-entries.php
+ *
+ * @param resource $searchResults
+ *
+ * @return int
+ */
+ public function countEntries($searchResults)
+ {
+ return $this->executeFailableOperation(function () use ($searchResults) {
+ return ldap_count_entries($this->connection, $searchResults);
+ });
+ }
+
+ /**
+ * Compare value of attribute found in entry specified with DN.
+ *
+ * @see http://php.net/manual/en/function.ldap-compare.php
+ *
+ * @param string $dn
+ * @param string $attribute
+ * @param string $value
+ *
+ * @return mixed
+ */
+ public function compare($dn, $attribute, $value)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $attribute, $value) {
+ return ldap_compare($this->connection, $dn, $attribute, $value);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getLastError()
+ {
+ if (! $this->connection) {
+ return;
+ }
+
+ return ldap_error($this->connection);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDetailedError()
+ {
+ if (! $number = $this->errNo()) {
+ return;
+ }
+
+ $this->getOption(LDAP_OPT_DIAGNOSTIC_MESSAGE, $message);
+
+ return new DetailedError($number, $this->err2Str($number), $message);
+ }
+
+ /**
+ * Get all binary values from the specified result entry.
+ *
+ * @see http://php.net/manual/en/function.ldap-get-values-len.php
+ *
+ * @param $entry
+ * @param $attribute
+ *
+ * @return array
+ */
+ public function getValuesLen($entry, $attribute)
+ {
+ return $this->executeFailableOperation(function () use ($entry, $attribute) {
+ return ldap_get_values_len($this->connection, $entry, $attribute);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setOption($option, $value)
+ {
+ return ldap_set_option($this->connection, $option, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getOption($option, &$value = null)
+ {
+ ldap_get_option($this->connection, $option, $value);
+
+ return $value;
+ }
+
+ /**
+ * Set a callback function to do re-binds on referral chasing.
+ *
+ * @see http://php.net/manual/en/function.ldap-set-rebind-proc.php
+ *
+ * @param callable $callback
+ *
+ * @return bool
+ */
+ public function setRebindCallback(callable $callback)
+ {
+ return ldap_set_rebind_proc($this->connection, $callback);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function startTLS()
+ {
+ return $this->executeFailableOperation(function () {
+ return ldap_start_tls($this->connection);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function connect($hosts = [], $port = 389)
+ {
+ $this->bound = false;
+
+ $this->host = $this->makeConnectionUris($hosts, $port);
+
+ return $this->connection = $this->executeFailableOperation(function () {
+ return ldap_connect($this->host);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function close()
+ {
+ $result = is_resource($this->connection) ? @ldap_close($this->connection) : false;
+
+ $this->connection = null;
+ $this->bound = false;
+ $this->host = null;
+
+ return $result;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+ {
+ return $this->executeFailableOperation(function () use (
+ $dn,
+ $filter,
+ $fields,
+ $onlyAttributes,
+ $size,
+ $time,
+ $deref,
+ $serverControls
+ ) {
+ return empty($serverControls)
+ ? ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
+ : ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+ {
+ return $this->executeFailableOperation(function () use (
+ $dn,
+ $filter,
+ $fields,
+ $onlyAttributes,
+ $size,
+ $time,
+ $deref,
+ $serverControls
+ ) {
+ return empty($serverControls)
+ ? ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
+ : ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+ {
+ return $this->executeFailableOperation(function () use (
+ $dn,
+ $filter,
+ $fields,
+ $onlyAttributes,
+ $size,
+ $time,
+ $deref,
+ $serverControls
+ ) {
+ return empty($serverControls)
+ ? ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref)
+ : ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time, $deref, $serverControls);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
+ {
+ return $this->executeFailableOperation(function () use (
+ $result,
+ &$errorCode,
+ &$dn,
+ &$errorMessage,
+ &$referrals,
+ &$serverControls
+ ) {
+ return empty($serverControls)
+ ? ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals)
+ : ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals, $serverControls);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function bind($username, $password)
+ {
+ return $this->bound = $this->executeFailableOperation(function () use ($username, $password) {
+ return ldap_bind($this->connection, $username, html_entity_decode($password));
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function add($dn, array $entry)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $entry) {
+ return ldap_add($this->connection, $dn, $entry);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function delete($dn)
+ {
+ return $this->executeFailableOperation(function () use ($dn) {
+ return ldap_delete($this->connection, $dn);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
+ {
+ return $this->executeFailableOperation(function () use (
+ $dn,
+ $newRdn,
+ $newParent,
+ $deleteOldRdn
+ ) {
+ return ldap_rename($this->connection, $dn, $newRdn, $newParent, $deleteOldRdn);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modify($dn, array $entry)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $entry) {
+ return ldap_modify($this->connection, $dn, $entry);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modifyBatch($dn, array $values)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $values) {
+ return ldap_modify_batch($this->connection, $dn, $values);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modAdd($dn, array $entry)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $entry) {
+ return ldap_mod_add($this->connection, $dn, $entry);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modReplace($dn, array $entry)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $entry) {
+ return ldap_mod_replace($this->connection, $dn, $entry);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modDelete($dn, array $entry)
+ {
+ return $this->executeFailableOperation(function () use ($dn, $entry) {
+ return ldap_mod_del($this->connection, $dn, $entry);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
+ {
+ return $this->executeFailableOperation(function () use ($pageSize, $isCritical, $cookie) {
+ return ldap_control_paged_result($this->connection, $pageSize, $isCritical, $cookie);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function controlPagedResultResponse($result, &$cookie, &$estimated = null)
+ {
+ return $this->executeFailableOperation(function () use ($result, &$cookie, &$estimated) {
+ return ldap_control_paged_result_response($this->connection, $result, $cookie, $estimated);
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function freeResult($result)
+ {
+ return ldap_free_result($result);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function errNo()
+ {
+ return $this->connection ? ldap_errno($this->connection) : null;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function err2Str($number)
+ {
+ return ldap_err2str($number);
+ }
+
+ /**
+ * Returns the extended error hex code of the last command.
+ *
+ * @return string|null
+ */
+ public function getExtendedErrorHex()
+ {
+ if (preg_match("/(?<=data\s).*?(?=,)/", $this->getExtendedError(), $code)) {
+ return $code[0];
+ }
+ }
+
+ /**
+ * Returns the extended error code of the last command.
+ *
+ * @return bool|string
+ */
+ public function getExtendedErrorCode()
+ {
+ return $this->extractDiagnosticCode($this->getExtendedError());
+ }
+
+ /**
+ * Extract the diagnostic code from the message.
+ *
+ * @param string $message
+ *
+ * @return string|bool
+ */
+ public function extractDiagnosticCode($message)
+ {
+ preg_match('/^([\da-fA-F]+):/', $message, $matches);
+
+ return isset($matches[1]) ? $matches[1] : false;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDiagnosticMessage()
+ {
+ $this->getOption(LDAP_OPT_ERROR_STRING, $message);
+
+ return $message;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapInterface.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapInterface.php
new file mode 100644
index 0000000..a1773ad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapInterface.php
@@ -0,0 +1,517 @@
+<?php
+
+namespace LdapRecord;
+
+interface LdapInterface
+{
+ /**
+ * The SSL LDAP protocol string.
+ *
+ * @var string
+ */
+ const PROTOCOL_SSL = 'ldaps://';
+
+ /**
+ * The standard LDAP protocol string.
+ *
+ * @var string
+ */
+ const PROTOCOL = 'ldap://';
+
+ /**
+ * The LDAP SSL port number.
+ *
+ * @var string
+ */
+ const PORT_SSL = 636;
+
+ /**
+ * The standard LDAP port number.
+ *
+ * @var string
+ */
+ const PORT = 389;
+
+ /**
+ * Various useful server control OID's.
+ *
+ * @see https://ldap.com/ldap-oid-reference-guide/
+ * @see http://msdn.microsoft.com/en-us/library/cc223359.aspx
+ */
+ const OID_SERVER_START_TLS = '1.3.6.1.4.1.1466.20037';
+ const OID_SERVER_PAGED_RESULTS = '1.2.840.113556.1.4.319';
+ const OID_SERVER_SHOW_DELETED = '1.2.840.113556.1.4.417';
+ const OID_SERVER_SORT = '1.2.840.113556.1.4.473';
+ const OID_SERVER_CROSSDOM_MOVE_TARGET = '1.2.840.113556.1.4.521';
+ const OID_SERVER_NOTIFICATION = '1.2.840.113556.1.4.528';
+ const OID_SERVER_EXTENDED_DN = '1.2.840.113556.1.4.529';
+ const OID_SERVER_LAZY_COMMIT = '1.2.840.113556.1.4.619';
+ const OID_SERVER_SD_FLAGS = '1.2.840.113556.1.4.801';
+ const OID_SERVER_TREE_DELETE = '1.2.840.113556.1.4.805';
+ const OID_SERVER_DIRSYNC = '1.2.840.113556.1.4.841';
+ const OID_SERVER_VERIFY_NAME = '1.2.840.113556.1.4.1338';
+ const OID_SERVER_DOMAIN_SCOPE = '1.2.840.113556.1.4.1339';
+ const OID_SERVER_SEARCH_OPTIONS = '1.2.840.113556.1.4.1340';
+ const OID_SERVER_PERMISSIVE_MODIFY = '1.2.840.113556.1.4.1413';
+ const OID_SERVER_ASQ = '1.2.840.113556.1.4.1504';
+ const OID_SERVER_FAST_BIND = '1.2.840.113556.1.4.1781';
+ const OID_SERVER_CONTROL_VLVREQUEST = '2.16.840.1.113730.3.4.9';
+
+ /**
+ * Query OID's.
+ *
+ * @see https://ldapwiki.com/wiki/LDAP_MATCHING_RULE_IN_CHAIN
+ */
+ const OID_MATCHING_RULE_IN_CHAIN = '1.2.840.113556.1.4.1941';
+
+ /**
+ * Set the current connection to use SSL.
+ *
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function ssl();
+
+ /**
+ * Determine if the current connection instance is using SSL.
+ *
+ * @return bool
+ */
+ public function isUsingSSL();
+
+ /**
+ * Set the current connection to use TLS.
+ *
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function tls();
+
+ /**
+ * Determine if the current connection instance is using TLS.
+ *
+ * @return bool
+ */
+ public function isUsingTLS();
+
+ /**
+ * Determine if the connection is bound.
+ *
+ * @return bool
+ */
+ public function isBound();
+
+ /**
+ * Determine if the connection has been created.
+ *
+ * @return bool
+ */
+ public function isConnected();
+
+ /**
+ * Determine the connection is able to modify passwords.
+ *
+ * @return bool
+ */
+ public function canChangePasswords();
+
+ /**
+ * Returns the full LDAP host URL.
+ *
+ * Ex: ldap://192.168.1.1:386
+ *
+ * @return string|null
+ */
+ public function getHost();
+
+ /**
+ * Get the underlying connection resource.
+ *
+ * @return resource|null
+ */
+ public function getConnection();
+
+ /**
+ * Retrieve the entries from a search result.
+ *
+ * @see http://php.net/manual/en/function.ldap-get-entries.php
+ *
+ * @param resource $searchResults
+ *
+ * @return array
+ */
+ public function getEntries($searchResults);
+
+ /**
+ * Retrieve the last error on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-error.php
+ *
+ * @return string|null
+ */
+ public function getLastError();
+
+ /**
+ * Return detailed information about an error.
+ *
+ * Returns false when there was a successful last request.
+ *
+ * Returns DetailedError when there was an error.
+ *
+ * @return DetailedError|null
+ */
+ public function getDetailedError();
+
+ /**
+ * Set an option on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-set-option.php
+ *
+ * @param int $option
+ * @param mixed $value
+ *
+ * @return bool
+ */
+ public function setOption($option, $value);
+
+ /**
+ * Set options on the current connection.
+ *
+ * @param array $options
+ *
+ * @return void
+ */
+ public function setOptions(array $options = []);
+
+ /**
+ * Get the value for the LDAP option.
+ *
+ * @see https://www.php.net/manual/en/function.ldap-get-option.php
+ *
+ * @param int $option
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ public function getOption($option, &$value = null);
+
+ /**
+ * Starts a connection using TLS.
+ *
+ * @see http://php.net/manual/en/function.ldap-start-tls.php
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function startTLS();
+
+ /**
+ * Connects to the specified hostname using the specified port.
+ *
+ * @see http://php.net/manual/en/function.ldap-start-tls.php
+ *
+ * @param string|array $hosts
+ * @param int $port
+ *
+ * @return resource|false
+ */
+ public function connect($hosts = [], $port = 389);
+
+ /**
+ * Closes the current connection.
+ *
+ * Returns false if no connection is present.
+ *
+ * @see http://php.net/manual/en/function.ldap-close.php
+ *
+ * @return bool
+ */
+ public function close();
+
+ /**
+ * Performs a search on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-search.php
+ *
+ * @param string $dn
+ * @param string $filter
+ * @param array $fields
+ * @param bool $onlyAttributes
+ * @param int $size
+ * @param int $time
+ * @param int $deref
+ * @param array $serverControls
+ *
+ * @return resource
+ */
+ public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = []);
+
+ /**
+ * Performs a single level search on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-list.php
+ *
+ * @param string $dn
+ * @param string $filter
+ * @param array $fields
+ * @param bool $onlyAttributes
+ * @param int $size
+ * @param int $time
+ * @param int $deref
+ * @param array $serverControls
+ *
+ * @return resource
+ */
+ public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = []);
+
+ /**
+ * Reads an entry on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-read.php
+ *
+ * @param string $dn
+ * @param string $filter
+ * @param array $fields
+ * @param bool $onlyAttributes
+ * @param int $size
+ * @param int $time
+ * @param int $deref
+ * @param array $serverControls
+ *
+ * @return resource
+ */
+ public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = []);
+
+ /**
+ * Extract information from an LDAP result.
+ *
+ * @see https://www.php.net/manual/en/function.ldap-parse-result.php
+ *
+ * @param resource $result
+ * @param int $errorCode
+ * @param string $dn
+ * @param string $errorMessage
+ * @param array $referrals
+ * @param array $serverControls
+ *
+ * @return bool
+ */
+ public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = []);
+
+ /**
+ * Binds to the current connection using the specified username and password.
+ * If sasl is true, the current connection is bound using SASL.
+ *
+ * @see http://php.net/manual/en/function.ldap-bind.php
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function bind($username, $password);
+
+ /**
+ * Adds an entry to the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-add.php
+ *
+ * @param string $dn
+ * @param array $entry
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function add($dn, array $entry);
+
+ /**
+ * Deletes an entry on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-delete.php
+ *
+ * @param string $dn
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function delete($dn);
+
+ /**
+ * Modify the name of an entry on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-rename.php
+ *
+ * @param string $dn
+ * @param string $newRdn
+ * @param string $newParent
+ * @param bool $deleteOldRdn
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false);
+
+ /**
+ * Modifies an existing entry on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-modify.php
+ *
+ * @param string $dn
+ * @param array $entry
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function modify($dn, array $entry);
+
+ /**
+ * Batch modifies an existing entry on the current connection.
+ *
+ * @see http://php.net/manual/en/function.ldap-modify-batch.php
+ *
+ * @param string $dn
+ * @param array $values
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function modifyBatch($dn, array $values);
+
+ /**
+ * Add attribute values to current attributes.
+ *
+ * @see http://php.net/manual/en/function.ldap-mod-add.php
+ *
+ * @param string $dn
+ * @param array $entry
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function modAdd($dn, array $entry);
+
+ /**
+ * Replaces attribute values with new ones.
+ *
+ * @see http://php.net/manual/en/function.ldap-mod-replace.php
+ *
+ * @param string $dn
+ * @param array $entry
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function modReplace($dn, array $entry);
+
+ /**
+ * Delete attribute values from current attributes.
+ *
+ * @see http://php.net/manual/en/function.ldap-mod-del.php
+ *
+ * @param string $dn
+ * @param array $entry
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function modDelete($dn, array $entry);
+
+ /**
+ * Send LDAP pagination control.
+ *
+ * @see http://php.net/manual/en/function.ldap-control-paged-result.php
+ *
+ * @param int $pageSize
+ * @param bool $isCritical
+ * @param string $cookie
+ *
+ * @return bool
+ */
+ public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '');
+
+ /**
+ * Retrieve the LDAP pagination cookie.
+ *
+ * @see http://php.net/manual/en/function.ldap-control-paged-result-response.php
+ *
+ * @param resource $result
+ * @param string $cookie
+ *
+ * @return bool
+ */
+ public function controlPagedResultResponse($result, &$cookie);
+
+ /**
+ * Frees up the memory allocated internally to store the result.
+ *
+ * @see https://www.php.net/manual/en/function.ldap-free-result.php
+ *
+ * @param resource $result
+ *
+ * @return bool
+ */
+ public function freeResult($result);
+
+ /**
+ * Returns the error number of the last command executed.
+ *
+ * @see http://php.net/manual/en/function.ldap-errno.php
+ *
+ * @return int|null
+ */
+ public function errNo();
+
+ /**
+ * Returns the error string of the specified error number.
+ *
+ * @see http://php.net/manual/en/function.ldap-err2str.php
+ *
+ * @param int $number
+ *
+ * @return string
+ */
+ public function err2Str($number);
+
+ /**
+ * Returns the LDAP protocol to utilize for the current connection.
+ *
+ * @return string
+ */
+ public function getProtocol();
+
+ /**
+ * Returns the extended error code of the last command.
+ *
+ * @return string
+ */
+ public function getExtendedError();
+
+ /**
+ * Return the diagnostic Message.
+ *
+ * @return string
+ */
+ public function getDiagnosticMessage();
+
+ /**
+ * Determine if the current PHP version supports server controls.
+ *
+ * @deprecated since v2.5.0
+ *
+ * @return bool
+ */
+ public function supportsServerControlsInMethods();
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapRecordException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapRecordException.php
new file mode 100644
index 0000000..b2439bf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/LdapRecordException.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace LdapRecord;
+
+use Exception;
+
+class LdapRecordException extends Exception
+{
+ /**
+ * The detailed LDAP error (if available).
+ *
+ * @var DetailedError|null
+ */
+ protected $detailedError;
+
+ /**
+ * Create a new Bind Exception with a detailed connection error.
+ *
+ * @param Exception $e
+ * @param DetailedError|null $error
+ *
+ * @return $this
+ */
+ public static function withDetailedError(Exception $e, DetailedError $error = null)
+ {
+ return (new static($e->getMessage(), $e->getCode(), $e))->setDetailedError($error);
+ }
+
+ /**
+ * Set the detailed error.
+ *
+ * @param DetailedError|null $error
+ *
+ * @return $this
+ */
+ public function setDetailedError(DetailedError $error = null)
+ {
+ $this->detailedError = $error;
+
+ return $this;
+ }
+
+ /**
+ * Returns the detailed error.
+ *
+ * @return DetailedError|null
+ */
+ public function getDetailedError()
+ {
+ return $this->detailedError;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Computer.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Computer.php
new file mode 100644
index 0000000..72db0a0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Computer.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+use LdapRecord\Models\ActiveDirectory\Concerns\HasPrimaryGroup;
+
+class Computer extends Entry
+{
+ use HasPrimaryGroup;
+
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'person',
+ 'organizationalperson',
+ 'user',
+ 'computer',
+ ];
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the current computer is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(Group::class, 'member')->with($this->primaryGroup());
+ }
+
+ /**
+ * The primary group relationship.
+ *
+ * @return Relations\HasOnePrimaryGroup
+ */
+ public function primaryGroup()
+ {
+ return $this->hasOnePrimaryGroup(Group::class, 'primarygroupid');
+ }
+
+ /**
+ * The managed by relationship.
+ *
+ * @return \LdapRecord\Models\Relations\HasOne
+ */
+ public function managedBy()
+ {
+ return $this->hasOne([Contact::class, Group::class, User::class], 'managedby');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Concerns/HasPrimaryGroup.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Concerns/HasPrimaryGroup.php
new file mode 100644
index 0000000..97fd3a1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Concerns/HasPrimaryGroup.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Concerns;
+
+use LdapRecord\Models\ActiveDirectory\Relations\HasOnePrimaryGroup;
+
+trait HasPrimaryGroup
+{
+ /**
+ * Returns a new has one primary group relationship.
+ *
+ * @param mixed $related
+ * @param string $relationKey
+ * @param string $foreignKey
+ *
+ * @return HasOnePrimaryGroup
+ */
+ public function hasOnePrimaryGroup($related, $relationKey, $foreignKey = 'primarygroupid')
+ {
+ return new HasOnePrimaryGroup($this->newQuery(), $this, $related, $relationKey, $foreignKey);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Contact.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Contact.php
new file mode 100644
index 0000000..52c451f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Contact.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Contact extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'person',
+ 'organizationalperson',
+ 'contact',
+ ];
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the current contact is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(Group::class, 'member');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Container.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Container.php
new file mode 100644
index 0000000..1636cf3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Container.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Container extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'container',
+ ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Entry.php
new file mode 100644
index 0000000..79a9d63
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Entry.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+use InvalidArgumentException;
+use LdapRecord\Connection;
+use LdapRecord\Models\Attributes\Sid;
+use LdapRecord\Models\Entry as BaseEntry;
+use LdapRecord\Models\Events\Updated;
+use LdapRecord\Models\Types\ActiveDirectory;
+use LdapRecord\Query\Model\ActiveDirectoryBuilder;
+
+/** @mixin ActiveDirectoryBuilder */
+class Entry extends BaseEntry implements ActiveDirectory
+{
+ /**
+ * The default attributes that should be mutated to dates.
+ *
+ * @var array
+ */
+ protected $defaultDates = [
+ 'whenchanged' => 'windows',
+ 'whencreated' => 'windows',
+ 'dscorepropagationdata' => 'windows',
+ ];
+
+ /**
+ * The attribute key that contains the Object SID.
+ *
+ * @var string
+ */
+ protected $sidKey = 'objectsid';
+
+ /**
+ * @inheritdoc
+ */
+ public function getObjectSidKey()
+ {
+ return $this->sidKey;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getObjectSid()
+ {
+ return $this->getFirstAttribute($this->sidKey);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getConvertedSid()
+ {
+ try {
+ return (string) new Sid($this->getObjectSid());
+ } catch (InvalidArgumentException $e) {
+ return;
+ }
+ }
+
+ /**
+ * Create a new query builder.
+ *
+ * @param Connection $connection
+ *
+ * @return ActiveDirectoryBuilder
+ */
+ public function newQueryBuilder(Connection $connection)
+ {
+ return new ActiveDirectoryBuilder($connection);
+ }
+
+ /**
+ * Determine if the object is deleted.
+ *
+ * @return bool
+ */
+ public function isDeleted()
+ {
+ return strtoupper($this->getFirstAttribute('isDeleted')) === 'TRUE';
+ }
+
+ /**
+ * Restore a deleted object.
+ *
+ * @param string|null $newParentDn
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return bool
+ */
+ public function restore($newParentDn = null)
+ {
+ if (! $this->isDeleted()) {
+ return false;
+ }
+
+ $root = $newParentDn ?? $this->getDefaultRestoreLocation();
+ $rdn = explode('\0A', $this->getDn(), 2)[0];
+ $newDn = implode(',', [$rdn, $root]);
+
+ // We will initialize a model listener for the "updated" event to set
+ // the models distinguished name so all attributes are synchronized
+ // properly after the model has been successfully restored.
+ $this->listenForModelEvent(Updated::class, function (Updated $event) use ($newDn) {
+ if ($this->is($event->getModel())) {
+ $this->setDn($newDn);
+ }
+ });
+
+ $this->save([
+ 'isDeleted' => null,
+ 'distinguishedName' => $newDn,
+ ]);
+ }
+
+ /**
+ * Get the RootDSE (AD schema) record from the directory.
+ *
+ * @param string|null $connection
+ *
+ * @throws \LdapRecord\Models\ModelNotFoundException
+ *
+ * @return static
+ */
+ public static function getRootDse($connection = null)
+ {
+ return static::on($connection ?? (new static())->getConnectionName())
+ ->in(null)
+ ->read()
+ ->whereHas('objectclass')
+ ->firstOrFail();
+ }
+
+ /**
+ * Get the objects restore location.
+ *
+ * @return string
+ */
+ protected function getDefaultRestoreLocation()
+ {
+ return $this->getFirstAttribute('lastKnownParent') ?? $this->getParentDn($this->getParentDn($this->getDn()));
+ }
+
+ /**
+ * Converts attributes for JSON serialization.
+ *
+ * @param array $attributes
+ *
+ * @return array
+ */
+ protected function convertAttributesForJson(array $attributes = [])
+ {
+ $attributes = parent::convertAttributesForJson($attributes);
+
+ if ($this->hasAttribute($this->sidKey)) {
+ // If the model has a SID set, we need to convert it due to it being in
+ // binary. Otherwise we will receive a JSON serialization exception.
+ return array_replace($attributes, [
+ $this->sidKey => [$this->getConvertedSid()],
+ ]);
+ }
+
+ return $attributes;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeDatabase.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeDatabase.php
new file mode 100644
index 0000000..77abbbc
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeDatabase.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class ExchangeDatabase extends Entry
+{
+ /**
+ * @inheritdoc
+ */
+ public static $objectClasses = ['msExchMDB'];
+
+ /**
+ * @inheritdoc
+ */
+ public static function boot()
+ {
+ parent::boot();
+
+ static::addGlobalScope(new Scopes\InConfigurationContext());
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeServer.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeServer.php
new file mode 100644
index 0000000..d304876
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ExchangeServer.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class ExchangeServer extends Entry
+{
+ /**
+ * @inheritdoc
+ */
+ public static $objectClasses = ['msExchExchangeServer'];
+
+ /**
+ * @inheritdoc
+ */
+ public static function boot()
+ {
+ parent::boot();
+
+ static::addGlobalScope(new Scopes\HasServerRoleAttribute());
+ static::addGlobalScope(new Scopes\InConfigurationContext());
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ForeignSecurityPrincipal.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ForeignSecurityPrincipal.php
new file mode 100644
index 0000000..25287ae
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/ForeignSecurityPrincipal.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class ForeignSecurityPrincipal extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = ['foreignsecurityprincipal'];
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the current security principal is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(Group::class, 'member');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Group.php
new file mode 100644
index 0000000..6076f2f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Group.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Group extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'group',
+ ];
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the current group is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(static::class, 'member');
+ }
+
+ /**
+ * The members relationship.
+ *
+ * Retrieves members that are apart of the group.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function members()
+ {
+ return $this->hasMany([
+ static::class, User::class, Contact::class, Computer::class,
+ ], 'memberof')
+ ->using($this, 'member')
+ ->with($this->primaryGroupMembers());
+ }
+
+ /**
+ * The primary group members relationship.
+ *
+ * Retrieves members that are apart the primary group.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function primaryGroupMembers()
+ {
+ return $this->hasMany([
+ static::class, User::class, Contact::class, Computer::class,
+ ], 'primarygroupid', 'rid');
+ }
+
+ /**
+ * Get the RID of the group.
+ *
+ * @return array
+ */
+ public function getRidAttribute()
+ {
+ $objectSidComponents = explode('-', $this->getConvertedSid());
+
+ return [end($objectSidComponents)];
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/OrganizationalUnit.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/OrganizationalUnit.php
new file mode 100644
index 0000000..80aae9f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/OrganizationalUnit.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class OrganizationalUnit extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'organizationalunit',
+ ];
+
+ /**
+ * Get the creatable RDN attribute name.
+ *
+ * @return string
+ */
+ public function getCreatableRdnAttribute()
+ {
+ return 'ou';
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Printer.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Printer.php
new file mode 100644
index 0000000..df74216
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Printer.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+class Printer extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = ['printqueue'];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Relations/HasOnePrimaryGroup.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Relations/HasOnePrimaryGroup.php
new file mode 100644
index 0000000..540ec77
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Relations/HasOnePrimaryGroup.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Relations;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Relations\HasOne;
+
+class HasOnePrimaryGroup extends HasOne
+{
+ /**
+ * Get the foreign model by the given value.
+ *
+ * @param string $value
+ *
+ * @return Model|null
+ */
+ protected function getForeignModelByValue($value)
+ {
+ return $this->query->findBySid(
+ $this->getParentModelObjectSid()
+ );
+ }
+
+ /**
+ * Get the foreign value from the given model.
+ *
+ * Retrieves the last RID from the models Object SID.
+ *
+ * @param Model $model
+ *
+ * @return string
+ */
+ protected function getForeignValueFromModel(Model $model)
+ {
+ $objectSidComponents = explode('-', $model->getConvertedSid());
+
+ return end($objectSidComponents);
+ }
+
+ /**
+ * Get the parent relationship models converted object sid.
+ *
+ * @return string
+ */
+ protected function getParentModelObjectSid()
+ {
+ return preg_replace(
+ '/\d+$/',
+ $this->parent->getFirstAttribute($this->relationKey),
+ $this->parent->getConvertedSid()
+ );
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/HasServerRoleAttribute.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/HasServerRoleAttribute.php
new file mode 100644
index 0000000..cd08648
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/HasServerRoleAttribute.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class HasServerRoleAttribute implements Scope
+{
+ /**
+ * Includes condition of having a serverRole attribute.
+ *
+ * @param Builder $query
+ * @param Model $model
+ *
+ * @return void
+ */
+ public function apply(Builder $query, Model $model)
+ {
+ $query->whereHas('serverRole');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/InConfigurationContext.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/InConfigurationContext.php
new file mode 100644
index 0000000..2b1a177
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/InConfigurationContext.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Scopes;
+
+use LdapRecord\Models\ActiveDirectory\Entry;
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class InConfigurationContext implements Scope
+{
+ /**
+ * Refines the base dn to be inside the configuration context.
+ *
+ * @param Builder $query
+ * @param Model $model
+ *
+ * @throws \LdapRecord\Models\ModelNotFoundException
+ *
+ * @return void
+ */
+ public function apply(Builder $query, Model $model)
+ {
+ $query->in($this->getConfigurationNamingContext($model));
+ }
+
+ /**
+ * Get the LDAP server configuration naming context distinguished name.
+ *
+ * @param Model $model
+ *
+ * @throws \LdapRecord\Models\ModelNotFoundException
+ *
+ * @return mixed
+ */
+ protected function getConfigurationNamingContext(Model $model)
+ {
+ return Entry::getRootDse($model->getConnectionName())
+ ->getFirstAttribute('configurationNamingContext');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/RejectComputerObjectClass.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/RejectComputerObjectClass.php
new file mode 100644
index 0000000..a616db1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/Scopes/RejectComputerObjectClass.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class RejectComputerObjectClass implements Scope
+{
+ /**
+ * Prevent computer objects from being included in results.
+ *
+ * @param Builder $query
+ * @param Model $model
+ *
+ * @return void
+ */
+ public function apply(Builder $query, Model $model)
+ {
+ $query->where('objectclass', '!=', 'computer');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/User.php
new file mode 100644
index 0000000..84dd74b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ActiveDirectory/User.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace LdapRecord\Models\ActiveDirectory;
+
+use Illuminate\Contracts\Auth\Authenticatable;
+use LdapRecord\Models\ActiveDirectory\Concerns\HasPrimaryGroup;
+use LdapRecord\Models\ActiveDirectory\Scopes\RejectComputerObjectClass;
+use LdapRecord\Models\Concerns\CanAuthenticate;
+use LdapRecord\Models\Concerns\HasPassword;
+use LdapRecord\Query\Model\Builder;
+
+class User extends Entry implements Authenticatable
+{
+ use HasPassword;
+ use HasPrimaryGroup;
+ use CanAuthenticate;
+
+ /**
+ * The password's attribute name.
+ *
+ * @var string
+ */
+ protected $passwordAttribute = 'unicodepwd';
+
+ /**
+ * The password's hash method.
+ *
+ * @var string
+ */
+ protected $passwordHashMethod = 'encode';
+
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'person',
+ 'organizationalperson',
+ 'user',
+ ];
+
+ /**
+ * The attributes that should be mutated to dates.
+ *
+ * @var array
+ */
+ protected $dates = [
+ 'lastlogon' => 'windows-int',
+ 'lastlogoff' => 'windows-int',
+ 'pwdlastset' => 'windows-int',
+ 'lockouttime' => 'windows-int',
+ 'accountexpires' => 'windows-int',
+ 'badpasswordtime' => 'windows-int',
+ 'lastlogontimestamp' => 'windows-int',
+ ];
+
+ /**
+ * @inheritdoc
+ */
+ protected static function boot()
+ {
+ parent::boot();
+
+ // Here we will add a global scope to reject the 'computer' object
+ // class. This is needed due to computer objects containing all
+ // of the ActiveDirectory 'user' object classes. Without
+ // this scope, they would be included in results.
+ static::addGlobalScope(new RejectComputerObjectClass());
+ }
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the user is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(Group::class, 'member')->with($this->primaryGroup());
+ }
+
+ /**
+ * The manager relationship.
+ *
+ * Retrieves the manager of the user.
+ *
+ * @return \LdapRecord\Models\Relations\HasOne
+ */
+ public function manager()
+ {
+ return $this->hasOne(static::class, 'manager');
+ }
+
+ /**
+ * The primary group relationship of the current user.
+ *
+ * Retrieves the primary group the user is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasOne
+ */
+ public function primaryGroup()
+ {
+ return $this->hasOnePrimaryGroup(Group::class, 'primarygroupid');
+ }
+
+ /**
+ * Scopes the query to exchange mailbox users.
+ *
+ * @param Builder $query
+ *
+ * @return Builder
+ */
+ public function scopeWhereHasMailbox(Builder $query)
+ {
+ return $query->whereHas('msExchMailboxGuid');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/AccountControl.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/AccountControl.php
new file mode 100644
index 0000000..9c6240b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/AccountControl.php
@@ -0,0 +1,502 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use ReflectionClass;
+
+class AccountControl
+{
+ const SCRIPT = 1;
+
+ const ACCOUNTDISABLE = 2;
+
+ const HOMEDIR_REQUIRED = 8;
+
+ const LOCKOUT = 16;
+
+ const PASSWD_NOTREQD = 32;
+
+ const PASSWD_CANT_CHANGE = 64;
+
+ const ENCRYPTED_TEXT_PWD_ALLOWED = 128;
+
+ const TEMP_DUPLICATE_ACCOUNT = 256;
+
+ const NORMAL_ACCOUNT = 512;
+
+ const INTERDOMAIN_TRUST_ACCOUNT = 2048;
+
+ const WORKSTATION_TRUST_ACCOUNT = 4096;
+
+ const SERVER_TRUST_ACCOUNT = 8192;
+
+ const DONT_EXPIRE_PASSWORD = 65536;
+
+ const MNS_LOGON_ACCOUNT = 131072;
+
+ const SMARTCARD_REQUIRED = 262144;
+
+ const TRUSTED_FOR_DELEGATION = 524288;
+
+ const NOT_DELEGATED = 1048576;
+
+ const USE_DES_KEY_ONLY = 2097152;
+
+ const DONT_REQ_PREAUTH = 4194304;
+
+ const PASSWORD_EXPIRED = 8388608;
+
+ const TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216;
+
+ const PARTIAL_SECRETS_ACCOUNT = 67108864;
+
+ /**
+ * The account control flag values.
+ *
+ * @var array
+ */
+ protected $values = [];
+
+ /**
+ * Constructor.
+ *
+ * @param int $flag
+ */
+ public function __construct($flag = null)
+ {
+ if (! is_null($flag)) {
+ $this->apply($flag);
+ }
+ }
+
+ /**
+ * Get the value when casted to string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->getValue();
+ }
+
+ /**
+ * Get the value when casted to int.
+ *
+ * @return int
+ */
+ public function __toInt()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * Add the flag to the account control values.
+ *
+ * @param int $flag
+ *
+ * @return $this
+ */
+ public function add($flag)
+ {
+ // Use the value as a key so if the same value
+ // is used, it will always be overwritten
+ $this->values[$flag] = $flag;
+
+ return $this;
+ }
+
+ /**
+ * Remove the flag from the account control.
+ *
+ * @param int $flag
+ *
+ * @return $this
+ */
+ public function remove($flag)
+ {
+ unset($this->values[$flag]);
+
+ return $this;
+ }
+
+ /**
+ * Extract and apply the flag.
+ *
+ * @param int $flag
+ *
+ * @return void
+ */
+ public function apply($flag)
+ {
+ $this->setValues($this->extractFlags($flag));
+ }
+
+ /**
+ * Determine if the account control contains the given UAC flag(s).
+ *
+ * @param int $flag
+ *
+ * @return bool
+ */
+ public function has($flag)
+ {
+ // Here we will extract the given flag into an array
+ // of possible flags. This will allow us to see if
+ // our AccountControl object contains any of them.
+ $flagsUsed = array_intersect(
+ $this->extractFlags($flag),
+ $this->values
+ );
+
+ return in_array($flag, $flagsUsed);
+ }
+
+ /**
+ * Determine if the account control does not contain the given UAC flag(s).
+ *
+ * @param int $flag
+ *
+ * @return bool
+ */
+ public function doesntHave($flag)
+ {
+ return ! $this->has($flag);
+ }
+
+ /**
+ * Generate an LDAP filter based on the current value.
+ *
+ * @return string
+ */
+ public function filter()
+ {
+ return sprintf('(UserAccountControl:1.2.840.113556.1.4.803:=%s)', $this->getValue());
+ }
+
+ /**
+ * The logon script will be run.
+ *
+ * @return $this
+ */
+ public function runLoginScript()
+ {
+ return $this->add(static::SCRIPT);
+ }
+
+ /**
+ * The user account is locked.
+ *
+ * @return $this
+ */
+ public function accountIsLocked()
+ {
+ return $this->add(static::LOCKOUT);
+ }
+
+ /**
+ * The user account is disabled.
+ *
+ * @return $this
+ */
+ public function accountIsDisabled()
+ {
+ return $this->add(static::ACCOUNTDISABLE);
+ }
+
+ /**
+ * This is an account for users whose primary account is in another domain.
+ *
+ * This account provides user access to this domain, but not to any domain that
+ * trusts this domain. This is sometimes referred to as a local user account.
+ *
+ * @return $this
+ */
+ public function accountIsTemporary()
+ {
+ return $this->add(static::TEMP_DUPLICATE_ACCOUNT);
+ }
+
+ /**
+ * This is a default account type that represents a typical user.
+ *
+ * @return $this
+ */
+ public function accountIsNormal()
+ {
+ return $this->add(static::NORMAL_ACCOUNT);
+ }
+
+ /**
+ * This is a permit to trust an account for a system domain that trusts other domains.
+ *
+ * @return $this
+ */
+ public function accountIsForInterdomain()
+ {
+ return $this->add(static::INTERDOMAIN_TRUST_ACCOUNT);
+ }
+
+ /**
+ * This is a computer account for a computer that is running Microsoft
+ * Windows NT 4.0 Workstation, Microsoft Windows NT 4.0 Server, Microsoft
+ * Windows 2000 Professional, or Windows 2000 Server and is a member of this domain.
+ *
+ * @return $this
+ */
+ public function accountIsForWorkstation()
+ {
+ return $this->add(static::WORKSTATION_TRUST_ACCOUNT);
+ }
+
+ /**
+ * This is a computer account for a domain controller that is a member of this domain.
+ *
+ * @return $this
+ */
+ public function accountIsForServer()
+ {
+ return $this->add(static::SERVER_TRUST_ACCOUNT);
+ }
+
+ /**
+ * This is an MNS logon account.
+ *
+ * @return $this
+ */
+ public function accountIsMnsLogon()
+ {
+ return $this->add(static::MNS_LOGON_ACCOUNT);
+ }
+
+ /**
+ * (Windows 2000/Windows Server 2003) This account does
+ * not require Kerberos pre-authentication for logging on.
+ *
+ * @return $this
+ */
+ public function accountDoesNotRequirePreAuth()
+ {
+ return $this->add(static::DONT_REQ_PREAUTH);
+ }
+
+ /**
+ * When this flag is set, it forces the user to log on by using a smart card.
+ *
+ * @return $this
+ */
+ public function accountRequiresSmartCard()
+ {
+ return $this->add(static::SMARTCARD_REQUIRED);
+ }
+
+ /**
+ * (Windows Server 2008/Windows Server 2008 R2) The account is a read-only domain controller (RODC).
+ *
+ * This is a security-sensitive setting. Removing this setting from an RODC compromises security on that server.
+ *
+ * @return $this
+ */
+ public function accountIsReadOnly()
+ {
+ return $this->add(static::PARTIAL_SECRETS_ACCOUNT);
+ }
+
+ /**
+ * The home folder is required.
+ *
+ * @return $this
+ */
+ public function homeFolderIsRequired()
+ {
+ return $this->add(static::HOMEDIR_REQUIRED);
+ }
+
+ /**
+ * No password is required.
+ *
+ * @return $this
+ */
+ public function passwordIsNotRequired()
+ {
+ return $this->add(static::PASSWD_NOTREQD);
+ }
+
+ /**
+ * The user cannot change the password. This is a permission on the user's object.
+ *
+ * For information about how to programmatically set this permission, visit the following link:
+ *
+ * @see http://msdn2.microsoft.com/en-us/library/aa746398.aspx
+ *
+ * @return $this
+ */
+ public function passwordCannotBeChanged()
+ {
+ return $this->add(static::PASSWD_CANT_CHANGE);
+ }
+
+ /**
+ * Represents the password, which should never expire on the account.
+ *
+ * @return $this
+ */
+ public function passwordDoesNotExpire()
+ {
+ return $this->add(static::DONT_EXPIRE_PASSWORD);
+ }
+
+ /**
+ * (Windows 2000/Windows Server 2003) The user's password has expired.
+ *
+ * @return $this
+ */
+ public function passwordIsExpired()
+ {
+ return $this->add(static::PASSWORD_EXPIRED);
+ }
+
+ /**
+ * The user can send an encrypted password.
+ *
+ * @return $this
+ */
+ public function allowEncryptedTextPassword()
+ {
+ return $this->add(static::ENCRYPTED_TEXT_PWD_ALLOWED);
+ }
+
+ /**
+ * When this flag is set, the service account (the user or computer account)
+ * under which a service runs is trusted for Kerberos delegation.
+ *
+ * Any such service can impersonate a client requesting the service.
+ *
+ * To enable a service for Kerberos delegation, you must set this
+ * flag on the userAccountControl property of the service account.
+ *
+ * @return $this
+ */
+ public function trustForDelegation()
+ {
+ return $this->add(static::TRUSTED_FOR_DELEGATION);
+ }
+
+ /**
+ * (Windows 2000/Windows Server 2003) The account is enabled for delegation.
+ *
+ * This is a security-sensitive setting. Accounts that have this option enabled
+ * should be tightly controlled. This setting lets a service that runs under the
+ * account assume a client's identity and authenticate as that user to other remote
+ * servers on the network.
+ *
+ * @return $this
+ */
+ public function trustToAuthForDelegation()
+ {
+ return $this->add(static::TRUSTED_TO_AUTH_FOR_DELEGATION);
+ }
+
+ /**
+ * When this flag is set, the security context of the user is not delegated to a
+ * service even if the service account is set as trusted for Kerberos delegation.
+ *
+ * @return $this
+ */
+ public function doNotTrustForDelegation()
+ {
+ return $this->add(static::NOT_DELEGATED);
+ }
+
+ /**
+ * (Windows 2000/Windows Server 2003) Restrict this principal to
+ * use only Data Encryption Standard (DES) encryption types for keys.
+ *
+ * @return $this
+ */
+ public function useDesKeyOnly()
+ {
+ return $this->add(static::USE_DES_KEY_ONLY);
+ }
+
+ /**
+ * Get the account control value.
+ *
+ * @return int
+ */
+ public function getValue()
+ {
+ return array_sum($this->values);
+ }
+
+ /**
+ * Get the account control flag values.
+ *
+ * @return array
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ /**
+ * Set the account control values.
+ *
+ * @param array $flags
+ *
+ * @return void
+ */
+ public function setValues(array $flags)
+ {
+ $this->values = $flags;
+ }
+
+ /**
+ * Get all flags that are currently applied to the value.
+ *
+ * @return array
+ */
+ public function getAppliedFlags()
+ {
+ $flags = $this->getAllFlags();
+
+ $exists = [];
+
+ foreach ($flags as $name => $flag) {
+ if ($this->has($flag)) {
+ $exists[$name] = $flag;
+ }
+ }
+
+ return $exists;
+ }
+
+ /**
+ * Get all possible account control flags.
+ *
+ * @return array
+ */
+ public function getAllFlags()
+ {
+ return (new ReflectionClass(__CLASS__))->getConstants();
+ }
+
+ /**
+ * Extracts the given flag into an array of flags used.
+ *
+ * @param int $flag
+ *
+ * @return array
+ */
+ public function extractFlags($flag)
+ {
+ $flags = [];
+
+ for ($i = 0; $i <= 26; $i++) {
+ if ((int) $flag & (1 << $i)) {
+ $flags[1 << $i] = 1 << $i;
+ }
+ }
+
+ return $flags;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedName.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedName.php
new file mode 100644
index 0000000..c092173
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedName.php
@@ -0,0 +1,419 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use LdapRecord\EscapesValues;
+use LdapRecord\Support\Arr;
+
+class DistinguishedName
+{
+ use EscapesValues;
+
+ /**
+ * The underlying raw value.
+ *
+ * @var string|null
+ */
+ protected $value;
+
+ /**
+ * Constructor.
+ *
+ * @param string|null $value
+ */
+ public function __construct($value = null)
+ {
+ $this->value = trim($value);
+ }
+
+ /**
+ * Get the distinguished name value.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->value;
+ }
+
+ /**
+ * Alias of the "build" method.
+ *
+ * @param string|null $value
+ *
+ * @return DistinguishedNameBuilder
+ */
+ public static function of($value = null)
+ {
+ return static::build($value);
+ }
+
+ /**
+ * Get a new DN builder object from the given DN.
+ *
+ * @param string|null $value
+ *
+ * @return DistinguishedNameBuilder
+ */
+ public static function build($value = null)
+ {
+ return new DistinguishedNameBuilder($value);
+ }
+
+ /**
+ * Make a new distinguished name instance.
+ *
+ * @param string|null $value
+ *
+ * @return static
+ */
+ public static function make($value = null)
+ {
+ return new static($value);
+ }
+
+ /**
+ * Explode a distinguished name into relative distinguished names.
+ *
+ * @param string $dn
+ *
+ * @return array
+ */
+ public static function explode($dn)
+ {
+ $dn = ldap_explode_dn($dn, $withoutAttributes = false);
+
+ if (! is_array($dn)) {
+ return [];
+ }
+
+ if (! array_key_exists('count', $dn)) {
+ return [];
+ }
+
+ unset($dn['count']);
+
+ return $dn;
+ }
+
+ /**
+ * Un-escapes a hexadecimal string into its original string representation.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public static function unescape($value)
+ {
+ return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
+ return chr(hexdec($matches[1]));
+ }, $value);
+ }
+
+ /**
+ * Explode the RDN into an attribute and value.
+ *
+ * @param string $rdn
+ *
+ * @return array
+ */
+ public static function explodeRdn($rdn)
+ {
+ return explode('=', $rdn, $limit = 2);
+ }
+
+ /**
+ * Implode the component attribute and value into an RDN.
+ *
+ * @param string $rdn
+ *
+ * @return string
+ */
+ public static function makeRdn(array $component)
+ {
+ return implode('=', $component);
+ }
+
+ /**
+ * Get the underlying value.
+ *
+ * @return string|null
+ */
+ public function get()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set the underlying value.
+ *
+ * @param string|null $value
+ *
+ * @return $this
+ */
+ public function set($value)
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get the distinguished name values without attributes.
+ *
+ * @return array
+ */
+ public function values()
+ {
+ $values = [];
+
+ foreach ($this->multi() as [, $value]) {
+ $values[] = static::unescape($value);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Get the distinguished name attributes without values.
+ *
+ * @return array
+ */
+ public function attributes()
+ {
+ $attributes = [];
+
+ foreach ($this->multi() as [$attribute]) {
+ $attributes[] = $attribute;
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Get the distinguished name components with attributes.
+ *
+ * @return array
+ */
+ public function components()
+ {
+ $components = [];
+
+ foreach ($this->multi() as [$attribute, $value]) {
+ // When a distinguished name is exploded, the values are automatically
+ // escaped. This cannot be opted out of. Here we will unescape
+ // the attribute value, then re-escape it to its original
+ // representation from the server using the "dn" flag.
+ $value = $this->escape(static::unescape($value))->dn();
+
+ $components[] = static::makeRdn([$attribute, $value]);
+ }
+
+ return $components;
+ }
+
+ /**
+ * Convert the distinguished name into an associative array.
+ *
+ * @return array
+ */
+ public function assoc()
+ {
+ $map = [];
+
+ foreach ($this->multi() as [$attribute, $value]) {
+ $attribute = $this->normalize($attribute);
+
+ array_key_exists($attribute, $map)
+ ? $map[$attribute][] = $value
+ : $map[$attribute] = [$value];
+ }
+
+ return $map;
+ }
+
+ /**
+ * Split the RDNs into a multi-dimensional array.
+ *
+ * @return array
+ */
+ public function multi()
+ {
+ return array_map(function ($rdn) {
+ return static::explodeRdn($rdn);
+ }, $this->rdns());
+ }
+
+ /**
+ * Split the distinguished name into an array of unescaped RDN's.
+ *
+ * @return array
+ */
+ public function rdns()
+ {
+ return static::explode($this->value);
+ }
+
+ /**
+ * Get the first RDNs value.
+ *
+ * @return string|null
+ */
+ public function name()
+ {
+ return Arr::first($this->values());
+ }
+
+ /**
+ * Get the first RDNs attribute.
+ *
+ * @return string|null
+ */
+ public function head()
+ {
+ return Arr::first($this->attributes());
+ }
+
+ /**
+ * Get the relative distinguished name.
+ *
+ * @return string|null
+ */
+ public function relative()
+ {
+ return Arr::first($this->components());
+ }
+
+ /**
+ * Alias of relative().
+ *
+ * Get the first RDN from the distinguished name.
+ *
+ * @return string|null
+ */
+ public function first()
+ {
+ return $this->relative();
+ }
+
+ /**
+ * Get the parent distinguished name.
+ *
+ * @return string|null
+ */
+ public function parent()
+ {
+ $components = $this->components();
+
+ array_shift($components);
+
+ return implode(',', $components) ?: null;
+ }
+
+ /**
+ * Determine if the current distinguished name is a parent of the given child.
+ *
+ * @param DistinguishedName $child
+ *
+ * @return bool
+ */
+ public function isParentOf(self $child)
+ {
+ return $child->isChildOf($this);
+ }
+
+ /**
+ * Determine if the current distinguished name is a child of the given parent.
+ *
+ * @param DistinguishedName $parent
+ *
+ * @return bool
+ */
+ public function isChildOf(self $parent)
+ {
+ if (
+ empty($components = $this->components()) ||
+ empty($parentComponents = $parent->components())
+ ) {
+ return false;
+ }
+
+ array_shift($components);
+
+ return $this->compare($components, $parentComponents);
+ }
+
+ /**
+ * Determine if the current distinguished name is an ancestor of the descendant.
+ *
+ * @param DistinguishedName $descendant
+ *
+ * @return bool
+ */
+ public function isAncestorOf(self $descendant)
+ {
+ return $descendant->isDescendantOf($this);
+ }
+
+ /**
+ * Determine if the current distinguished name is a descendant of the ancestor.
+ *
+ * @param DistinguishedName $ancestor
+ *
+ * @return bool
+ */
+ public function isDescendantOf(self $ancestor)
+ {
+ if (
+ empty($components = $this->components()) ||
+ empty($ancestorComponents = $ancestor->components())
+ ) {
+ return false;
+ }
+
+ if (! $length = count($components) - count($ancestorComponents)) {
+ return false;
+ }
+
+ array_splice($components, $offset = 0, $length);
+
+ return $this->compare($components, $ancestorComponents);
+ }
+
+ /**
+ * Compare whether the two distinguished name values are equal.
+ *
+ * @param array $values
+ * @param array $other
+ *
+ * @return bool
+ */
+ protected function compare(array $values, array $other)
+ {
+ return $this->recase($values) == $this->recase($other);
+ }
+
+ /**
+ * Recase the array values.
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ protected function recase(array $values)
+ {
+ return array_map([$this, 'normalize'], $values);
+ }
+
+ /**
+ * Normalize the string value.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ protected function normalize($value)
+ {
+ return strtolower($value);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedNameBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedNameBuilder.php
new file mode 100644
index 0000000..83dfe71
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/DistinguishedNameBuilder.php
@@ -0,0 +1,251 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use LdapRecord\EscapesValues;
+use LdapRecord\Support\Arr;
+
+class DistinguishedNameBuilder
+{
+ use EscapesValues;
+
+ /**
+ * The components of the DN.
+ *
+ * @var array
+ */
+ protected $components = [];
+
+ /**
+ * Whether to output the DN in reverse.
+ *
+ * @var bool
+ */
+ protected $reverse = false;
+
+ /**
+ * Constructor.
+ *
+ * @param string|null $value
+ */
+ public function __construct($dn = null)
+ {
+ $this->components = array_map(function ($rdn) {
+ return DistinguishedName::explodeRdn($rdn);
+ }, DistinguishedName::make($dn)->components());
+ }
+
+ /**
+ * Forward missing method calls onto the Distinguished Name object.
+ *
+ * @param string $method
+ * @param array $args
+ *
+ * @return mixed
+ */
+ public function __call($method, $args)
+ {
+ return $this->get()->{$method}(...$args);
+ }
+
+ /**
+ * Get the distinguished name value.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->get();
+ }
+
+ /**
+ * Prepend an RDN onto the DN.
+ *
+ * @param string|array $attribute
+ * @param string|null $value
+ *
+ * @return $this
+ */
+ public function prepend($attribute, $value = null)
+ {
+ array_unshift(
+ $this->components,
+ ...$this->componentize($attribute, $value)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Append an RDN onto the DN.
+ *
+ * @param string|array $attribute
+ * @param string|null $value
+ *
+ * @return $this
+ */
+ public function append($attribute, $value = null)
+ {
+ array_push(
+ $this->components,
+ ...$this->componentize($attribute, $value)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Componentize the attribute and value.
+ *
+ * @param string|array $attribute
+ * @param string|null $value
+ *
+ * @return array
+ */
+ protected function componentize($attribute, $value = null)
+ {
+ // Here we will make the assumption that an array of
+ // RDN's have been given if the value is null, and
+ // attempt to break them into their components.
+ if (is_null($value)) {
+ $attributes = Arr::wrap($attribute);
+
+ $components = array_map([$this, 'makeComponentizedArray'], $attributes);
+ } else {
+ $components = [[$attribute, $value]];
+ }
+
+ return array_map(function ($component) {
+ [$attribute, $value] = $component;
+
+ return $this->makeAppendableComponent($attribute, $value);
+ }, $components);
+ }
+
+ /**
+ * Make a componentized array by exploding the value if it's a string.
+ *
+ * @param string $value
+ *
+ * @return array
+ */
+ protected function makeComponentizedArray($value)
+ {
+ return is_array($value) ? $value : DistinguishedName::explodeRdn($value);
+ }
+
+ /**
+ * Make an appendable component array from the attribute and value.
+ *
+ * @param string|array $attribute
+ * @param string|null $value
+ *
+ * @return array
+ */
+ protected function makeAppendableComponent($attribute, $value = null)
+ {
+ return [trim($attribute), $this->escape(trim($value))->dn()];
+ }
+
+ /**
+ * Pop an RDN off of the end of the DN.
+ *
+ * @param int $amount
+ * @param array $removed
+ *
+ * @return $this
+ */
+ public function pop($amount = 1, &$removed = [])
+ {
+ $removed = array_map(function ($component) {
+ return DistinguishedName::makeRdn($component);
+ }, array_splice($this->components, -$amount, $amount));
+
+ return $this;
+ }
+
+ /**
+ * Shift an RDN off of the beginning of the DN.
+ *
+ * @param int $amount
+ * @param array $removed
+ *
+ * @return $this
+ */
+ public function shift($amount = 1, &$removed = [])
+ {
+ $removed = array_map(function ($component) {
+ return DistinguishedName::makeRdn($component);
+ }, array_splice($this->components, 0, $amount));
+
+ return $this;
+ }
+
+ /**
+ * Whether to output the DN in reverse.
+ *
+ * @return $this
+ */
+ public function reverse()
+ {
+ $this->reverse = true;
+
+ return $this;
+ }
+
+ /**
+ * Get the components of the DN.
+ *
+ * @param null|string $type
+ *
+ * @return array
+ */
+ public function components($type = null)
+ {
+ return is_null($type)
+ ? $this->components
+ : $this->componentsOfType($type);
+ }
+
+ /**
+ * Get the components of a particular type.
+ *
+ * @param string $type
+ *
+ * @return array
+ */
+ protected function componentsOfType($type)
+ {
+ $components = array_filter($this->components, function ($component) use ($type) {
+ return ([$name] = $component) && strtolower($name) === strtolower($type);
+ });
+
+ return array_values($components);
+ }
+
+ /**
+ * Get the fully qualified DN.
+ *
+ * @return DistinguishedName
+ */
+ public function get()
+ {
+ return new DistinguishedName($this->build());
+ }
+
+ /**
+ * Build the distinguished name from the components.
+ *
+ * @return $this
+ */
+ protected function build()
+ {
+ $components = $this->reverse
+ ? array_reverse($this->components)
+ : $this->components;
+
+ return implode(',', array_map(function ($component) {
+ return DistinguishedName::makeRdn($component);
+ }, $components));
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/EscapedValue.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/EscapedValue.php
new file mode 100644
index 0000000..cc04a67
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/EscapedValue.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+class EscapedValue
+{
+ /**
+ * The value to be escaped.
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * The characters to ignore when escaping.
+ *
+ * @var string
+ */
+ protected $ignore;
+
+ /**
+ * The escape flags.
+ *
+ * @var int
+ */
+ protected $flags;
+
+ /**
+ * Constructor.
+ *
+ * @param string $value
+ * @param string $ignore
+ * @param int $flags
+ */
+ public function __construct($value, $ignore = '', $flags = 0)
+ {
+ $this->value = $value;
+ $this->ignore = $ignore;
+ $this->flags = $flags;
+ }
+
+ /**
+ * Get the escaped value.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->get();
+ }
+
+ /**
+ * Get the escaped value.
+ *
+ * @return mixed
+ */
+ public function get()
+ {
+ return ldap_escape($this->value, $this->ignore, $this->flags);
+ }
+
+ /**
+ * Set the characters to exclude from being escaped.
+ *
+ * @param string $characters
+ *
+ * @return $this
+ */
+ public function ignore($characters)
+ {
+ $this->ignore = $characters;
+
+ return $this;
+ }
+
+ /**
+ * Prepare the value to be escaped for use in a distinguished name.
+ *
+ * @return $this
+ */
+ public function dn()
+ {
+ $this->flags = LDAP_ESCAPE_DN;
+
+ return $this;
+ }
+
+ /**
+ * Prepare the value to be escaped for use in a filter.
+ *
+ * @return $this
+ */
+ public function filter()
+ {
+ $this->flags = LDAP_ESCAPE_FILTER;
+
+ return $this;
+ }
+
+ /**
+ * Prepare the value to be escaped for use in a distinguished name and filter.
+ *
+ * @return $this
+ */
+ public function both()
+ {
+ $this->flags = LDAP_ESCAPE_FILTER + LDAP_ESCAPE_DN;
+
+ return $this;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Guid.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Guid.php
new file mode 100644
index 0000000..d139f5f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Guid.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+use LdapRecord\Utilities;
+
+class Guid
+{
+ /**
+ * The string GUID value.
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * The guid structure in order by section to parse using substr().
+ *
+ * @author Chad Sikorra <Chad.Sikorra@gmail.com>
+ *
+ * @see https://github.com/ldaptools/ldaptools
+ *
+ * @var array
+ */
+ protected $guidSections = [
+ [[-26, 2], [-28, 2], [-30, 2], [-32, 2]],
+ [[-22, 2], [-24, 2]],
+ [[-18, 2], [-20, 2]],
+ [[-16, 4]],
+ [[-12, 12]],
+ ];
+
+ /**
+ * The hexadecimal octet order based on string position.
+ *
+ * @author Chad Sikorra <Chad.Sikorra@gmail.com>
+ *
+ * @see https://github.com/ldaptools/ldaptools
+ *
+ * @var array
+ */
+ protected $octetSections = [
+ [6, 4, 2, 0],
+ [10, 8],
+ [14, 12],
+ [16, 18, 20, 22, 24, 26, 28, 30],
+ ];
+
+ /**
+ * Determines if the specified GUID is valid.
+ *
+ * @param string $guid
+ *
+ * @return bool
+ */
+ public static function isValid($guid)
+ {
+ return Utilities::isValidGuid($guid);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param mixed $value
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct($value)
+ {
+ if (static::isValid($value)) {
+ $this->value = $value;
+ } elseif ($value = $this->binaryGuidToString($value)) {
+ $this->value = $value;
+ } else {
+ throw new InvalidArgumentException('Invalid Binary / String GUID.');
+ }
+ }
+
+ /**
+ * Returns the string value of the GUID.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * Returns the string value of the SID.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Get the binary representation of the GUID string.
+ *
+ * @return string
+ */
+ public function getBinary()
+ {
+ return hex2bin($this->getHex());
+ }
+
+ /**
+ * Get the hexadecimal representation of the GUID string.
+ *
+ * @return string
+ */
+ public function getHex()
+ {
+ $data = '';
+
+ $guid = str_replace('-', '', $this->value);
+
+ foreach ($this->octetSections as $section) {
+ $data .= $this->parseSection($guid, $section, $octet = true);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Returns the string variant of a binary GUID.
+ *
+ * @param string $binary
+ *
+ * @return string|null
+ */
+ protected function binaryGuidToString($binary)
+ {
+ return Utilities::binaryGuidToString($binary);
+ }
+
+ /**
+ * Return the specified section of the hexadecimal string.
+ *
+ * @author Chad Sikorra <Chad.Sikorra@gmail.com>
+ *
+ * @see https://github.com/ldaptools/ldaptools
+ *
+ * @param string $hex The full hex string.
+ * @param array $sections An array of start and length (unless octet is true, then length is always 2).
+ * @param bool $octet Whether this is for octet string form.
+ *
+ * @return string The concatenated sections in upper-case.
+ */
+ protected function parseSection($hex, array $sections, $octet = false)
+ {
+ $parsedString = '';
+
+ foreach ($sections as $section) {
+ $start = $octet ? $section : $section[0];
+
+ $length = $octet ? 2 : $section[1];
+
+ $parsedString .= substr($hex, $start, $length);
+ }
+
+ return $parsedString;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/MbString.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/MbString.php
new file mode 100644
index 0000000..672e60d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/MbString.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+class MbString
+{
+ /**
+ * Get the integer value of a specific character.
+ *
+ * @param $string
+ *
+ * @return int
+ */
+ public static function ord($string)
+ {
+ if (static::isLoaded()) {
+ $result = unpack('N', mb_convert_encoding($string, 'UCS-4BE', 'UTF-8'));
+
+ if (is_array($result)) {
+ return $result[1];
+ }
+ }
+
+ return ord($string);
+ }
+
+ /**
+ * Get the character for a specific integer value.
+ *
+ * @param $int
+ *
+ * @return string
+ */
+ public static function chr($int)
+ {
+ if (static::isLoaded()) {
+ return mb_convert_encoding(pack('n', $int), 'UTF-8', 'UTF-16BE');
+ }
+
+ return chr($int);
+ }
+
+ /**
+ * Split a string into its individual characters and return it as an array.
+ *
+ * @param string $value
+ *
+ * @return string[]
+ */
+ public static function split($value)
+ {
+ return preg_split('/(?<!^)(?!$)/u', $value);
+ }
+
+ /**
+ * Detects if the given string is UTF 8.
+ *
+ * @param $string
+ *
+ * @return string|false
+ */
+ public static function isUtf8($string)
+ {
+ if (static::isLoaded()) {
+ return mb_detect_encoding($string, 'UTF-8', $strict = true);
+ }
+
+ return $string;
+ }
+
+ /**
+ * Checks if the mbstring extension is enabled in PHP.
+ *
+ * @return bool
+ */
+ public static function isLoaded()
+ {
+ return extension_loaded('mbstring');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Password.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Password.php
new file mode 100644
index 0000000..7f0b412
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Password.php
@@ -0,0 +1,340 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+use LdapRecord\LdapRecordException;
+use ReflectionMethod;
+
+class Password
+{
+ const CRYPT_SALT_TYPE_MD5 = 1;
+ const CRYPT_SALT_TYPE_SHA256 = 5;
+ const CRYPT_SALT_TYPE_SHA512 = 6;
+
+ /**
+ * Make an encoded password for transmission over LDAP.
+ *
+ * @param string $password
+ *
+ * @return string
+ */
+ public static function encode($password)
+ {
+ return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"');
+ }
+
+ /**
+ * Make a salted md5 password.
+ *
+ * @param string $password
+ * @param null|string $salt
+ *
+ * @return string
+ */
+ public static function smd5($password, $salt = null)
+ {
+ return '{SMD5}'.static::makeHash($password, 'md5', null, $salt ?? random_bytes(4));
+ }
+
+ /**
+ * Make a salted SHA password.
+ *
+ * @param string $password
+ * @param null|string $salt
+ *
+ * @return string
+ */
+ public static function ssha($password, $salt = null)
+ {
+ return '{SSHA}'.static::makeHash($password, 'sha1', null, $salt ?? random_bytes(4));
+ }
+
+ /**
+ * Make a salted SSHA256 password.
+ *
+ * @param string $password
+ * @param null|string $salt
+ *
+ * @return string
+ */
+ public static function ssha256($password, $salt = null)
+ {
+ return '{SSHA256}'.static::makeHash($password, 'hash', 'sha256', $salt ?? random_bytes(4));
+ }
+
+ /**
+ * Make a salted SSHA384 password.
+ *
+ * @param string $password
+ * @param null|string $salt
+ *
+ * @return string
+ */
+ public static function ssha384($password, $salt = null)
+ {
+ return '{SSHA384}'.static::makeHash($password, 'hash', 'sha384', $salt ?? random_bytes(4));
+ }
+
+ /**
+ * Make a salted SSHA512 password.
+ *
+ * @param string $password
+ * @param null|string $salt
+ *
+ * @return string
+ */
+ public static function ssha512($password, $salt = null)
+ {
+ return '{SSHA512}'.static::makeHash($password, 'hash', 'sha512', $salt ?? random_bytes(4));
+ }
+
+ /**
+ * Make a non-salted SHA password.
+ *
+ * @param string $password
+ *
+ * @return string
+ */
+ public static function sha($password)
+ {
+ return '{SHA}'.static::makeHash($password, 'sha1');
+ }
+
+ /**
+ * Make a non-salted SHA256 password.
+ *
+ * @param string $password
+ *
+ * @return string
+ */
+ public static function sha256($password)
+ {
+ return '{SHA256}'.static::makeHash($password, 'hash', 'sha256');
+ }
+
+ /**
+ * Make a non-salted SHA384 password.
+ *
+ * @param string $password
+ *
+ * @return string
+ */
+ public static function sha384($password)
+ {
+ return '{SHA384}'.static::makeHash($password, 'hash', 'sha384');
+ }
+
+ /**
+ * Make a non-salted SHA512 password.
+ *
+ * @param string $password
+ *
+ * @return string
+ */
+ public static function sha512($password)
+ {
+ return '{SHA512}'.static::makeHash($password, 'hash', 'sha512');
+ }
+
+ /**
+ * Make a non-salted md5 password.
+ *
+ * @param string $password
+ *
+ * @return string
+ */
+ public static function md5($password)
+ {
+ return '{MD5}'.static::makeHash($password, 'md5');
+ }
+
+ /**
+ * Crypt password with an MD5 salt.
+ *
+ * @param string $password
+ * @param string $salt
+ *
+ * @return string
+ */
+ public static function md5Crypt($password, $salt = null)
+ {
+ return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_MD5, $salt);
+ }
+
+ /**
+ * Crypt password with a SHA256 salt.
+ *
+ * @param string $password
+ * @param string $salt
+ *
+ * @return string
+ */
+ public static function sha256Crypt($password, $salt = null)
+ {
+ return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA256, $salt);
+ }
+
+ /**
+ * Crypt a password with a SHA512 salt.
+ *
+ * @param string $password
+ * @param string $salt
+ *
+ * @return string
+ */
+ public static function sha512Crypt($password, $salt = null)
+ {
+ return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA512, $salt);
+ }
+
+ /**
+ * Make a new password hash.
+ *
+ * @param string $password The password to make a hash of.
+ * @param string $method The hash function to use.
+ * @param string|null $algo The algorithm to use for hashing.
+ * @param string|null $salt The salt to append onto the hash.
+ *
+ * @return string
+ */
+ protected static function makeHash($password, $method, $algo = null, $salt = null)
+ {
+ $params = $algo ? [$algo, $password.$salt] : [$password.$salt];
+
+ return base64_encode(pack('H*', call_user_func($method, ...$params)).$salt);
+ }
+
+ /**
+ * Make a hashed password.
+ *
+ * @param string $password
+ * @param int $type
+ * @param null|string $salt
+ *
+ * @return string
+ */
+ protected static function makeCrypt($password, $type, $salt = null)
+ {
+ return crypt($password, $salt ?? static::makeCryptSalt($type));
+ }
+
+ /**
+ * Make a salt for the crypt() method using the given type.
+ *
+ * @param int $type
+ *
+ * @return string
+ */
+ protected static function makeCryptSalt($type)
+ {
+ [$prefix, $length] = static::makeCryptPrefixAndLength($type);
+
+ $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+ while (strlen($prefix) < $length) {
+ $prefix .= substr($chars, random_int(0, strlen($chars) - 1), 1);
+ }
+
+ return $prefix;
+ }
+
+ /**
+ * Determine the crypt prefix and length.
+ *
+ * @param int $type
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return array
+ */
+ protected static function makeCryptPrefixAndLength($type)
+ {
+ switch ($type) {
+ case static::CRYPT_SALT_TYPE_MD5:
+ return ['$1$', 12];
+ case static::CRYPT_SALT_TYPE_SHA256:
+ return ['$5$', 16];
+ case static::CRYPT_SALT_TYPE_SHA512:
+ return ['$6$', 16];
+ default:
+ throw new InvalidArgumentException("Invalid crypt type [$type].");
+ }
+ }
+
+ /**
+ * Attempt to retrieve the hash method used for the password.
+ *
+ * @param string $password
+ *
+ * @return string|void
+ */
+ public static function getHashMethod($password)
+ {
+ if (! preg_match('/^\{(\w+)\}/', $password, $matches)) {
+ return;
+ }
+
+ return $matches[1];
+ }
+
+ /**
+ * Attempt to retrieve the hash method and algorithm used for the password.
+ *
+ * @param string $password
+ *
+ * @return array|void
+ */
+ public static function getHashMethodAndAlgo($password)
+ {
+ if (! preg_match('/^\{(\w+)\}\$([0-9a-z]{1})\$/', $password, $matches)) {
+ return;
+ }
+
+ return [$matches[1], $matches[2]];
+ }
+
+ /**
+ * Attempt to retrieve a salt from the encrypted password.
+ *
+ * @throws LdapRecordException
+ *
+ * @return string
+ */
+ public static function getSalt($encryptedPassword)
+ {
+ // crypt() methods.
+ if (preg_match('/^\{(\w+)\}(\$.*\$).*$/', $encryptedPassword, $matches)) {
+ return $matches[2];
+ }
+
+ // All other methods.
+ if (preg_match('/{([^}]+)}(.*)/', $encryptedPassword, $matches)) {
+ return substr(base64_decode($matches[2]), -4);
+ }
+
+ throw new LdapRecordException('Could not extract salt from encrypted password.');
+ }
+
+ /**
+ * Determine if the hash method requires a salt to be given.
+ *
+ * @param string $method
+ *
+ * @throws \ReflectionException
+ *
+ * @return bool
+ */
+ public static function hashMethodRequiresSalt($method): bool
+ {
+ $parameters = (new ReflectionMethod(static::class, $method))->getParameters();
+
+ foreach ($parameters as $parameter) {
+ if ($parameter->name === 'salt') {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Sid.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Sid.php
new file mode 100644
index 0000000..4ec46ea
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Sid.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+use LdapRecord\Utilities;
+
+class Sid
+{
+ /**
+ * The string SID value.
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * Determines if the specified SID is valid.
+ *
+ * @param string $sid
+ *
+ * @return bool
+ */
+ public static function isValid($sid)
+ {
+ return Utilities::isValidSid($sid);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param mixed $value
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct($value)
+ {
+ if (static::isValid($value)) {
+ $this->value = $value;
+ } elseif ($value = $this->binarySidToString($value)) {
+ $this->value = $value;
+ } else {
+ throw new InvalidArgumentException('Invalid Binary / String SID.');
+ }
+ }
+
+ /**
+ * Returns the string value of the SID.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * Returns the string value of the SID.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Returns the binary variant of the SID.
+ *
+ * @return string
+ */
+ public function getBinary()
+ {
+ $sid = explode('-', ltrim($this->value, 'S-'));
+
+ $level = (int) array_shift($sid);
+
+ $authority = (int) array_shift($sid);
+
+ $subAuthorities = array_map('intval', $sid);
+
+ $params = array_merge(
+ ['C2xxNV*', $level, count($subAuthorities), $authority],
+ $subAuthorities
+ );
+
+ return call_user_func_array('pack', $params);
+ }
+
+ /**
+ * Returns the string variant of a binary SID.
+ *
+ * @param string $binary
+ *
+ * @return string|null
+ */
+ protected function binarySidToString($binary)
+ {
+ return Utilities::binarySidToString($binary);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSProperty.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSProperty.php
new file mode 100644
index 0000000..ad56aa1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSProperty.php
@@ -0,0 +1,396 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+class TSProperty
+{
+ /**
+ * Nibble control values. The first value for each is if the nibble is <= 9, otherwise the second value is used.
+ */
+ const NIBBLE_CONTROL = [
+ 'X' => ['001011', '011010'],
+ 'Y' => ['001110', '011010'],
+ ];
+
+ /**
+ * The nibble header.
+ */
+ const NIBBLE_HEADER = '1110';
+
+ /**
+ * Conversion factor needed for time values in the TSPropertyArray (stored in microseconds).
+ */
+ const TIME_CONVERSION = 60 * 1000;
+
+ /**
+ * A simple map to help determine how the property needs to be decoded/encoded from/to its binary value.
+ *
+ * There are some names that are simple repeats but have 'W' at the end. Not sure as to what that signifies. I
+ * cannot find any information on them in Microsoft documentation. However, their values appear to stay in sync with
+ * their non 'W' counterparts. But not doing so when manipulating the data manually does not seem to affect anything.
+ * This probably needs more investigation.
+ *
+ * @var array
+ */
+ protected $propTypes = [
+ 'string' => [
+ 'CtxWFHomeDir',
+ 'CtxWFHomeDirW',
+ 'CtxWFHomeDirDrive',
+ 'CtxWFHomeDirDriveW',
+ 'CtxInitialProgram',
+ 'CtxInitialProgramW',
+ 'CtxWFProfilePath',
+ 'CtxWFProfilePathW',
+ 'CtxWorkDirectory',
+ 'CtxWorkDirectoryW',
+ 'CtxCallbackNumber',
+ ],
+ 'time' => [
+ 'CtxMaxDisconnectionTime',
+ 'CtxMaxConnectionTime',
+ 'CtxMaxIdleTime',
+ ],
+ 'int' => [
+ 'CtxCfgFlags1',
+ 'CtxCfgPresent',
+ 'CtxKeyboardLayout',
+ 'CtxMinEncryptionLevel',
+ 'CtxNWLogonServer',
+ 'CtxShadow',
+ ],
+ ];
+
+ /**
+ * The property name.
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The property value.
+ *
+ * @var string|int
+ */
+ protected $value;
+
+ /**
+ * The property value type.
+ *
+ * @var int
+ */
+ protected $valueType = 1;
+
+ /**
+ * Pass binary TSProperty data to construct its object representation.
+ *
+ * @param string|null $value
+ */
+ public function __construct($value = null)
+ {
+ if ($value) {
+ $this->decode(bin2hex($value));
+ }
+ }
+
+ /**
+ * Set the name for the TSProperty.
+ *
+ * @param string $name
+ *
+ * @return TSProperty
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Get the name for the TSProperty.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the value for the TSProperty.
+ *
+ * @param string|int $value
+ *
+ * @return TSProperty
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get the value for the TSProperty.
+ *
+ * @return string|int
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Convert the TSProperty name/value back to its binary
+ * representation for the userParameters blob.
+ *
+ * @return string
+ */
+ public function toBinary()
+ {
+ $name = bin2hex($this->name);
+
+ $binValue = $this->getEncodedValueForProp($this->name, $this->value);
+
+ $valueLen = strlen(bin2hex($binValue)) / 3;
+
+ $binary = hex2bin(
+ $this->dec2hex(strlen($name))
+ .$this->dec2hex($valueLen)
+ .$this->dec2hex($this->valueType)
+ .$name
+ );
+
+ return $binary.$binValue;
+ }
+
+ /**
+ * Given a TSProperty blob, decode the name/value/type/etc.
+ *
+ * @param string $tsProperty
+ */
+ protected function decode($tsProperty)
+ {
+ $nameLength = hexdec(substr($tsProperty, 0, 2));
+
+ // 1 data byte is 3 encoded bytes
+ $valueLength = hexdec(substr($tsProperty, 2, 2)) * 3;
+
+ $this->valueType = hexdec(substr($tsProperty, 4, 2));
+ $this->name = pack('H*', substr($tsProperty, 6, $nameLength));
+ $this->value = $this->getDecodedValueForProp($this->name, substr($tsProperty, 6 + $nameLength, $valueLength));
+ }
+
+ /**
+ * Based on the property name/value in question, get its encoded form.
+ *
+ * @param string $propName
+ * @param string|int $propValue
+ *
+ * @return string
+ */
+ protected function getEncodedValueForProp($propName, $propValue)
+ {
+ if (in_array($propName, $this->propTypes['string'])) {
+ // Simple strings are null terminated. Unsure if this is
+ // needed or simply a product of how ADUC does stuff?
+ $value = $this->encodePropValue($propValue."\0", true);
+ } elseif (in_array($propName, $this->propTypes['time'])) {
+ // Needs to be in microseconds (assuming it is in minute format)...
+ $value = $this->encodePropValue($propValue * self::TIME_CONVERSION);
+ } else {
+ $value = $this->encodePropValue($propValue);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Based on the property name in question, get its actual value from the binary blob value.
+ *
+ * @param string $propName
+ * @param string $propValue
+ *
+ * @return string|int
+ */
+ protected function getDecodedValueForProp($propName, $propValue)
+ {
+ if (in_array($propName, $this->propTypes['string'])) {
+ // Strip away null terminators. I think this should
+ // be desired, otherwise it just ends in confusion.
+ $value = str_replace("\0", '', $this->decodePropValue($propValue, true));
+ } elseif (in_array($propName, $this->propTypes['time'])) {
+ // Convert from microseconds to minutes (how ADUC displays
+ // it anyway, and seems the most practical).
+ $value = hexdec($this->decodePropValue($propValue)) / self::TIME_CONVERSION;
+ } elseif (in_array($propName, $this->propTypes['int'])) {
+ $value = hexdec($this->decodePropValue($propValue));
+ } else {
+ $value = $this->decodePropValue($propValue);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Decode the property by inspecting the nibbles of each blob, checking
+ * the control, and adding up the results into a final value.
+ *
+ * @param string $hex
+ * @param bool $string Whether or not this is simple string data.
+ *
+ * @return string
+ */
+ protected function decodePropValue($hex, $string = false)
+ {
+ $decodePropValue = '';
+
+ $blobs = str_split($hex, 6);
+
+ foreach ($blobs as $blob) {
+ $bin = decbin(hexdec($blob));
+
+ $controlY = substr($bin, 4, 6);
+ $nibbleY = substr($bin, 10, 4);
+ $controlX = substr($bin, 14, 6);
+ $nibbleX = substr($bin, 20, 4);
+
+ $byte = $this->nibbleControl($nibbleX, $controlX).$this->nibbleControl($nibbleY, $controlY);
+
+ if ($string) {
+ $decodePropValue .= MbString::chr(bindec($byte));
+ } else {
+ $decodePropValue = $this->dec2hex(bindec($byte)).$decodePropValue;
+ }
+ }
+
+ return $decodePropValue;
+ }
+
+ /**
+ * Get the encoded property value as a binary blob.
+ *
+ * @param string $value
+ * @param bool $string
+ *
+ * @return string
+ */
+ protected function encodePropValue($value, $string = false)
+ {
+ // An int must be properly padded. (then split and reversed).
+ // For a string, we just split the chars. This seems
+ // to be the easiest way to handle UTF-8 characters
+ // instead of trying to work with their hex values.
+ $chars = $string ? MbString::split($value) : array_reverse(str_split($this->dec2hex($value, 8), 2));
+
+ $encoded = '';
+
+ foreach ($chars as $char) {
+ // Get the bits for the char. Using this method to ensure it is fully padded.
+ $bits = sprintf('%08b', $string ? MbString::ord($char) : hexdec($char));
+ $nibbleX = substr($bits, 0, 4);
+ $nibbleY = substr($bits, 4, 4);
+
+ // Construct the value with the header, high nibble, then low nibble.
+ $value = self::NIBBLE_HEADER;
+
+ foreach (['Y' => $nibbleY, 'X' => $nibbleX] as $nibbleType => $nibble) {
+ $value .= $this->getNibbleWithControl($nibbleType, $nibble);
+ }
+
+ // Convert it back to a binary bit stream
+ foreach ([0, 8, 16] as $start) {
+ $encoded .= $this->packBitString(substr($value, $start, 8), 8);
+ }
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * PHP's pack() function has no 'b' or 'B' template. This is
+ * a workaround that turns a literal bit-string into a
+ * packed byte-string with 8 bits per byte.
+ *
+ * @param string $bits
+ * @param bool $len
+ *
+ * @return string
+ */
+ protected function packBitString($bits, $len)
+ {
+ $bits = substr($bits, 0, $len);
+ // Pad input with zeros to next multiple of 4 above $len
+ $bits = str_pad($bits, 4 * (int) (($len + 3) / 4), '0');
+
+ // Split input into chunks of 4 bits, convert each to hex and pack them
+ $nibbles = str_split($bits, 4);
+ foreach ($nibbles as $i => $nibble) {
+ $nibbles[$i] = base_convert($nibble, 2, 16);
+ }
+
+ return pack('H*', implode('', $nibbles));
+ }
+
+ /**
+ * Based on the control, adjust the nibble accordingly.
+ *
+ * @param string $nibble
+ * @param string $control
+ *
+ * @return string
+ */
+ protected function nibbleControl($nibble, $control)
+ {
+ // This control stays constant for the low/high nibbles,
+ // so it doesn't matter which we compare to
+ if ($control == self::NIBBLE_CONTROL['X'][1]) {
+ $dec = bindec($nibble);
+ $dec += 9;
+ $nibble = str_pad(decbin($dec), 4, '0', STR_PAD_LEFT);
+ }
+
+ return $nibble;
+ }
+
+ /**
+ * Get the nibble value with the control prefixed.
+ *
+ * If the nibble dec is <= 9, the control X equals 001011 and Y equals 001110, otherwise if the nibble dec is > 9
+ * the control for X or Y equals 011010. Additionally, if the dec value of the nibble is > 9, then the nibble value
+ * must be subtracted by 9 before the final value is constructed.
+ *
+ * @param string $nibbleType Either X or Y
+ * @param string $nibble
+ *
+ * @return string
+ */
+ protected function getNibbleWithControl($nibbleType, $nibble)
+ {
+ $dec = bindec($nibble);
+
+ if ($dec > 9) {
+ $dec -= 9;
+ $control = self::NIBBLE_CONTROL[$nibbleType][1];
+ } else {
+ $control = self::NIBBLE_CONTROL[$nibbleType][0];
+ }
+
+ return $control.sprintf('%04d', decbin($dec));
+ }
+
+ /**
+ * Need to make sure hex values are always an even length, so pad as needed.
+ *
+ * @param int $int
+ * @param int $padLength The hex string must be padded to this length (with zeros).
+ *
+ * @return string
+ */
+ protected function dec2hex($int, $padLength = 2)
+ {
+ return str_pad(dechex($int), $padLength, 0, STR_PAD_LEFT);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSPropertyArray.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSPropertyArray.php
new file mode 100644
index 0000000..1831688
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/TSPropertyArray.php
@@ -0,0 +1,295 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use InvalidArgumentException;
+
+class TSPropertyArray
+{
+ /**
+ * Represents that the TSPropertyArray data is valid.
+ */
+ const VALID_SIGNATURE = 'P';
+
+ /**
+ * The default values for the TSPropertyArray structure.
+ *
+ * @var array
+ */
+ const DEFAULTS = [
+ 'CtxCfgPresent' => 2953518677,
+ 'CtxWFProfilePath' => '',
+ 'CtxWFProfilePathW' => '',
+ 'CtxWFHomeDir' => '',
+ 'CtxWFHomeDirW' => '',
+ 'CtxWFHomeDirDrive' => '',
+ 'CtxWFHomeDirDriveW' => '',
+ 'CtxShadow' => 1,
+ 'CtxMaxDisconnectionTime' => 0,
+ 'CtxMaxConnectionTime' => 0,
+ 'CtxMaxIdleTime' => 0,
+ 'CtxWorkDirectory' => '',
+ 'CtxWorkDirectoryW' => '',
+ 'CtxCfgFlags1' => 2418077696,
+ 'CtxInitialProgram' => '',
+ 'CtxInitialProgramW' => '',
+ ];
+
+ /**
+ * @var string The default data that occurs before the TSPropertyArray (CtxCfgPresent with a bunch of spaces...?)
+ */
+ protected $defaultPreBinary = '43747843666750726573656e742020202020202020202020202020202020202020202020202020202020202020202020';
+
+ /**
+ * @var TSProperty[]
+ */
+ protected $tsProperty = [];
+
+ /**
+ * @var string
+ */
+ protected $signature = self::VALID_SIGNATURE;
+
+ /**
+ * Binary data that occurs before the TSPropertyArray data in userParameters.
+ *
+ * @var string
+ */
+ protected $preBinary = '';
+
+ /**
+ * Binary data that occurs after the TSPropertyArray data in userParameters.
+ *
+ * @var string
+ */
+ protected $postBinary = '';
+
+ /**
+ * Construct in one of the following ways:.
+ *
+ * - Pass an array of TSProperty key => value pairs (See DEFAULTS constant).
+ * - Pass the userParameters binary value. The object representation of that will be decoded and constructed.
+ * - Pass nothing and a default set of TSProperty key => value pairs will be used (See DEFAULTS constant).
+ *
+ * @param mixed $tsPropertyArray
+ */
+ public function __construct($tsPropertyArray = null)
+ {
+ $this->preBinary = hex2bin($this->defaultPreBinary);
+
+ if (is_null($tsPropertyArray) || is_array($tsPropertyArray)) {
+ $tsPropertyArray = $tsPropertyArray ?: self::DEFAULTS;
+
+ foreach ($tsPropertyArray as $key => $value) {
+ $tsProperty = new TSProperty();
+
+ $this->tsProperty[$key] = $tsProperty->setName($key)->setValue($value);
+ }
+ } else {
+ $this->decodeUserParameters($tsPropertyArray);
+ }
+ }
+
+ /**
+ * Check if a specific TSProperty exists by its property name.
+ *
+ * @param string $propName
+ *
+ * @return bool
+ */
+ public function has($propName)
+ {
+ return array_key_exists(strtolower($propName), array_change_key_case($this->tsProperty));
+ }
+
+ /**
+ * Get a TSProperty object by its property name (ie. CtxWFProfilePath).
+ *
+ * @param string $propName
+ *
+ * @return TSProperty
+ */
+ public function get($propName)
+ {
+ $this->validateProp($propName);
+
+ return $this->getTsPropObj($propName);
+ }
+
+ /**
+ * Add a TSProperty object. If it already exists, it will be overwritten.
+ *
+ * @param TSProperty $tsProperty
+ *
+ * @return $this
+ */
+ public function add(TSProperty $tsProperty)
+ {
+ $this->tsProperty[$tsProperty->getName()] = $tsProperty;
+
+ return $this;
+ }
+
+ /**
+ * Remove a TSProperty by its property name (ie. CtxMinEncryptionLevel).
+ *
+ * @param string $propName
+ *
+ * @return $this
+ */
+ public function remove($propName)
+ {
+ foreach (array_keys($this->tsProperty) as $property) {
+ if (strtolower($propName) == strtolower($property)) {
+ unset($this->tsProperty[$property]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the value for a specific TSProperty by its name.
+ *
+ * @param string $propName
+ * @param mixed $propValue
+ *
+ * @return $this
+ */
+ public function set($propName, $propValue)
+ {
+ $this->validateProp($propName);
+
+ $this->getTsPropObj($propName)->setValue($propValue);
+
+ return $this;
+ }
+
+ /**
+ * Get the full binary representation of the userParameters containing the TSPropertyArray data.
+ *
+ * @return string
+ */
+ public function toBinary()
+ {
+ $binary = $this->preBinary;
+
+ $binary .= hex2bin(str_pad(dechex(MbString::ord($this->signature)), 2, 0, STR_PAD_LEFT));
+
+ $binary .= hex2bin(str_pad(dechex(count($this->tsProperty)), 2, 0, STR_PAD_LEFT));
+
+ foreach ($this->tsProperty as $tsProperty) {
+ $binary .= $tsProperty->toBinary();
+ }
+
+ return $binary.$this->postBinary;
+ }
+
+ /**
+ * Get a simple associative array containing of all TSProperty names and values.
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $userParameters = [];
+
+ foreach ($this->tsProperty as $property => $tsPropObj) {
+ $userParameters[$property] = $tsPropObj->getValue();
+ }
+
+ return $userParameters;
+ }
+
+ /**
+ * Get all TSProperty objects.
+ *
+ * @return TSProperty[]
+ */
+ public function getTSProperties()
+ {
+ return $this->tsProperty;
+ }
+
+ /**
+ * Validates that the given property name exists.
+ *
+ * @param string $propName
+ */
+ protected function validateProp($propName)
+ {
+ if (! $this->has($propName)) {
+ throw new InvalidArgumentException(sprintf('TSProperty for "%s" does not exist.', $propName));
+ }
+ }
+
+ /**
+ * @param string $propName
+ *
+ * @return TSProperty
+ */
+ protected function getTsPropObj($propName)
+ {
+ return array_change_key_case($this->tsProperty)[strtolower($propName)];
+ }
+
+ /**
+ * Get an associative array with all of the userParameters property names and values.
+ *
+ * @param string $userParameters
+ *
+ * @return void
+ */
+ protected function decodeUserParameters($userParameters)
+ {
+ $userParameters = bin2hex($userParameters);
+
+ // Save the 96-byte array of reserved data, so as to not ruin anything that may be stored there.
+ $this->preBinary = hex2bin(substr($userParameters, 0, 96));
+ // The signature is a 2-byte unicode character at the front
+ $this->signature = MbString::chr(hexdec(substr($userParameters, 96, 2)));
+ // This asserts the validity of the tsPropertyArray data. For some reason 'P' means valid...
+ if ($this->signature != self::VALID_SIGNATURE) {
+ throw new InvalidArgumentException('Invalid TSPropertyArray data');
+ }
+
+ // The property count is a 2-byte unsigned integer indicating the number of elements for the tsPropertyArray
+ // It starts at position 98. The actual variable data begins at position 100.
+ $length = $this->addTSPropData(substr($userParameters, 100), hexdec(substr($userParameters, 98, 2)));
+
+ // Reserved data length + (count and sig length == 4) + the added lengths of the TSPropertyArray
+ // This saves anything after that variable TSPropertyArray data, so as to not squash anything stored there
+ if (strlen($userParameters) > (96 + 4 + $length)) {
+ $this->postBinary = hex2bin(substr($userParameters, (96 + 4 + $length)));
+ }
+ }
+
+ /**
+ * Given the start of TSPropertyArray hex data, and the count for the number
+ * of TSProperty structures in contains, parse and split out the
+ * individual TSProperty structures. Return the full length
+ * of the TSPropertyArray data.
+ *
+ * @param string $tsPropertyArray
+ * @param int $tsPropCount
+ *
+ * @return int The length of the data in the TSPropertyArray
+ */
+ protected function addTSPropData($tsPropertyArray, $tsPropCount)
+ {
+ $length = 0;
+
+ for ($i = 0; $i < $tsPropCount; $i++) {
+ // Prop length = name length + value length + type length + the space for the length data.
+ $propLength = hexdec(substr($tsPropertyArray, $length, 2)) + (hexdec(substr($tsPropertyArray, $length + 2, 2)) * 3) + 6;
+
+ $tsProperty = new TSProperty(hex2bin(substr($tsPropertyArray, $length, $propLength)));
+
+ $this->tsProperty[$tsProperty->getName()] = $tsProperty;
+
+ $length += $propLength;
+ }
+
+ return $length;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Timestamp.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Timestamp.php
new file mode 100644
index 0000000..abd656c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Attributes/Timestamp.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace LdapRecord\Models\Attributes;
+
+use Carbon\Carbon;
+use Carbon\CarbonInterface;
+use DateTime;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Utilities;
+
+class Timestamp
+{
+ /**
+ * The current timestamp type.
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * The available timestamp types.
+ *
+ * @var array
+ */
+ protected $types = [
+ 'ldap',
+ 'windows',
+ 'windows-int',
+ ];
+
+ /**
+ * Constructor.
+ *
+ * @param string $type
+ *
+ * @throws LdapRecordException
+ */
+ public function __construct($type)
+ {
+ $this->setType($type);
+ }
+
+ /**
+ * Set the type of timestamp to convert from / to.
+ *
+ * @param string $type
+ *
+ * @throws LdapRecordException
+ */
+ public function setType($type)
+ {
+ if (! in_array($type, $this->types)) {
+ throw new LdapRecordException("Unrecognized LDAP date type [$type]");
+ }
+
+ $this->type = $type;
+ }
+
+ /**
+ * Converts the value to an LDAP date string.
+ *
+ * @param mixed $value
+ *
+ * @throws LdapRecordException
+ *
+ * @return float|string
+ */
+ public function fromDateTime($value)
+ {
+ $value = is_array($value) ? reset($value) : $value;
+
+ // If the value is being converted to a windows integer format but it
+ // is already in that format, we will simply return the value back.
+ if ($this->type == 'windows-int' && $this->valueIsWindowsIntegerType($value)) {
+ return $value;
+ }
+ // If the value is numeric, we will assume it's a UNIX timestamp.
+ elseif (is_numeric($value)) {
+ $value = Carbon::createFromTimestamp($value);
+ }
+ // If a string is given, we will pass it into a new carbon instance.
+ elseif (is_string($value)) {
+ $value = Carbon::parse($value);
+ }
+ // If a date object is given, we will convert it to a carbon instance.
+ elseif ($value instanceof DateTime) {
+ $value = Carbon::instance($value);
+ }
+
+ switch ($this->type) {
+ case 'ldap':
+ $value = $this->convertDateTimeToLdapTime($value);
+ break;
+ case 'windows':
+ $value = $this->convertDateTimeToWindows($value);
+ break;
+ case 'windows-int':
+ $value = $this->convertDateTimeToWindowsInteger($value);
+ break;
+ default:
+ throw new LdapRecordException("Unrecognized date type [{$this->type}]");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Determine if the value given is in Windows Integer (NTFS Filetime) format.
+ *
+ * @param int|string $value
+ *
+ * @return bool
+ */
+ protected function valueIsWindowsIntegerType($value)
+ {
+ return is_numeric($value) && strlen((string) $value) === 18;
+ }
+
+ /**
+ * Converts the LDAP timestamp value to a Carbon instance.
+ *
+ * @param mixed $value
+ *
+ * @throws LdapRecordException
+ *
+ * @return Carbon|false
+ */
+ public function toDateTime($value)
+ {
+ $value = is_array($value) ? reset($value) : $value;
+
+ if ($value instanceof CarbonInterface || $value instanceof DateTime) {
+ return Carbon::instance($value);
+ }
+
+ switch ($this->type) {
+ case 'ldap':
+ $value = $this->convertLdapTimeToDateTime($value);
+ break;
+ case 'windows':
+ $value = $this->convertWindowsTimeToDateTime($value);
+ break;
+ case 'windows-int':
+ $value = $this->convertWindowsIntegerTimeToDateTime($value);
+ break;
+ default:
+ throw new LdapRecordException("Unrecognized date type [{$this->type}]");
+ }
+
+ return $value instanceof DateTime ? Carbon::instance($value) : $value;
+ }
+
+ /**
+ * Converts standard LDAP timestamps to a date time object.
+ *
+ * @param string $value
+ *
+ * @return DateTime|bool
+ */
+ protected function convertLdapTimeToDateTime($value)
+ {
+ return DateTime::createFromFormat(
+ strpos($value, 'Z') !== false ? 'YmdHis\Z' : 'YmdHisT',
+ $value
+ );
+ }
+
+ /**
+ * Converts date objects to a standard LDAP timestamp.
+ *
+ * @param DateTime $date
+ *
+ * @return string
+ */
+ protected function convertDateTimeToLdapTime(DateTime $date)
+ {
+ return $date->format(
+ $date->getOffset() == 0 ? 'YmdHis\Z' : 'YmdHisO'
+ );
+ }
+
+ /**
+ * Converts standard windows timestamps to a date time object.
+ *
+ * @param string $value
+ *
+ * @return DateTime|bool
+ */
+ protected function convertWindowsTimeToDateTime($value)
+ {
+ return DateTime::createFromFormat(
+ strpos($value, '0Z') !== false ? 'YmdHis.0\Z' : 'YmdHis.0T',
+ $value
+ );
+ }
+
+ /**
+ * Converts date objects to a windows timestamp.
+ *
+ * @param DateTime $date
+ *
+ * @return string
+ */
+ protected function convertDateTimeToWindows(DateTime $date)
+ {
+ return $date->format(
+ $date->getOffset() == 0 ? 'YmdHis.0\Z' : 'YmdHis.0O'
+ );
+ }
+
+ /**
+ * Converts standard windows integer dates to a date time object.
+ *
+ * @param int $value
+ *
+ * @throws \Exception
+ *
+ * @return DateTime|bool
+ */
+ protected function convertWindowsIntegerTimeToDateTime($value)
+ {
+ // ActiveDirectory dates that contain integers may return
+ // "0" when they are not set. We will validate that here.
+ if (! $value) {
+ return false;
+ }
+
+ return (new DateTime())->setTimestamp(
+ Utilities::convertWindowsTimeToUnixTime($value)
+ );
+ }
+
+ /**
+ * Converts date objects to a windows integer timestamp.
+ *
+ * @param DateTime $date
+ *
+ * @return float
+ */
+ protected function convertDateTimeToWindowsInteger(DateTime $date)
+ {
+ return Utilities::convertUnixTimeToWindowsTime($date->getTimestamp());
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/BatchModification.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/BatchModification.php
new file mode 100644
index 0000000..37f0e87
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/BatchModification.php
@@ -0,0 +1,307 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use InvalidArgumentException;
+
+class BatchModification
+{
+ use DetectsResetIntegers;
+
+ /**
+ * The array keys to be used in batch modifications.
+ */
+ const KEY_ATTRIB = 'attrib';
+ const KEY_MODTYPE = 'modtype';
+ const KEY_VALUES = 'values';
+
+ /**
+ * The attribute of the modification.
+ *
+ * @var string|null
+ */
+ protected $attribute;
+
+ /**
+ * The original value of the attribute before modification.
+ *
+ * @var array
+ */
+ protected $original = [];
+
+ /**
+ * The values of the modification.
+ *
+ * @var array
+ */
+ protected $values = [];
+
+ /**
+ * The modtype integer of the batch modification.
+ *
+ * @var int|null
+ */
+ protected $type;
+
+ /**
+ * Constructor.
+ *
+ * @param string|null $attribute
+ * @param string|int|null $type
+ * @param array $values
+ */
+ public function __construct($attribute = null, $type = null, array $values = [])
+ {
+ $this->setAttribute($attribute)
+ ->setType($type)
+ ->setValues($values);
+ }
+
+ /**
+ * Set the original value of the attribute before modification.
+ *
+ * @param array|string $original
+ *
+ * @return $this
+ */
+ public function setOriginal($original = [])
+ {
+ $this->original = $this->normalizeAttributeValues($original);
+
+ return $this;
+ }
+
+ /**
+ * Returns the original value of the attribute before modification.
+ *
+ * @return array
+ */
+ public function getOriginal()
+ {
+ return $this->original;
+ }
+
+ /**
+ * Set the attribute of the modification.
+ *
+ * @param string $attribute
+ *
+ * @return $this
+ */
+ public function setAttribute($attribute)
+ {
+ $this->attribute = $attribute;
+
+ return $this;
+ }
+
+ /**
+ * Returns the attribute of the modification.
+ *
+ * @return string
+ */
+ public function getAttribute()
+ {
+ return $this->attribute;
+ }
+
+ /**
+ * Set the values of the modification.
+ *
+ * @param array $values
+ *
+ * @return $this
+ */
+ public function setValues(array $values = [])
+ {
+ // Null and empty values must also not be added to a batch
+ // modification. Passing null or empty values will result
+ // in an exception when trying to save the modification.
+ $this->values = array_filter($this->normalizeAttributeValues($values), function ($value) {
+ return is_numeric($value) && $this->valueIsResetInteger((int) $value) ?: ! empty($value);
+ });
+
+ return $this;
+ }
+
+ /**
+ * Normalize all of the attribute values.
+ *
+ * @param array|string $values
+ *
+ * @return array
+ */
+ protected function normalizeAttributeValues($values = [])
+ {
+ // We must convert all of the values to strings. Only strings can
+ // be used in batch modifications, otherwise we will we will
+ // receive an LDAP exception while attempting to save.
+ return array_map('strval', (array) $values);
+ }
+
+ /**
+ * Returns the values of the modification.
+ *
+ * @return array
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ /**
+ * Set the type of the modification.
+ *
+ * @param int|null $type
+ *
+ * @return $this
+ */
+ public function setType($type = null)
+ {
+ if (is_null($type)) {
+ return $this;
+ }
+
+ if (! $this->isValidType($type)) {
+ throw new InvalidArgumentException('Given batch modification type is invalid.');
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * Returns the type of the modification.
+ *
+ * @return int
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Determines if the batch modification is valid in its current state.
+ *
+ * @return bool
+ */
+ public function isValid()
+ {
+ return ! is_null($this->get());
+ }
+
+ /**
+ * Builds the type of modification automatically
+ * based on the current and original values.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ switch (true) {
+ case empty($this->original) && empty($this->values):
+ return $this;
+ case ! empty($this->original) && empty($this->values):
+ return $this->setType(LDAP_MODIFY_BATCH_REMOVE_ALL);
+ case empty($this->original) && ! empty($this->values):
+ return $this->setType(LDAP_MODIFY_BATCH_ADD);
+ default:
+ return $this->determineBatchTypeFromOriginal();
+ }
+ }
+
+ /**
+ * Determine the batch modification type from the original values.
+ *
+ * @return $this
+ */
+ protected function determineBatchTypeFromOriginal()
+ {
+ $added = $this->getAddedValues();
+ $removed = $this->getRemovedValues();
+
+ switch (true) {
+ case ! empty($added) && ! empty($removed):
+ return $this->setType(LDAP_MODIFY_BATCH_REPLACE);
+ case ! empty($added):
+ return $this->setValues($added)->setType(LDAP_MODIFY_BATCH_ADD);
+ case ! empty($removed):
+ return $this->setValues($removed)->setType(LDAP_MODIFY_BATCH_REMOVE);
+ default:
+ return $this;
+ }
+ }
+
+ /**
+ * Get the values that were added to the attribute.
+ *
+ * @return array
+ */
+ protected function getAddedValues()
+ {
+ return array_values(
+ array_diff($this->values, $this->original)
+ );
+ }
+
+ /**
+ * Get the values that were removed from the attribute.
+ *
+ * @return array
+ */
+ protected function getRemovedValues()
+ {
+ return array_values(
+ array_diff($this->original, $this->values)
+ );
+ }
+
+ /**
+ * Returns the built batch modification array.
+ *
+ * @return array|null
+ */
+ public function get()
+ {
+ switch ($this->type) {
+ case LDAP_MODIFY_BATCH_REMOVE_ALL:
+ // A values key cannot be provided when
+ // a remove all type is selected.
+ return [
+ static::KEY_ATTRIB => $this->attribute,
+ static::KEY_MODTYPE => $this->type,
+ ];
+ case LDAP_MODIFY_BATCH_REMOVE:
+ // Fallthrough.
+ case LDAP_MODIFY_BATCH_ADD:
+ // Fallthrough.
+ case LDAP_MODIFY_BATCH_REPLACE:
+ return [
+ static::KEY_ATTRIB => $this->attribute,
+ static::KEY_MODTYPE => $this->type,
+ static::KEY_VALUES => $this->values,
+ ];
+ default:
+ // If the modtype isn't recognized, we'll return null.
+ return;
+ }
+ }
+
+ /**
+ * Determines if the given modtype is valid.
+ *
+ * @param int $type
+ *
+ * @return bool
+ */
+ protected function isValidType($type)
+ {
+ return in_array($type, [
+ LDAP_MODIFY_BATCH_REMOVE_ALL,
+ LDAP_MODIFY_BATCH_REMOVE,
+ LDAP_MODIFY_BATCH_REPLACE,
+ LDAP_MODIFY_BATCH_ADD,
+ ]);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Collection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Collection.php
new file mode 100644
index 0000000..850167b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Collection.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use Closure;
+use LdapRecord\Models\Attributes\DistinguishedName;
+use LdapRecord\Query\Collection as QueryCollection;
+use LdapRecord\Support\Arr;
+
+class Collection extends QueryCollection
+{
+ /**
+ * Determine if the collection contains all of the given models, or any models.
+ *
+ * @param mixed $models
+ *
+ * @return bool
+ */
+ public function exists($models = null)
+ {
+ $models = $this->getArrayableModels($models);
+
+ // If any arguments were given and the result set is
+ // empty, we can simply return false here. We can't
+ // verify the existence of models without results.
+ if (func_num_args() > 0 && empty(array_filter($models))) {
+ return false;
+ }
+
+ if (! $models) {
+ return parent::isNotEmpty();
+ }
+
+ foreach ($models as $model) {
+ $exists = parent::contains(function (Model $related) use ($model) {
+ return $this->compareModelWithRelated($model, $related);
+ });
+
+ if (! $exists) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if any of the given models are contained in the collection.
+ *
+ * @param mixed $key
+ * @param mixed $operator
+ * @param mixed $value
+ *
+ * @return bool
+ */
+ public function contains($key, $operator = null, $value = null)
+ {
+ if (func_num_args() > 1 || $key instanceof Closure) {
+ // If we are supplied with more than one argument, or
+ // we were passed a closure, we will utilize the
+ // parents contains method, for compatibility.
+ return parent::contains($key, $operator, $value);
+ }
+
+ foreach ($this->getArrayableModels($key) as $model) {
+ $exists = parent::contains(function (Model $related) use ($model) {
+ return $this->compareModelWithRelated($model, $related);
+ });
+
+ if ($exists) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the provided models as an array.
+ *
+ * @param mixed $models
+ *
+ * @return array
+ */
+ protected function getArrayableModels($models = null)
+ {
+ return $models instanceof QueryCollection
+ ? $models->toArray()
+ : Arr::wrap($models);
+ }
+
+ /**
+ * Compare the related model with the given.
+ *
+ * @param Model|string $model
+ * @param Model $related
+ *
+ * @return bool
+ */
+ protected function compareModelWithRelated($model, $related)
+ {
+ if (is_string($model)) {
+ return $this->isValidDn($model)
+ ? $related->getDn() == $model
+ : $related->getName() == $model;
+ }
+
+ return $related->is($model);
+ }
+
+ /**
+ * Determine if the given string is a valid distinguished name.
+ *
+ * @param string $dn
+ *
+ * @return bool
+ */
+ protected function isValidDn($dn)
+ {
+ return ! empty((new DistinguishedName($dn))->components());
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/CanAuthenticate.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/CanAuthenticate.php
new file mode 100644
index 0000000..f287454
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/CanAuthenticate.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+trait CanAuthenticate
+{
+ /**
+ * Get the name of the unique identifier for the user.
+ *
+ * @return string
+ */
+ public function getAuthIdentifierName()
+ {
+ return $this->guidKey;
+ }
+
+ /**
+ * Get the unique identifier for the user.
+ *
+ * @return mixed
+ */
+ public function getAuthIdentifier()
+ {
+ return $this->getConvertedGuid();
+ }
+
+ /**
+ * Get the password for the user.
+ *
+ * @return string
+ */
+ public function getAuthPassword()
+ {
+ }
+
+ /**
+ * Get the token value for the "remember me" session.
+ *
+ * @return string
+ */
+ public function getRememberToken()
+ {
+ }
+
+ /**
+ * Set the token value for the "remember me" session.
+ *
+ * @param string $value
+ *
+ * @return void
+ */
+ public function setRememberToken($value)
+ {
+ }
+
+ /**
+ * Get the column name for the "remember me" token.
+ *
+ * @return string
+ */
+ public function getRememberTokenName()
+ {
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasAttributes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasAttributes.php
new file mode 100644
index 0000000..20fcec0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasAttributes.php
@@ -0,0 +1,1106 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use Carbon\Carbon;
+use DateTimeInterface;
+use Exception;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Attributes\MbString;
+use LdapRecord\Models\Attributes\Timestamp;
+use LdapRecord\Models\DetectsResetIntegers;
+use LdapRecord\Support\Arr;
+
+trait HasAttributes
+{
+ use DetectsResetIntegers;
+
+ /**
+ * The models original attributes.
+ *
+ * @var array
+ */
+ protected $original = [];
+
+ /**
+ * The models attributes.
+ *
+ * @var array
+ */
+ protected $attributes = [];
+
+ /**
+ * The attributes that should be mutated to dates.
+ *
+ * @var array
+ */
+ protected $dates = [];
+
+ /**
+ * The attributes that should be cast to their native types.
+ *
+ * @var array
+ */
+ protected $casts = [];
+
+ /**
+ * The accessors to append to the model's array form.
+ *
+ * @var array
+ */
+ protected $appends = [];
+
+ /**
+ * The format that dates must be output to for serialization.
+ *
+ * @var string
+ */
+ protected $dateFormat;
+
+ /**
+ * The default attributes that should be mutated to dates.
+ *
+ * @var array
+ */
+ protected $defaultDates = [
+ 'createtimestamp' => 'ldap',
+ 'modifytimestamp' => 'ldap',
+ ];
+
+ /**
+ * The cache of the mutated attributes for each class.
+ *
+ * @var array
+ */
+ protected static $mutatorCache = [];
+
+ /**
+ * Convert the model's attributes to an array.
+ *
+ * @return array
+ */
+ public function attributesToArray()
+ {
+ // Here we will replace our LDAP formatted dates with
+ // properly formatted ones, so dates do not need to
+ // be converted manually after being returned.
+ $attributes = $this->addDateAttributesToArray(
+ $attributes = $this->getArrayableAttributes()
+ );
+
+ $attributes = $this->addMutatedAttributesToArray(
+ $attributes,
+ $this->getMutatedAttributes()
+ );
+
+ // Before we go ahead and encode each value, we'll attempt
+ // converting any necessary attribute values to ensure
+ // they can be encoded, such as GUIDs and SIDs.
+ $attributes = $this->convertAttributesForJson($attributes);
+
+ // Here we will grab all of the appended, calculated attributes to this model
+ // as these attributes are not really in the attributes array, but are run
+ // when we need to array or JSON the model for convenience to the coder.
+ foreach ($this->getArrayableAppends() as $key) {
+ $attributes[$key] = $this->mutateAttributeForArray($key, null);
+ }
+
+ // Now we will go through each attribute to make sure it is
+ // properly encoded. If attributes aren't in UTF-8, we will
+ // encounter JSON encoding errors upon model serialization.
+ return $this->encodeAttributes($attributes);
+ }
+
+ /**
+ * Add the date attributes to the attributes array.
+ *
+ * @param array $attributes
+ *
+ * @return array
+ */
+ protected function addDateAttributesToArray(array $attributes)
+ {
+ foreach ($this->getDates() as $attribute => $type) {
+ if (! isset($attributes[$attribute])) {
+ continue;
+ }
+
+ $date = $this->asDateTime($attributes[$attribute], $type);
+
+ $attributes[$attribute] = $date instanceof Carbon
+ ? Arr::wrap($this->serializeDate($date))
+ : $attributes[$attribute];
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Prepare a date for array / JSON serialization.
+ *
+ * @param DateTimeInterface $date
+ *
+ * @return string
+ */
+ protected function serializeDate(DateTimeInterface $date)
+ {
+ return $date->format($this->getDateFormat());
+ }
+
+ /**
+ * Recursively UTF-8 encode the given attributes.
+ *
+ * @return array
+ */
+ public function encodeAttributes($attributes)
+ {
+ array_walk_recursive($attributes, function (&$value) {
+ $value = $this->encodeValue($value);
+ });
+
+ return $attributes;
+ }
+
+ /**
+ * Encode the given value for proper serialization.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ protected function encodeValue($value)
+ {
+ // If we are able to detect the encoding, we will
+ // encode only the attributes that need to be,
+ // so that we do not double encode values.
+ return MbString::isLoaded() && MbString::isUtf8($value) ? $value : utf8_encode($value);
+ }
+
+ /**
+ * Add the mutated attributes to the attributes array.
+ *
+ * @param array $attributes
+ * @param array $mutatedAttributes
+ *
+ * @return array
+ */
+ protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
+ {
+ foreach ($mutatedAttributes as $key) {
+ // We want to spin through all the mutated attributes for this model and call
+ // the mutator for the attribute. We cache off every mutated attributes so
+ // we don't have to constantly check on attributes that actually change.
+ if (! Arr::exists($attributes, $key)) {
+ continue;
+ }
+
+ // Next, we will call the mutator for this attribute so that we can get these
+ // mutated attribute's actual values. After we finish mutating each of the
+ // attributes we will return this final array of the mutated attributes.
+ $attributes[$key] = $this->mutateAttributeForArray(
+ $key,
+ $attributes[$key]
+ );
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Set the model's original attributes with the model's current attributes.
+ *
+ * @return $this
+ */
+ public function syncOriginal()
+ {
+ $this->original = $this->attributes;
+
+ return $this;
+ }
+
+ /**
+ * Fills the entry with the supplied attributes.
+ *
+ * @param array $attributes
+ *
+ * @return $this
+ */
+ public function fill(array $attributes = [])
+ {
+ foreach ($attributes as $key => $value) {
+ $this->setAttribute($key, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the models attribute by its key.
+ *
+ * @param int|string $key
+ *
+ * @return mixed
+ */
+ public function getAttribute($key)
+ {
+ if (! $key) {
+ return;
+ }
+
+ return $this->getAttributeValue($key);
+ }
+
+ /**
+ * Get an attributes value.
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getAttributeValue($key)
+ {
+ $key = $this->normalizeAttributeKey($key);
+ $value = $this->getAttributeFromArray($key);
+
+ if ($this->hasGetMutator($key)) {
+ return $this->getMutatedAttributeValue($key, $value);
+ }
+
+ if ($this->isDateAttribute($key) && ! is_null($value)) {
+ return $this->asDateTime(Arr::first($value), $this->getDates()[$key]);
+ }
+
+ if ($this->isCastedAttribute($key) && ! is_null($value)) {
+ return $this->castAttribute($key, $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Determine if the given attribute is a date.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function isDateAttribute($key)
+ {
+ return array_key_exists($key, $this->getDates());
+ }
+
+ /**
+ * Get the attributes that should be mutated to dates.
+ *
+ * @return array
+ */
+ public function getDates()
+ {
+ // Since array string keys can be unique depending
+ // on casing differences, we need to normalize the
+ // array key case so they are merged properly.
+ return array_merge(
+ array_change_key_case($this->defaultDates, CASE_LOWER),
+ array_change_key_case($this->dates, CASE_LOWER)
+ );
+ }
+
+ /**
+ * Convert the given date value to an LDAP compatible value.
+ *
+ * @param string $type
+ * @param mixed $value
+ *
+ * @throws LdapRecordException
+ *
+ * @return float|string
+ */
+ public function fromDateTime($type, $value)
+ {
+ return (new Timestamp($type))->fromDateTime($value);
+ }
+
+ /**
+ * Convert the given LDAP date value to a Carbon instance.
+ *
+ * @param mixed $value
+ * @param string $type
+ *
+ * @throws LdapRecordException
+ *
+ * @return Carbon|false
+ */
+ public function asDateTime($value, $type)
+ {
+ return (new Timestamp($type))->toDateTime($value);
+ }
+
+ /**
+ * Determine whether an attribute should be cast to a native type.
+ *
+ * @param string $key
+ * @param array|string|null $types
+ *
+ * @return bool
+ */
+ public function hasCast($key, $types = null)
+ {
+ if (array_key_exists($key, $this->getCasts())) {
+ return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the attributes that should be cast to their native types.
+ *
+ * @return array
+ */
+ protected function getCasts()
+ {
+ return array_change_key_case($this->casts, CASE_LOWER);
+ }
+
+ /**
+ * Determine whether a value is JSON castable for inbound manipulation.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ protected function isJsonCastable($key)
+ {
+ return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
+ }
+
+ /**
+ * Get the type of cast for a model attribute.
+ *
+ * @param string $key
+ *
+ * @return string
+ */
+ protected function getCastType($key)
+ {
+ if ($this->isDecimalCast($this->getCasts()[$key])) {
+ return 'decimal';
+ }
+
+ if ($this->isDateTimeCast($this->getCasts()[$key])) {
+ return 'datetime';
+ }
+
+ return trim(strtolower($this->getCasts()[$key]));
+ }
+
+ /**
+ * Determine if the cast is a decimal.
+ *
+ * @param string $cast
+ *
+ * @return bool
+ */
+ protected function isDecimalCast($cast)
+ {
+ return strncmp($cast, 'decimal:', 8) === 0;
+ }
+
+ /**
+ * Determine if the cast is a datetime.
+ *
+ * @param string $cast
+ *
+ * @return bool
+ */
+ protected function isDateTimeCast($cast)
+ {
+ return strncmp($cast, 'datetime:', 8) === 0;
+ }
+
+ /**
+ * Determine if the given attribute must be casted.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ protected function isCastedAttribute($key)
+ {
+ return array_key_exists($key, array_change_key_case($this->casts, CASE_LOWER));
+ }
+
+ /**
+ * Cast an attribute to a native PHP type.
+ *
+ * @param string $key
+ * @param array|null $value
+ *
+ * @return mixed
+ */
+ protected function castAttribute($key, $value)
+ {
+ $value = $this->castRequiresArrayValue($key) ? $value : Arr::first($value);
+
+ if (is_null($value)) {
+ return $value;
+ }
+
+ switch ($this->getCastType($key)) {
+ case 'int':
+ case 'integer':
+ return (int) $value;
+ case 'real':
+ case 'float':
+ case 'double':
+ return $this->fromFloat($value);
+ case 'decimal':
+ return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
+ case 'string':
+ return (string) $value;
+ case 'bool':
+ case 'boolean':
+ return $this->asBoolean($value);
+ case 'object':
+ return $this->fromJson($value, $asObject = true);
+ case 'array':
+ case 'json':
+ return $this->fromJson($value);
+ case 'collection':
+ return $this->newCollection($value);
+ case 'datetime':
+ return $this->asDateTime($value, explode(':', $this->getCasts()[$key], 2)[1]);
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Determine if the cast type requires the first attribute value.
+ *
+ * @return bool
+ */
+ protected function castRequiresArrayValue($key)
+ {
+ return in_array($this->getCastType($key), ['collection']);
+ }
+
+ /**
+ * Cast the given attribute to JSON.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return string
+ */
+ protected function castAttributeAsJson($key, $value)
+ {
+ $value = $this->asJson($value);
+
+ if ($value === false) {
+ $class = get_class($this);
+ $message = json_last_error_msg();
+
+ throw new Exception("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}.");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the model to its JSON representation.
+ *
+ * @return string
+ */
+ public function toJson()
+ {
+ return json_encode($this);
+ }
+
+ /**
+ * Encode the given value as JSON.
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ protected function asJson($value)
+ {
+ return json_encode($value);
+ }
+
+ /**
+ * Decode the given JSON back into an array or object.
+ *
+ * @param string $value
+ * @param bool $asObject
+ *
+ * @return mixed
+ */
+ public function fromJson($value, $asObject = false)
+ {
+ return json_decode($value, ! $asObject);
+ }
+
+ /**
+ * Decode the given float.
+ *
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ public function fromFloat($value)
+ {
+ switch ((string) $value) {
+ case 'Infinity':
+ return INF;
+ case '-Infinity':
+ return -INF;
+ case 'NaN':
+ return NAN;
+ default:
+ return (float) $value;
+ }
+ }
+
+ /**
+ * Cast the value to a boolean.
+ *
+ * @param mixed $value
+ *
+ * @return bool
+ */
+ protected function asBoolean($value)
+ {
+ $map = ['true' => true, 'false' => false];
+
+ return $map[strtolower($value)] ?? (bool) $value;
+ }
+
+ /**
+ * Cast a decimal value as a string.
+ *
+ * @param float $value
+ * @param int $decimals
+ *
+ * @return string
+ */
+ protected function asDecimal($value, $decimals)
+ {
+ return number_format($value, $decimals, '.', '');
+ }
+
+ /**
+ * Get an attribute array of all arrayable attributes.
+ *
+ * @return array
+ */
+ protected function getArrayableAttributes()
+ {
+ return $this->getArrayableItems($this->attributes);
+ }
+
+ /**
+ * Get an attribute array of all arrayable values.
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ protected function getArrayableItems(array $values)
+ {
+ if (count($visible = $this->getVisible()) > 0) {
+ $values = array_intersect_key($values, array_flip($visible));
+ }
+
+ if (count($hidden = $this->getHidden()) > 0) {
+ $values = array_diff_key($values, array_flip($hidden));
+ }
+
+ return $values;
+ }
+
+ /**
+ * Get all of the appendable values that are arrayable.
+ *
+ * @return array
+ */
+ protected function getArrayableAppends()
+ {
+ if (empty($this->appends)) {
+ return [];
+ }
+
+ return $this->getArrayableItems(
+ array_combine($this->appends, $this->appends)
+ );
+ }
+
+ /**
+ * Get the format for date serialization.
+ *
+ * @return string
+ */
+ public function getDateFormat()
+ {
+ return $this->dateFormat ?: DateTimeInterface::ISO8601;
+ }
+
+ /**
+ * Set the date format used by the model for serialization.
+ *
+ * @param string $format
+ *
+ * @return $this
+ */
+ public function setDateFormat($format)
+ {
+ $this->dateFormat = $format;
+
+ return $this;
+ }
+
+ /**
+ * Get an attribute from the $attributes array.
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ protected function getAttributeFromArray($key)
+ {
+ return $this->getNormalizedAttributes()[$key] ?? null;
+ }
+
+ /**
+ * Get the attributes with their keys normalized.
+ *
+ * @return array
+ */
+ protected function getNormalizedAttributes()
+ {
+ return array_change_key_case($this->attributes, CASE_LOWER);
+ }
+
+ /**
+ * Returns the first attribute by the specified key.
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getFirstAttribute($key)
+ {
+ return Arr::first(
+ Arr::wrap($this->getAttribute($key))
+ );
+ }
+
+ /**
+ * Returns all of the models attributes.
+ *
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Set an attribute value by the specified key and sub-key.
+ *
+ * @param mixed $key
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setAttribute($key, $value)
+ {
+ $key = $this->normalizeAttributeKey($key);
+
+ if ($this->hasSetMutator($key)) {
+ return $this->setMutatedAttributeValue($key, $value);
+ } elseif (
+ $value &&
+ $this->isDateAttribute($key) &&
+ ! $this->valueIsResetInteger($value)
+ ) {
+ $value = $this->fromDateTime($this->getDates()[$key], $value);
+ }
+
+ if ($this->isJsonCastable($key) && ! is_null($value)) {
+ $value = $this->castAttributeAsJson($key, $value);
+ }
+
+ $this->attributes[$key] = Arr::wrap($value);
+
+ return $this;
+ }
+
+ /**
+ * Set the models first attribute value.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setFirstAttribute($key, $value)
+ {
+ return $this->setAttribute($key, Arr::wrap($value));
+ }
+
+ /**
+ * Add a unique value to the given attribute.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function addAttributeValue($key, $value)
+ {
+ return $this->setAttribute($key, array_unique(
+ array_merge(
+ Arr::wrap($this->getAttribute($key)),
+ Arr::wrap($value)
+ )
+ ));
+ }
+
+ /**
+ * Determine if a get mutator exists for an attribute.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function hasGetMutator($key)
+ {
+ return method_exists($this, 'get'.$this->getMutatorMethodName($key).'Attribute');
+ }
+
+ /**
+ * Determine if a set mutator exists for an attribute.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function hasSetMutator($key)
+ {
+ return method_exists($this, 'set'.$this->getMutatorMethodName($key).'Attribute');
+ }
+
+ /**
+ * Set the value of an attribute using its mutator.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected function setMutatedAttributeValue($key, $value)
+ {
+ return $this->{'set'.$this->getMutatorMethodName($key).'Attribute'}($value);
+ }
+
+ /**
+ * Get the value of an attribute using its mutator.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected function getMutatedAttributeValue($key, $value)
+ {
+ return $this->{'get'.$this->getMutatorMethodName($key).'Attribute'}($value);
+ }
+
+ /**
+ * Get the mutator attribute method name.
+ *
+ * Hyphenated attributes will use pascal cased methods.
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ protected function getMutatorMethodName($key)
+ {
+ $key = ucwords(str_replace('-', ' ', $key));
+
+ return str_replace(' ', '', $key);
+ }
+
+ /**
+ * Get the value of an attribute using its mutator for array conversion.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return array
+ */
+ protected function mutateAttributeForArray($key, $value)
+ {
+ return Arr::wrap(
+ $this->getMutatedAttributeValue($key, $value)
+ );
+ }
+
+ /**
+ * Set the attributes property.
+ *
+ * Used when constructing an existing LDAP record.
+ *
+ * @param array $attributes
+ *
+ * @return $this
+ */
+ public function setRawAttributes(array $attributes = [])
+ {
+ // We will filter out those annoying 'count' keys
+ // returned with LDAP results and lowercase all
+ // root array keys to prevent any casing issues.
+ $raw = array_change_key_case($this->filterRawAttributes($attributes), CASE_LOWER);
+
+ // Before setting the models attributes, we will filter
+ // out the attributes that contain an integer key. LDAP
+ // search results will contain integer keys that have
+ // attribute names as values. We don't need these.
+ $this->attributes = array_filter($raw, function ($key) {
+ return ! is_int($key);
+ }, ARRAY_FILTER_USE_KEY);
+
+ // LDAP search results will contain the distinguished
+ // name inside of the `dn` key. We will retrieve this,
+ // and then set it on the model for accessibility.
+ if (Arr::exists($attributes, 'dn')) {
+ $this->dn = Arr::accessible($attributes['dn'])
+ ? Arr::first($attributes['dn'])
+ : $attributes['dn'];
+ }
+
+ $this->syncOriginal();
+
+ // Here we will set the exists attribute to true,
+ // since raw attributes are only set in the case
+ // of attributes being loaded by query results.
+ $this->exists = true;
+
+ return $this;
+ }
+
+ /**
+ * Filters the count key recursively from raw LDAP attributes.
+ *
+ * @param array $attributes
+ * @param array $keys
+ *
+ * @return array
+ */
+ public function filterRawAttributes(array $attributes = [], array $keys = ['count', 'dn'])
+ {
+ foreach ($keys as $key) {
+ unset($attributes[$key]);
+ }
+
+ foreach ($attributes as $key => $value) {
+ $attributes[$key] = is_array($value)
+ ? $this->filterRawAttributes($value, $keys)
+ : $value;
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Determine if the model has the given attribute.
+ *
+ * @param int|string $key
+ *
+ * @return bool
+ */
+ public function hasAttribute($key)
+ {
+ return [] !== ($this->attributes[$this->normalizeAttributeKey($key)] ?? []);
+ }
+
+ /**
+ * Returns the number of attributes.
+ *
+ * @return int
+ */
+ public function countAttributes()
+ {
+ return count($this->getAttributes());
+ }
+
+ /**
+ * Returns the models original attributes.
+ *
+ * @return array
+ */
+ public function getOriginal()
+ {
+ return $this->original;
+ }
+
+ /**
+ * Get the attributes that have been changed since last sync.
+ *
+ * @return array
+ */
+ public function getDirty()
+ {
+ $dirty = [];
+
+ foreach ($this->attributes as $key => $value) {
+ if ($this->isDirty($key)) {
+ // We need to reset the array using array_values due to
+ // LDAP requiring consecutive indices (0, 1, 2 etc.).
+ // We would receive an exception otherwise.
+ $dirty[$key] = array_values($value);
+ }
+ }
+
+ return $dirty;
+ }
+
+ /**
+ * Determine if the given attribute is dirty.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function isDirty($key)
+ {
+ return ! $this->originalIsEquivalent($key);
+ }
+
+ /**
+ * Get the accessors being appended to the models array form.
+ *
+ * @return array
+ */
+ public function getAppends()
+ {
+ return $this->appends;
+ }
+
+ /**
+ * Set the accessors to append to model arrays.
+ *
+ * @param array $appends
+ *
+ * @return $this
+ */
+ public function setAppends(array $appends)
+ {
+ $this->appends = $appends;
+
+ return $this;
+ }
+
+ /**
+ * Return whether the accessor attribute has been appended.
+ *
+ * @param string $attribute
+ *
+ * @return bool
+ */
+ public function hasAppended($attribute)
+ {
+ return in_array($attribute, $this->appends);
+ }
+
+ /**
+ * Returns a normalized attribute key.
+ *
+ * @param string $key
+ *
+ * @return string
+ */
+ public function normalizeAttributeKey($key)
+ {
+ // Since LDAP supports hyphens in attribute names,
+ // we'll convert attributes being retrieved by
+ // underscores into hyphens for convenience.
+ return strtolower(
+ str_replace('_', '-', $key)
+ );
+ }
+
+ /**
+ * Determine if the new and old values for a given key are equivalent.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ protected function originalIsEquivalent($key)
+ {
+ if (! array_key_exists($key, $this->original)) {
+ return false;
+ }
+
+ $current = $this->attributes[$key];
+ $original = $this->original[$key];
+
+ if ($current === $original) {
+ return true;
+ }
+
+ return is_numeric($current) &&
+ is_numeric($original) &&
+ strcmp((string) $current, (string) $original) === 0;
+ }
+
+ /**
+ * Get the mutated attributes for a given instance.
+ *
+ * @return array
+ */
+ public function getMutatedAttributes()
+ {
+ $class = static::class;
+
+ if (! isset(static::$mutatorCache[$class])) {
+ static::cacheMutatedAttributes($class);
+ }
+
+ return static::$mutatorCache[$class];
+ }
+
+ /**
+ * Extract and cache all the mutated attributes of a class.
+ *
+ * @param string $class
+ *
+ * @return void
+ */
+ public static function cacheMutatedAttributes($class)
+ {
+ static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->reject(function ($match) {
+ return $match === 'First';
+ })->map(function ($match) {
+ return lcfirst($match);
+ })->all();
+ }
+
+ /**
+ * Get all of the attribute mutator methods.
+ *
+ * @param mixed $class
+ *
+ * @return array
+ */
+ protected static function getMutatorMethods($class)
+ {
+ preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
+
+ return $matches[1];
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasEvents.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasEvents.php
new file mode 100644
index 0000000..1bc76d0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasEvents.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use Closure;
+use LdapRecord\Models\Events\Event;
+
+trait HasEvents
+{
+ /**
+ * Fires the specified model event.
+ *
+ * @param Event $event
+ *
+ * @return mixed
+ */
+ protected function fireModelEvent(Event $event)
+ {
+ return static::getConnectionContainer()->getEventDispatcher()->fire($event);
+ }
+
+ /**
+ * Listens to a model event.
+ *
+ * @param string $event
+ * @param Closure $listener
+ *
+ * @return mixed
+ */
+ protected function listenForModelEvent($event, Closure $listener)
+ {
+ return static::getConnectionContainer()->getEventDispatcher()->listen($event, $listener);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasGlobalScopes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasGlobalScopes.php
new file mode 100644
index 0000000..c14abad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasGlobalScopes.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use Closure;
+use InvalidArgumentException;
+use LdapRecord\Models\Scope;
+
+trait HasGlobalScopes
+{
+ /**
+ * Register a new global scope on the model.
+ *
+ * @param Scope|Closure|string $scope
+ * @param Closure|null $implementation
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return mixed
+ */
+ public static function addGlobalScope($scope, Closure $implementation = null)
+ {
+ if (is_string($scope) && ! is_null($implementation)) {
+ return static::$globalScopes[static::class][$scope] = $implementation;
+ } elseif ($scope instanceof Closure) {
+ return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope;
+ } elseif ($scope instanceof Scope) {
+ return static::$globalScopes[static::class][get_class($scope)] = $scope;
+ }
+
+ throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.');
+ }
+
+ /**
+ * Determine if a model has a global scope.
+ *
+ * @param Scope|string $scope
+ *
+ * @return bool
+ */
+ public static function hasGlobalScope($scope)
+ {
+ return ! is_null(static::getGlobalScope($scope));
+ }
+
+ /**
+ * Get a global scope registered with the model.
+ *
+ * @param Scope|string $scope
+ *
+ * @return Scope|Closure|null
+ */
+ public static function getGlobalScope($scope)
+ {
+ if (array_key_exists(static::class, static::$globalScopes)) {
+ $scopeName = is_string($scope) ? $scope : get_class($scope);
+
+ return array_key_exists($scopeName, static::$globalScopes[static::class])
+ ? static::$globalScopes[static::class][$scopeName]
+ : null;
+ }
+ }
+
+ /**
+ * Get the global scopes for this class instance.
+ *
+ * @return array
+ */
+ public function getGlobalScopes()
+ {
+ return array_key_exists(static::class, static::$globalScopes)
+ ? static::$globalScopes[static::class]
+ : [];
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasPassword.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasPassword.php
new file mode 100644
index 0000000..9822456
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasPassword.php
@@ -0,0 +1,251 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use LdapRecord\ConnectionException;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Attributes\Password;
+
+trait HasPassword
+{
+ /**
+ * Set the password on the user.
+ *
+ * @param string|array $password
+ *
+ * @throws ConnectionException
+ */
+ public function setPasswordAttribute($password)
+ {
+ $this->validateSecureConnection();
+
+ // Here we will attempt to determine the password hash method in use
+ // by parsing the users hashed password (if it as available). If a
+ // method is determined, we will override the default here.
+ if (! ($method = $this->determinePasswordHashMethod())) {
+ $method = $this->getPasswordHashMethod();
+ }
+
+ // If the password given is an array, we can assume we
+ // are changing the password for the current user.
+ if (is_array($password)) {
+ $this->setChangedPassword(
+ $this->getHashedPassword($method, $password[0], $this->getPasswordSalt($method)),
+ $this->getHashedPassword($method, $password[1]),
+ $this->getPasswordAttributeName()
+ );
+ }
+ // Otherwise, we will assume the password is being
+ // reset, overwriting the one currently in place.
+ else {
+ $this->setPassword(
+ $this->getHashedPassword($method, $password),
+ $this->getPasswordAttributeName()
+ );
+ }
+ }
+
+ /**
+ * Alias for setting the password on the user.
+ *
+ * @param string|array $password
+ *
+ * @throws ConnectionException
+ */
+ public function setUnicodepwdAttribute($password)
+ {
+ $this->setPasswordAttribute($password);
+ }
+
+ /**
+ * An accessor for retrieving the user's hashed password value.
+ *
+ * @return string|null
+ */
+ public function getPasswordAttribute()
+ {
+ return $this->getAttribute($this->getPasswordAttributeName())[0] ?? null;
+ }
+
+ /**
+ * Get the name of the attribute that contains the user's password.
+ *
+ * @return string
+ */
+ public function getPasswordAttributeName()
+ {
+ if (property_exists($this, 'passwordAttribute')) {
+ return $this->passwordAttribute;
+ }
+
+ if (method_exists($this, 'passwordAttribute')) {
+ return $this->passwordAttribute();
+ }
+
+ return 'unicodepwd';
+ }
+
+ /**
+ * Get the name of the method to use for hashing the user's password.
+ *
+ * @return string
+ */
+ public function getPasswordHashMethod()
+ {
+ if (property_exists($this, 'passwordHashMethod')) {
+ return $this->passwordHashMethod;
+ }
+
+ if (method_exists($this, 'passwordHashMethod')) {
+ return $this->passwordHashMethod();
+ }
+
+ return 'encode';
+ }
+
+ /**
+ * Set the changed password.
+ *
+ * @param string $oldPassword
+ * @param string $newPassword
+ * @param string $attribute
+ *
+ * @return void
+ */
+ protected function setChangedPassword($oldPassword, $newPassword, $attribute)
+ {
+ // Create batch modification for removing the old password.
+ $this->addModification(
+ $this->newBatchModification(
+ $attribute,
+ LDAP_MODIFY_BATCH_REMOVE,
+ [$oldPassword]
+ )
+ );
+
+ // Create batch modification for adding the new password.
+ $this->addModification(
+ $this->newBatchModification(
+ $attribute,
+ LDAP_MODIFY_BATCH_ADD,
+ [$newPassword]
+ )
+ );
+ }
+
+ /**
+ * Set the password on the model.
+ *
+ * @param string $password
+ * @param string $attribute
+ *
+ * @return void
+ */
+ protected function setPassword($password, $attribute)
+ {
+ $this->addModification(
+ $this->newBatchModification(
+ $attribute,
+ LDAP_MODIFY_BATCH_REPLACE,
+ [$password]
+ )
+ );
+ }
+
+ /**
+ * Encode / hash the given password.
+ *
+ * @param string $method
+ * @param string $password
+ * @param string $salt
+ *
+ * @throws LdapRecordException
+ *
+ * @return string
+ */
+ protected function getHashedPassword($method, $password, $salt = null)
+ {
+ if (! method_exists(Password::class, $method)) {
+ throw new LdapRecordException("Password hashing method [{$method}] does not exist.");
+ }
+
+ if (Password::hashMethodRequiresSalt($method)) {
+ return Password::{$method}($password, $salt);
+ }
+
+ return Password::{$method}($password);
+ }
+
+ /**
+ * Validates that the current LDAP connection is secure.
+ *
+ * @throws ConnectionException
+ *
+ * @return void
+ */
+ protected function validateSecureConnection()
+ {
+ $connection = $this->getConnection();
+
+ if ($connection->isConnected()) {
+ $secure = $connection->getLdapConnection()->canChangePasswords();
+ } else {
+ $secure = $connection->getConfiguration()->get('use_ssl') || $connection->getConfiguration()->get('use_tls');
+ }
+
+ if (! $secure) {
+ throw new ConnectionException(
+ 'You must be connected to your LDAP server with TLS or SSL to perform this operation.'
+ );
+ }
+ }
+
+ /**
+ * Attempt to retrieve the password's salt.
+ *
+ * @param string $method
+ *
+ * @return string|null
+ */
+ public function getPasswordSalt($method)
+ {
+ if (! Password::hashMethodRequiresSalt($method)) {
+ return;
+ }
+
+ return Password::getSalt($this->password);
+ }
+
+ /**
+ * Determine the password hash method to use from the users current password.
+ *
+ * @return string|void
+ */
+ public function determinePasswordHashMethod()
+ {
+ if (! $password = $this->password) {
+ return;
+ }
+
+ if (! $method = Password::getHashMethod($password)) {
+ return;
+ }
+
+ [,$algo] = array_pad(
+ Password::getHashMethodAndAlgo($password) ?? [],
+ $length = 2,
+ $value = null
+ );
+
+ switch ($algo) {
+ case Password::CRYPT_SALT_TYPE_MD5:
+ return 'md5'.$method;
+ case Password::CRYPT_SALT_TYPE_SHA256:
+ return 'sha256'.$method;
+ case Password::CRYPT_SALT_TYPE_SHA512:
+ return 'sha512'.$method;
+ default:
+ return $method;
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasRelationships.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasRelationships.php
new file mode 100644
index 0000000..a8a5cac
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasRelationships.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+use LdapRecord\Models\Relations\HasMany;
+use LdapRecord\Models\Relations\HasManyIn;
+use LdapRecord\Models\Relations\HasOne;
+use LdapRecord\Support\Arr;
+
+trait HasRelationships
+{
+ /**
+ * Returns a new has one relationship.
+ *
+ * @param mixed $related
+ * @param string $relationKey
+ * @param string $foreignKey
+ *
+ * @return HasOne
+ */
+ public function hasOne($related, $relationKey, $foreignKey = 'dn')
+ {
+ return new HasOne($this->newQuery(), $this, $related, $relationKey, $foreignKey);
+ }
+
+ /**
+ * Returns a new has many relationship.
+ *
+ * @param mixed $related
+ * @param string $relationKey
+ * @param string $foreignKey
+ *
+ * @return HasMany
+ */
+ public function hasMany($related, $relationKey, $foreignKey = 'dn')
+ {
+ return new HasMany($this->newQuery(), $this, $related, $relationKey, $foreignKey, $this->guessRelationshipName());
+ }
+
+ /**
+ * Returns a new has many in relationship.
+ *
+ * @param mixed $related
+ * @param string $relationKey
+ * @param string $foreignKey
+ *
+ * @return HasManyIn
+ */
+ public function hasManyIn($related, $relationKey, $foreignKey = 'dn')
+ {
+ return new HasManyIn($this->newQuery(), $this, $related, $relationKey, $foreignKey, $this->guessRelationshipName());
+ }
+
+ /**
+ * Get the relationships name.
+ *
+ * @return string|null
+ */
+ protected function guessRelationshipName()
+ {
+ return Arr::last(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3))['function'];
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasScopes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasScopes.php
new file mode 100644
index 0000000..6c97cf9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HasScopes.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+trait HasScopes
+{
+ /**
+ * Begin querying the direct descendants of the model.
+ *
+ * @return \LdapRecord\Query\Model\Builder
+ */
+ public function descendants()
+ {
+ return $this->in($this->getDn())->listing();
+ }
+
+ /**
+ * Begin querying the direct ancestors of the model.
+ *
+ * @return \LdapRecord\Query\Model\Builder
+ */
+ public function ancestors()
+ {
+ $parent = $this->getParentDn($this->getDn());
+
+ return $this->in($this->getParentDn($parent))->listing();
+ }
+
+ /**
+ * Begin querying the direct siblings of the model.
+ *
+ * @return \LdapRecord\Query\Model\Builder
+ */
+ public function siblings()
+ {
+ return $this->in($this->getParentDn($this->getDn()))->listing();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HidesAttributes.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HidesAttributes.php
new file mode 100644
index 0000000..9cc2100
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Concerns/HidesAttributes.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace LdapRecord\Models\Concerns;
+
+/**
+ * @author Taylor Otwell
+ *
+ * @see https://laravel.com
+ */
+trait HidesAttributes
+{
+ /**
+ * The attributes that should be hidden for serialization.
+ *
+ * @var array
+ */
+ protected $hidden = [];
+
+ /**
+ * The attributes that should be visible in serialization.
+ *
+ * @var array
+ */
+ protected $visible = [];
+
+ /**
+ * Get the hidden attributes for the model.
+ *
+ * @return array
+ */
+ public function getHidden()
+ {
+ return array_map(function ($key) {
+ return $this->normalizeAttributeKey($key);
+ }, $this->hidden);
+ }
+
+ /**
+ * Set the hidden attributes for the model.
+ *
+ * @param array $hidden
+ *
+ * @return $this
+ */
+ public function setHidden(array $hidden)
+ {
+ $this->hidden = $hidden;
+
+ return $this;
+ }
+
+ /**
+ * Add hidden attributes for the model.
+ *
+ * @param array|string|null $attributes
+ *
+ * @return void
+ */
+ public function addHidden($attributes = null)
+ {
+ $this->hidden = array_merge(
+ $this->hidden,
+ is_array($attributes) ? $attributes : func_get_args()
+ );
+ }
+
+ /**
+ * Get the visible attributes for the model.
+ *
+ * @return array
+ */
+ public function getVisible()
+ {
+ return array_map(function ($key) {
+ return $this->normalizeAttributeKey($key);
+ }, $this->visible);
+ }
+
+ /**
+ * Set the visible attributes for the model.
+ *
+ * @param array $visible
+ *
+ * @return $this
+ */
+ public function setVisible(array $visible)
+ {
+ $this->visible = $visible;
+
+ return $this;
+ }
+
+ /**
+ * Add visible attributes for the model.
+ *
+ * @param array|string|null $attributes
+ *
+ * @return void
+ */
+ public function addVisible($attributes = null)
+ {
+ $this->visible = array_merge(
+ $this->visible,
+ is_array($attributes) ? $attributes : func_get_args()
+ );
+ }
+
+ /**
+ * Make the given, typically hidden, attributes visible.
+ *
+ * @param array|string $attributes
+ *
+ * @return $this
+ */
+ public function makeVisible($attributes)
+ {
+ $this->hidden = array_diff($this->hidden, (array) $attributes);
+
+ if (! empty($this->visible)) {
+ $this->addVisible($attributes);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Make the given, typically visible, attributes hidden.
+ *
+ * @param array|string $attributes
+ *
+ * @return $this
+ */
+ public function makeHidden($attributes)
+ {
+ $attributes = (array) $attributes;
+
+ $this->visible = array_diff($this->visible, $attributes);
+
+ $this->hidden = array_unique(array_merge($this->hidden, $attributes));
+
+ return $this;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DetectsResetIntegers.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DetectsResetIntegers.php
new file mode 100644
index 0000000..8712ef7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DetectsResetIntegers.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace LdapRecord\Models;
+
+trait DetectsResetIntegers
+{
+ /**
+ * Determine if the given value is an LDAP reset integer.
+ *
+ * The integer values '0' and '-1' can be used on certain
+ * LDAP attributes to instruct the server to reset the
+ * value to an 'unset' or 'cleared' state.
+ *
+ * @param mixed $value
+ *
+ * @return bool
+ */
+ protected function valueIsResetInteger($value)
+ {
+ return in_array($value, [0, -1], $strict = true);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Entry.php
new file mode 100644
index 0000000..1bf8325
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Entry.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace LdapRecord\Models\DirectoryServer;
+
+use LdapRecord\Models\Model;
+
+class Entry extends Model
+{
+ /**
+ * The attribute key that contains the models object GUID.
+ *
+ * @var string
+ */
+ protected $guidKey = 'gidNumber';
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Group.php
new file mode 100644
index 0000000..49a6e0a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/Group.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace LdapRecord\Models\DirectoryServer;
+
+class Group extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'groupOfUniqueNames',
+ 'posixGroup',
+ ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/User.php
new file mode 100644
index 0000000..430588b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/DirectoryServer/User.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace LdapRecord\Models\DirectoryServer;
+
+class User extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'nsPerson',
+ 'nsAccount',
+ 'nsOrgPerson',
+ 'posixAccount',
+ ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Entry.php
new file mode 100644
index 0000000..dcfda57
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Entry.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models;
+
+class Entry extends Model
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Created.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Created.php
new file mode 100644
index 0000000..c101235
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Created.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Created extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Creating.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Creating.php
new file mode 100644
index 0000000..c4e6ad7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Creating.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Creating extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleted.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleted.php
new file mode 100644
index 0000000..7852659
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleted.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Deleted extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleting.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleting.php
new file mode 100644
index 0000000..9a0810d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Deleting.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Deleting extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Event.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Event.php
new file mode 100644
index 0000000..20de0b7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Event.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+use LdapRecord\Models\Model;
+
+abstract class Event
+{
+ /**
+ * The model that the event is being triggered on.
+ *
+ * @var Model
+ */
+ protected $model;
+
+ /**
+ * Constructor.
+ *
+ * @param Model $model
+ */
+ public function __construct(Model $model)
+ {
+ $this->model = $model;
+ }
+
+ /**
+ * Returns the model that generated the event.
+ *
+ * @return Model
+ */
+ public function getModel()
+ {
+ return $this->model;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renamed.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renamed.php
new file mode 100644
index 0000000..0f02b6d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renamed.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Renamed extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renaming.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renaming.php
new file mode 100644
index 0000000..83427ca
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Renaming.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+use LdapRecord\Models\Model;
+
+class Renaming extends Event
+{
+ /**
+ * The models RDN.
+ *
+ * @var string
+ */
+ protected $rdn;
+
+ /**
+ * The models new parent DN.
+ *
+ * @var string
+ */
+ protected $newParentDn;
+
+ /**
+ * Constructor.
+ *
+ * @param Model $model
+ * @param string $rdn
+ * @param string $newParentDn
+ */
+ public function __construct(Model $model, $rdn, $newParentDn)
+ {
+ parent::__construct($model);
+
+ $this->rdn = $rdn;
+ $this->newParentDn = $newParentDn;
+ }
+
+ /**
+ * Get the models RDN.
+ *
+ * @return string
+ */
+ public function getRdn()
+ {
+ return $this->rdn;
+ }
+
+ /**
+ * Get the models parent DN.
+ *
+ * @return string
+ */
+ public function getNewParentDn()
+ {
+ return $this->newParentDn;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saved.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saved.php
new file mode 100644
index 0000000..cf9c5ad
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saved.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Saved extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saving.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saving.php
new file mode 100644
index 0000000..0c99403
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Saving.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Saving extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updated.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updated.php
new file mode 100644
index 0000000..b0dd611
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updated.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Updated extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updating.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updating.php
new file mode 100644
index 0000000..20ae60c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Events/Updating.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Events;
+
+class Updating extends Event
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Entry.php
new file mode 100644
index 0000000..7fdda9c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Entry.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA;
+
+use LdapRecord\Connection;
+use LdapRecord\Models\Entry as BaseEntry;
+use LdapRecord\Models\FreeIPA\Scopes\AddEntryUuidToSelects;
+use LdapRecord\Models\Types\FreeIPA;
+use LdapRecord\Query\Model\FreeIpaBuilder;
+
+/** @mixin FreeIpaBuilder */
+class Entry extends BaseEntry implements FreeIPA
+{
+ /**
+ * The attribute key that contains the models object GUID.
+ *
+ * @var string
+ */
+ protected $guidKey = 'ipauniqueid';
+
+ /**
+ * The default attributes that should be mutated to dates.
+ *
+ * @var array
+ */
+ protected $defaultDates = [
+ 'krblastpwdchange' => 'ldap',
+ 'krbpasswordexpiration' => 'ldap',
+ ];
+
+ /**
+ * @inheritdoc
+ */
+ protected static function boot()
+ {
+ parent::boot();
+
+ // Here we'll add a global scope to all FreeIPA models to ensure the
+ // Entry UUID is always selected on each query. This attribute is
+ // virtual, so it must be manually selected to be included.
+ static::addGlobalScope(new AddEntryUuidToSelects());
+ }
+
+ /**
+ * Create a new query builder.
+ *
+ * @param Connection $connection
+ *
+ * @return FreeIpaBuilder
+ */
+ public function newQueryBuilder(Connection $connection)
+ {
+ return new FreeIpaBuilder($connection);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Group.php
new file mode 100644
index 0000000..10fd934
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Group.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA;
+
+class Group extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'groupofnames',
+ 'nestedgroup',
+ 'ipausergroup',
+ 'posixgroup',
+ ];
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the current group is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(self::class, 'member');
+ }
+
+ /**
+ * Retrieve the members of the group.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function members()
+ {
+ return $this->hasMany(User::class, 'memberof')->using($this, 'member');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Scopes/AddEntryUuidToSelects.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Scopes/AddEntryUuidToSelects.php
new file mode 100644
index 0000000..039c05e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/Scopes/AddEntryUuidToSelects.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class AddEntryUuidToSelects implements Scope
+{
+ /**
+ * Add the entry UUID to the selected attributes.
+ *
+ * @param Builder $query
+ * @param Model $model
+ *
+ * @return void
+ */
+ public function apply(Builder $query, Model $model)
+ {
+ empty($query->columns)
+ ? $query->addSelect(['*', $model->getGuidKey()])
+ : $query->addSelect($model->getGuidKey());
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/User.php
new file mode 100644
index 0000000..24c7f3b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/FreeIPA/User.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace LdapRecord\Models\FreeIPA;
+
+class User extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'person',
+ 'inetorgperson',
+ 'organizationalperson',
+ ];
+
+ /**
+ * Retrieve groups that the current user is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(Group::class, 'member');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Model.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Model.php
new file mode 100644
index 0000000..6ba24b4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Model.php
@@ -0,0 +1,1441 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use ArrayAccess;
+use InvalidArgumentException;
+use JsonSerializable;
+use LdapRecord\Connection;
+use LdapRecord\Container;
+use LdapRecord\EscapesValues;
+use LdapRecord\Models\Attributes\DistinguishedName;
+use LdapRecord\Models\Attributes\Guid;
+use LdapRecord\Models\Events\Renamed;
+use LdapRecord\Models\Events\Renaming;
+use LdapRecord\Query\Model\Builder;
+use LdapRecord\Support\Arr;
+use UnexpectedValueException;
+
+/** @mixin Builder */
+abstract class Model implements ArrayAccess, JsonSerializable
+{
+ use EscapesValues;
+ use Concerns\HasEvents;
+ use Concerns\HasScopes;
+ use Concerns\HasAttributes;
+ use Concerns\HasGlobalScopes;
+ use Concerns\HidesAttributes;
+ use Concerns\HasRelationships;
+
+ /**
+ * Indicates if the model exists in the LDAP directory.
+ *
+ * @var bool
+ */
+ public $exists = false;
+
+ /**
+ * Indicates whether the model was created during the current request lifecycle.
+ *
+ * @var bool
+ */
+ public $wasRecentlyCreated = false;
+
+ /**
+ * Indicates whether the model was renamed during the current request lifecycle.
+ *
+ * @var bool
+ */
+ public $wasRecentlyRenamed = false;
+
+ /**
+ * The models distinguished name.
+ *
+ * @var string|null
+ */
+ protected $dn;
+
+ /**
+ * The base DN of where the model should be created in.
+ *
+ * @var string|null
+ */
+ protected $in;
+
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [];
+
+ /**
+ * The connection container instance.
+ *
+ * @var Container
+ */
+ protected static $container;
+
+ /**
+ * The LDAP connection name for the model.
+ *
+ * @var string|null
+ */
+ protected $connection;
+
+ /**
+ * The attribute key that contains the models object GUID.
+ *
+ * @var string
+ */
+ protected $guidKey = 'objectguid';
+
+ /**
+ * Contains the models modifications.
+ *
+ * @var array
+ */
+ protected $modifications = [];
+
+ /**
+ * The array of global scopes on the model.
+ *
+ * @var array
+ */
+ protected static $globalScopes = [];
+
+ /**
+ * The array of booted models.
+ *
+ * @var array
+ */
+ protected static $booted = [];
+
+ /**
+ * Constructor.
+ *
+ * @param array $attributes
+ */
+ public function __construct(array $attributes = [])
+ {
+ $this->bootIfNotBooted();
+
+ $this->fill($attributes);
+ }
+
+ /**
+ * Check if the model needs to be booted and if so, do it.
+ *
+ * @return void
+ */
+ protected function bootIfNotBooted()
+ {
+ if (! isset(static::$booted[static::class])) {
+ static::$booted[static::class] = true;
+
+ static::boot();
+ }
+ }
+
+ /**
+ * The "booting" method of the model.
+ *
+ * @return void
+ */
+ protected static function boot()
+ {
+ //
+ }
+
+ /**
+ * Clear the list of booted models so they will be re-booted.
+ *
+ * @return void
+ */
+ public static function clearBootedModels()
+ {
+ static::$booted = [];
+
+ static::$globalScopes = [];
+ }
+
+ /**
+ * Handle dynamic method calls into the model.
+ *
+ * @param string $method
+ * @param array $parameters
+ *
+ * @return mixed
+ */
+ public function __call($method, $parameters)
+ {
+ if (method_exists($this, $method)) {
+ return $this->$method(...$parameters);
+ }
+
+ return $this->newQuery()->$method(...$parameters);
+ }
+
+ /**
+ * Handle dynamic static method calls into the method.
+ *
+ * @param string $method
+ * @param array $parameters
+ *
+ * @return mixed
+ */
+ public static function __callStatic($method, $parameters)
+ {
+ return (new static())->$method(...$parameters);
+ }
+
+ /**
+ * Returns the models distinguished name.
+ *
+ * @return string|null
+ */
+ public function getDn()
+ {
+ return $this->dn;
+ }
+
+ /**
+ * Set the models distinguished name.
+ *
+ * @param string $dn
+ *
+ * @return static
+ */
+ public function setDn($dn)
+ {
+ $this->dn = (string) $dn;
+
+ return $this;
+ }
+
+ /**
+ * Get the LDAP connection for the model.
+ *
+ * @return Connection
+ */
+ public function getConnection()
+ {
+ return static::resolveConnection($this->getConnectionName());
+ }
+
+ /**
+ * Get the current connection name for the model.
+ *
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Set the connection associated with the model.
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setConnection($name)
+ {
+ $this->connection = $name;
+
+ return $this;
+ }
+
+ /**
+ * Begin querying the model on a given connection.
+ *
+ * @param string|null $connection
+ *
+ * @return Builder
+ */
+ public static function on($connection = null)
+ {
+ $instance = new static();
+
+ $instance->setConnection($connection);
+
+ return $instance->newQuery();
+ }
+
+ /**
+ * Get all the models from the directory.
+ *
+ * @param array|mixed $attributes
+ *
+ * @return Collection|static[]
+ */
+ public static function all($attributes = ['*'])
+ {
+ return static::query()->select($attributes)->paginate();
+ }
+
+ /**
+ * Begin querying the model.
+ *
+ * @return Builder
+ */
+ public static function query()
+ {
+ return (new static())->newQuery();
+ }
+
+ /**
+ * Get a new query for builder filtered by the current models object classes.
+ *
+ * @return Builder
+ */
+ public function newQuery()
+ {
+ return $this->registerModelScopes(
+ $this->newQueryWithoutScopes()
+ );
+ }
+
+ /**
+ * Get a new query builder that doesn't have any global scopes.
+ *
+ * @return Builder
+ */
+ public function newQueryWithoutScopes()
+ {
+ return static::resolveConnection(
+ $this->getConnectionName()
+ )->query()->model($this);
+ }
+
+ /**
+ * Create a new query builder.
+ *
+ * @param Connection $connection
+ *
+ * @return Builder
+ */
+ public function newQueryBuilder(Connection $connection)
+ {
+ return new Builder($connection);
+ }
+
+ /**
+ * Create a new model instance.
+ *
+ * @param array $attributes
+ *
+ * @return static
+ */
+ public function newInstance(array $attributes = [])
+ {
+ return (new static($attributes))->setConnection($this->getConnectionName());
+ }
+
+ /**
+ * Resolve a connection instance.
+ *
+ * @param string|null $connection
+ *
+ * @return Connection
+ */
+ public static function resolveConnection($connection = null)
+ {
+ return static::getConnectionContainer()->get($connection);
+ }
+
+ /**
+ * Get the connection container.
+ *
+ * @return Container
+ */
+ public static function getConnectionContainer()
+ {
+ return static::$container ?? static::getDefaultConnectionContainer();
+ }
+
+ /**
+ * Get the default singleton container instance.
+ *
+ * @return Container
+ */
+ public static function getDefaultConnectionContainer()
+ {
+ return Container::getInstance();
+ }
+
+ /**
+ * Set the connection container.
+ *
+ * @param Container $container
+ *
+ * @return void
+ */
+ public static function setConnectionContainer(Container $container)
+ {
+ static::$container = $container;
+ }
+
+ /**
+ * Unset the connection container.
+ *
+ * @return void
+ */
+ public static function unsetConnectionContainer()
+ {
+ static::$container = null;
+ }
+
+ /**
+ * Register the query scopes for this builder instance.
+ *
+ * @param Builder $builder
+ *
+ * @return Builder
+ */
+ public function registerModelScopes($builder)
+ {
+ $this->applyObjectClassScopes($builder);
+
+ $this->registerGlobalScopes($builder);
+
+ return $builder;
+ }
+
+ /**
+ * Register the global model scopes.
+ *
+ * @param Builder $builder
+ *
+ * @return Builder
+ */
+ public function registerGlobalScopes($builder)
+ {
+ foreach ($this->getGlobalScopes() as $identifier => $scope) {
+ $builder->withGlobalScope($identifier, $scope);
+ }
+
+ return $builder;
+ }
+
+ /**
+ * Apply the model object class scopes to the given builder instance.
+ *
+ * @param Builder $query
+ *
+ * @return void
+ */
+ public function applyObjectClassScopes(Builder $query)
+ {
+ foreach (static::$objectClasses as $objectClass) {
+ $query->where('objectclass', '=', $objectClass);
+ }
+ }
+
+ /**
+ * Returns the models distinguished name when the model is converted to a string.
+ *
+ * @return null|string
+ */
+ public function __toString()
+ {
+ return $this->getDn();
+ }
+
+ /**
+ * Returns a new batch modification.
+ *
+ * @param string|null $attribute
+ * @param string|int|null $type
+ * @param array $values
+ *
+ * @return BatchModification
+ */
+ public function newBatchModification($attribute = null, $type = null, $values = [])
+ {
+ return new BatchModification($attribute, $type, $values);
+ }
+
+ /**
+ * Returns a new collection with the specified items.
+ *
+ * @param mixed $items
+ *
+ * @return Collection
+ */
+ public function newCollection($items = [])
+ {
+ return new Collection($items);
+ }
+
+ /**
+ * Dynamically retrieve attributes on the object.
+ *
+ * @param mixed $key
+ *
+ * @return bool
+ */
+ public function __get($key)
+ {
+ return $this->getAttribute($key);
+ }
+
+ /**
+ * Dynamically set attributes on the object.
+ *
+ * @param mixed $key
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function __set($key, $value)
+ {
+ return $this->setAttribute($key, $value);
+ }
+
+ /**
+ * Determine if the given offset exists.
+ *
+ * @param string $offset
+ *
+ * @return bool
+ */
+ public function offsetExists($offset)
+ {
+ return ! is_null($this->getAttribute($offset));
+ }
+
+ /**
+ * Get the value for a given offset.
+ *
+ * @param string $offset
+ *
+ * @return mixed
+ */
+ public function offsetGet($offset)
+ {
+ return $this->getAttribute($offset);
+ }
+
+ /**
+ * Set the value at the given offset.
+ *
+ * @param string $offset
+ * @param mixed $value
+ *
+ * @return void
+ */
+ public function offsetSet($offset, $value)
+ {
+ $this->setAttribute($offset, $value);
+ }
+
+ /**
+ * Unset the value at the given offset.
+ *
+ * @param string $offset
+ *
+ * @return void
+ */
+ public function offsetUnset($offset)
+ {
+ unset($this->attributes[$offset]);
+ }
+
+ /**
+ * Determine if an attribute exists on the model.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function __isset($key)
+ {
+ return $this->offsetExists($key);
+ }
+
+ /**
+ * Unset an attribute on the model.
+ *
+ * @param string $key
+ *
+ * @return void
+ */
+ public function __unset($key)
+ {
+ $this->offsetUnset($key);
+ }
+
+ /**
+ * Convert the object into something JSON serializable.
+ *
+ * @return array
+ */
+ public function jsonSerialize()
+ {
+ return $this->attributesToArray();
+ }
+
+ /**
+ * Converts extra attributes for JSON serialization.
+ *
+ * @param array $attributes
+ *
+ * @return array
+ */
+ protected function convertAttributesForJson(array $attributes = [])
+ {
+ // If the model has a GUID set, we need to convert
+ // it due to it being in binary. Otherwise we'll
+ // receive a JSON serialization exception.
+ if ($this->hasAttribute($this->guidKey)) {
+ return array_replace($attributes, [
+ $this->guidKey => [$this->getConvertedGuid()],
+ ]);
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Reload a fresh model instance from the directory.
+ *
+ * @return static|false
+ */
+ public function fresh()
+ {
+ if (! $this->exists) {
+ return false;
+ }
+
+ return $this->newQuery()->find($this->dn);
+ }
+
+ /**
+ * Determine if two models have the same distinguished name and belong to the same connection.
+ *
+ * @param static $model
+ *
+ * @return bool
+ */
+ public function is(self $model)
+ {
+ return $this->dn == $model->getDn() && $this->getConnectionName() == $model->getConnectionName();
+ }
+
+ /**
+ * Hydrate a new collection of models from LDAP search results.
+ *
+ * @param array $records
+ *
+ * @return Collection
+ */
+ public function hydrate($records)
+ {
+ return $this->newCollection($records)->transform(function ($attributes) {
+ return $attributes instanceof static
+ ? $attributes
+ : static::newInstance()->setRawAttributes($attributes);
+ });
+ }
+
+ /**
+ * Converts the current model into the given model.
+ *
+ * @param Model $into
+ *
+ * @return Model
+ */
+ public function convert(self $into)
+ {
+ $into->setDn($this->getDn());
+ $into->setConnection($this->getConnectionName());
+
+ $this->exists
+ ? $into->setRawAttributes($this->getAttributes())
+ : $into->fill($this->getAttributes());
+
+ return $into;
+ }
+
+ /**
+ * Refreshes the current models attributes with the directory values.
+ *
+ * @return bool
+ */
+ public function refresh()
+ {
+ if ($model = $this->fresh()) {
+ $this->setRawAttributes($model->getAttributes());
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the model's batch modifications to be processed.
+ *
+ * @return array
+ */
+ public function getModifications()
+ {
+ $builtModifications = [];
+
+ foreach ($this->buildModificationsFromDirty() as $modification) {
+ $builtModifications[] = $modification->get();
+ }
+
+ return array_merge($this->modifications, $builtModifications);
+ }
+
+ /**
+ * Set the models batch modifications.
+ *
+ * @param array $modifications
+ *
+ * @return $this
+ */
+ public function setModifications(array $modifications = [])
+ {
+ $this->modifications = [];
+
+ foreach ($modifications as $modification) {
+ $this->addModification($modification);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds a batch modification to the model.
+ *
+ * @param array|BatchModification $mod
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return $this
+ */
+ public function addModification($mod = [])
+ {
+ if ($mod instanceof BatchModification) {
+ $mod = $mod->get();
+ }
+
+ if ($this->isValidModification($mod)) {
+ $this->modifications[] = $mod;
+
+ return $this;
+ }
+
+ throw new InvalidArgumentException(
+ "The batch modification array does not include the mandatory 'attrib' or 'modtype' keys."
+ );
+ }
+
+ /**
+ * Get the model's guid attribute key name.
+ *
+ * @return string
+ */
+ public function getGuidKey()
+ {
+ return $this->guidKey;
+ }
+
+ /**
+ * Get the model's ANR attributes for querying when incompatible with ANR.
+ *
+ * @return array
+ */
+ public function getAnrAttributes()
+ {
+ return ['cn', 'sn', 'uid', 'name', 'mail', 'givenname', 'displayname'];
+ }
+
+ /**
+ * Get the name of the model, or the given DN.
+ *
+ * @param string|null $dn
+ *
+ * @return string|null
+ */
+ public function getName($dn = null)
+ {
+ return $this->newDn($dn ?? $this->dn)->name();
+ }
+
+ /**
+ * Get the head attribute of the model, or the given DN.
+ *
+ * @param string|null $dn
+ *
+ * @return string|null
+ */
+ public function getHead($dn = null)
+ {
+ return $this->newDn($dn ?? $this->dn)->head();
+ }
+
+ /**
+ * Get the RDN of the model, of the given DN.
+ *
+ * @param string|null
+ *
+ * @return string|null
+ */
+ public function getRdn($dn = null)
+ {
+ return $this->newDn($dn ?? $this->dn)->relative();
+ }
+
+ /**
+ * Get the parent distinguished name of the model, or the given DN.
+ *
+ * @param string|null
+ *
+ * @return string|null
+ */
+ public function getParentDn($dn = null)
+ {
+ return $this->newDn($dn ?? $this->dn)->parent();
+ }
+
+ /**
+ * Create a new Distinguished Name object.
+ *
+ * @param string|null $dn
+ *
+ * @return DistinguishedName
+ */
+ public function newDn($dn = null)
+ {
+ return new DistinguishedName($dn);
+ }
+
+ /**
+ * Get the model's object GUID key.
+ *
+ * @return void
+ */
+ public function getObjectGuidKey()
+ {
+ return $this->guidKey;
+ }
+
+ /**
+ * Get the model's binary object GUID.
+ *
+ * @see https://msdn.microsoft.com/en-us/library/ms679021(v=vs.85).aspx
+ *
+ * @return string|null
+ */
+ public function getObjectGuid()
+ {
+ return $this->getFirstAttribute($this->guidKey);
+ }
+
+ /**
+ * Get the model's object classes.
+ *
+ * @return array
+ */
+ public function getObjectClasses()
+ {
+ return $this->getAttribute('objectclass') ?: [];
+ }
+
+ /**
+ * Get the model's string GUID.
+ *
+ * @return string|null
+ */
+ public function getConvertedGuid()
+ {
+ try {
+ return (string) new Guid($this->getObjectGuid());
+ } catch (InvalidArgumentException $e) {
+ return;
+ }
+ }
+
+ /**
+ * Determine if the current model is a direct descendant of the given.
+ *
+ * @param static|string $parent
+ *
+ * @return bool
+ */
+ public function isChildOf($parent)
+ {
+ return $this->newDn($this->getDn())->isChildOf(
+ $this->newDn((string) $parent)
+ );
+ }
+
+ /**
+ * Determine if the current model is a direct ascendant of the given.
+ *
+ * @param static|string $child
+ *
+ * @return bool
+ */
+ public function isParentOf($child)
+ {
+ return $this->newDn($this->getDn())->isParentOf(
+ $this->newDn((string) $child)
+ );
+ }
+
+ /**
+ * Determine if the current model is a descendant of the given.
+ *
+ * @param static|string $model
+ *
+ * @return bool
+ */
+ public function isDescendantOf($model)
+ {
+ return $this->dnIsInside($this->getDn(), $model);
+ }
+
+ /**
+ * Determine if the current model is a ancestor of the given.
+ *
+ * @param static|string $model
+ *
+ * @return bool
+ */
+ public function isAncestorOf($model)
+ {
+ return $this->dnIsInside($model, $this->getDn());
+ }
+
+ /**
+ * Determines if the DN is inside of the parent DN.
+ *
+ * @param static|string $dn
+ * @param static|string $parentDn
+ *
+ * @return bool
+ */
+ protected function dnIsInside($dn, $parentDn)
+ {
+ return $this->newDn((string) $dn)->isDescendantOf(
+ $this->newDn($parentDn)
+ );
+ }
+
+ /**
+ * Set the base DN of where the model should be created in.
+ *
+ * @param static|string $dn
+ *
+ * @return $this
+ */
+ public function inside($dn)
+ {
+ $this->in = $dn instanceof self ? $dn->getDn() : $dn;
+
+ return $this;
+ }
+
+ /**
+ * Save the model to the directory.
+ *
+ * @param array $attributes The attributes to update or create for the current entry.
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function save(array $attributes = [])
+ {
+ $this->fill($attributes);
+
+ $this->fireModelEvent(new Events\Saving($this));
+
+ $this->exists ? $this->performUpdate() : $this->performInsert();
+
+ $this->fireModelEvent(new Events\Saved($this));
+
+ $this->in = null;
+ }
+
+ /**
+ * Inserts the model into the directory.
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ protected function performInsert()
+ {
+ // Here we will populate the models object classes if it
+ // does not already have any set. An LDAP object cannot
+ // be successfully created in the server without them.
+ if (! $this->hasAttribute('objectclass')) {
+ $this->setAttribute('objectclass', static::$objectClasses);
+ }
+
+ $query = $this->newQuery();
+
+ // If the model does not currently have a distinguished
+ // name, we will attempt to generate one automatically
+ // using the current query builder's DN as the base.
+ if (empty($this->getDn())) {
+ $this->setDn($this->getCreatableDn());
+ }
+
+ $this->fireModelEvent(new Events\Creating($this));
+
+ // Here we perform the insert of new object in the directory,
+ // but filter out any empty attributes before sending them
+ // to the server. LDAP servers will throw an exception if
+ // attributes have been given empty or null values.
+ $query->insert($this->getDn(), array_filter($this->getAttributes()));
+
+ $this->fireModelEvent(new Events\Created($this));
+
+ $this->syncOriginal();
+
+ $this->exists = true;
+
+ $this->wasRecentlyCreated = true;
+ }
+
+ /**
+ * Updates the model in the directory.
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ protected function performUpdate()
+ {
+ if (! count($modifications = $this->getModifications())) {
+ return;
+ }
+
+ $this->fireModelEvent(new Events\Updating($this));
+
+ $this->newQuery()->update($this->dn, $modifications);
+
+ $this->fireModelEvent(new Events\Updated($this));
+
+ $this->syncOriginal();
+
+ $this->modifications = [];
+ }
+
+ /**
+ * Create the model in the directory.
+ *
+ * @param array $attributes The attributes for the new entry.
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return Model
+ */
+ public static function create(array $attributes = [])
+ {
+ $instance = new static($attributes);
+
+ $instance->save();
+
+ return $instance;
+ }
+
+ /**
+ * Create an attribute on the model.
+ *
+ * @param string $attribute The attribute to create
+ * @param mixed $value The value of the new attribute
+ *
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function createAttribute($attribute, $value)
+ {
+ $this->validateExistence();
+
+ $this->newQuery()->insertAttributes($this->dn, [$attribute => (array) $value]);
+
+ $this->addAttributeValue($attribute, $value);
+ }
+
+ /**
+ * Update the model.
+ *
+ * @param array $attributes The attributes to update for the current entry.
+ *
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function update(array $attributes = [])
+ {
+ $this->validateExistence();
+
+ $this->save($attributes);
+ }
+
+ /**
+ * Update the model attribute with the specified value.
+ *
+ * @param string $attribute The attribute to modify
+ * @param mixed $value The new value for the attribute
+ *
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function updateAttribute($attribute, $value)
+ {
+ $this->validateExistence();
+
+ $this->newQuery()->updateAttributes($this->dn, [$attribute => (array) $value]);
+
+ $this->addAttributeValue($attribute, $value);
+ }
+
+ /**
+ * Destroy the models for the given distinguished names.
+ *
+ * @param Collection|array|string $dns
+ * @param bool $recursive
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return int
+ */
+ public static function destroy($dns, $recursive = false)
+ {
+ $count = 0;
+
+ $dns = is_string($dns) ? (array) $dns : $dns;
+
+ $instance = new static();
+
+ foreach ($dns as $dn) {
+ if (! $model = $instance->find($dn)) {
+ continue;
+ }
+
+ $model->delete($recursive);
+
+ $count++;
+ }
+
+ return $count;
+ }
+
+ /**
+ * Delete the model from the directory.
+ *
+ * Throws a ModelNotFoundException if the current model does
+ * not exist or does not contain a distinguished name.
+ *
+ * @param bool $recursive Whether to recursively delete leaf nodes (models that are children).
+ *
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function delete($recursive = false)
+ {
+ $this->validateExistence();
+
+ $this->fireModelEvent(new Events\Deleting($this));
+
+ if ($recursive) {
+ $this->deleteLeafNodes();
+ }
+
+ $this->newQuery()->delete($this->dn);
+
+ // If the deletion is successful, we will mark the model
+ // as non-existing, and then fire the deleted event so
+ // developers can hook in and run further operations.
+ $this->exists = false;
+
+ $this->fireModelEvent(new Events\Deleted($this));
+ }
+
+ /**
+ * Deletes leaf nodes that are attached to the model.
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return Collection
+ */
+ protected function deleteLeafNodes()
+ {
+ return $this->newQueryWithoutScopes()
+ ->in($this->dn)
+ ->listing()
+ ->paginate()
+ ->each(function (self $model) {
+ $model->delete($recursive = true);
+ });
+ }
+
+ /**
+ * Delete an attribute on the model.
+ *
+ * @param string|array $attributes The attribute(s) to delete
+ *
+ * Delete specific values in attributes:
+ *
+ * ["memberuid" => "jdoe"]
+ *
+ * Delete an entire attribute:
+ *
+ * ["memberuid" => []]
+ *
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function deleteAttribute($attributes)
+ {
+ $this->validateExistence();
+
+ $attributes = $this->makeDeletableAttributes($attributes);
+
+ $this->newQuery()->deleteAttributes($this->dn, $attributes);
+
+ foreach ($attributes as $attribute => $value) {
+ // If the attribute value is empty, we can assume the
+ // attribute was completely deleted from the model.
+ // We will pull the attribute out and continue on.
+ if (empty($value)) {
+ unset($this->attributes[$attribute]);
+ }
+ // Otherwise, only specific attribute values have been
+ // removed. We will determine which ones have been
+ // removed and update the attributes value.
+ elseif (Arr::exists($this->attributes, $attribute)) {
+ $this->attributes[$attribute] = array_values(
+ array_diff($this->attributes[$attribute], (array) $value)
+ );
+ }
+ }
+
+ $this->syncOriginal();
+ }
+
+ /**
+ * Make a deletable attribute array.
+ *
+ * @param string|array $attributes
+ *
+ * @return array
+ */
+ protected function makeDeletableAttributes($attributes)
+ {
+ $delete = [];
+
+ foreach (Arr::wrap($attributes) as $key => $value) {
+ is_int($key)
+ ? $delete[$value] = []
+ : $delete[$key] = Arr::wrap($value);
+ }
+
+ return $delete;
+ }
+
+ /**
+ * Move the model into the given new parent.
+ *
+ * For example: $user->move($ou);
+ *
+ * @param static|string $newParentDn The new parent of the current model.
+ * @param bool $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
+ *
+ * @throws UnexpectedValueException
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function move($newParentDn, $deleteOldRdn = true)
+ {
+ $this->validateExistence();
+
+ if (! $rdn = $this->getRdn()) {
+ throw new UnexpectedValueException('Current model does not contain an RDN to move.');
+ }
+
+ $this->rename($rdn, $newParentDn, $deleteOldRdn);
+ }
+
+ /**
+ * Rename the model to a new RDN and new parent.
+ *
+ * @param string $rdn The models new relative distinguished name. Example: "cn=JohnDoe"
+ * @param static|string|null $newParentDn The models new parent distinguished name (if moving). Leave this null if you are only renaming. Example: "ou=MovedUsers,dc=acme,dc=org"
+ * @param bool|true $deleteOldRdn Whether to delete the old models relative distinguished name once renamed / moved.
+ *
+ * @throws ModelDoesNotExistException
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function rename($rdn, $newParentDn = null, $deleteOldRdn = true)
+ {
+ $this->validateExistence();
+
+ if ($newParentDn instanceof self) {
+ $newParentDn = $newParentDn->getDn();
+ }
+
+ if (is_null($newParentDn)) {
+ $newParentDn = $this->getParentDn($this->dn);
+ }
+
+ // If the RDN and the new parent DN are the same as the current,
+ // we will simply return here to prevent a rename operation
+ // being sent, which would fail anyway in such case.
+ if (
+ $rdn === $this->getRdn()
+ && $newParentDn === $this->getParentDn()
+ ) {
+ return;
+ }
+
+ $this->fireModelEvent(new Renaming($this, $rdn, $newParentDn));
+
+ $this->newQuery()->rename($this->dn, $rdn, $newParentDn, $deleteOldRdn);
+
+ // If the model was successfully renamed, we will set
+ // its new DN so any further updates to the model
+ // can be performed without any issues.
+ $this->dn = implode(',', [$rdn, $newParentDn]);
+
+ $map = $this->newDn($this->dn)->assoc();
+
+ // Here we'll populate the models new primary
+ // RDN attribute on the model so we do not
+ // have to re-synchronize with the server.
+ $modelNameAttribute = key($map);
+
+ $this->attributes[$modelNameAttribute]
+ = $this->original[$modelNameAttribute]
+ = [reset($map[$modelNameAttribute])];
+
+ $this->fireModelEvent(new Renamed($this));
+
+ $this->wasRecentlyRenamed = true;
+ }
+
+ /**
+ * Get a distinguished name that is creatable for the model.
+ *
+ * @param string|null $name
+ * @param string|null $attribute
+ *
+ * @return string
+ */
+ public function getCreatableDn($name = null, $attribute = null)
+ {
+ return implode(',', [
+ $this->getCreatableRdn($name, $attribute),
+ $this->in ?? $this->newQuery()->getbaseDn(),
+ ]);
+ }
+
+ /**
+ * Get a creatable (escaped) RDN for the model.
+ *
+ * @param string|null $name
+ * @param string|null $attribute
+ *
+ * @return string
+ */
+ public function getCreatableRdn($name = null, $attribute = null)
+ {
+ $attribute = $attribute ?? $this->getCreatableRdnAttribute();
+
+ $name = $this->escape(
+ $name ?? $this->getFirstAttribute($attribute)
+ )->dn();
+
+ return "$attribute=$name";
+ }
+
+ /**
+ * Get the creatable RDN attribute name.
+ *
+ * @return string
+ */
+ protected function getCreatableRdnAttribute()
+ {
+ return 'cn';
+ }
+
+ /**
+ * Determines if the given modification is valid.
+ *
+ * @param mixed $mod
+ *
+ * @return bool
+ */
+ protected function isValidModification($mod)
+ {
+ return Arr::accessible($mod)
+ && Arr::exists($mod, BatchModification::KEY_MODTYPE)
+ && Arr::exists($mod, BatchModification::KEY_ATTRIB);
+ }
+
+ /**
+ * Builds the models modifications from its dirty attributes.
+ *
+ * @return BatchModification[]
+ */
+ protected function buildModificationsFromDirty()
+ {
+ $modifications = [];
+
+ foreach ($this->getDirty() as $attribute => $values) {
+ $modification = $this->newBatchModification($attribute, null, (array) $values);
+
+ if (Arr::exists($this->original, $attribute)) {
+ // If the attribute we're modifying has an original value, we will
+ // give the BatchModification object its values to automatically
+ // determine which type of LDAP operation we need to perform.
+ $modification->setOriginal($this->original[$attribute]);
+ }
+
+ if (! $modification->build()->isValid()) {
+ continue;
+ }
+
+ $modifications[] = $modification;
+ }
+
+ return $modifications;
+ }
+
+ /**
+ * Validates that the current model exists.
+ *
+ * @throws ModelDoesNotExistException
+ *
+ * @return void
+ */
+ protected function validateExistence()
+ {
+ if (! $this->exists || is_null($this->dn)) {
+ throw ModelDoesNotExistException::forModel($this);
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelDoesNotExistException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelDoesNotExistException.php
new file mode 100644
index 0000000..2dd2ba9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelDoesNotExistException.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use LdapRecord\LdapRecordException;
+
+class ModelDoesNotExistException extends LdapRecordException
+{
+ /**
+ * The class name of the model that does not exist.
+ *
+ * @var Model
+ */
+ protected $model;
+
+ /**
+ * Create a new exception for the given model.
+ *
+ * @param Model $model
+ *
+ * @return ModelDoesNotExistException
+ */
+ public static function forModel(Model $model)
+ {
+ return (new static())->setModel($model);
+ }
+
+ /**
+ * Set the model that does not exist.
+ *
+ * @param Model $model
+ *
+ * @return ModelDoesNotExistException
+ */
+ public function setModel(Model $model)
+ {
+ $this->model = $model;
+
+ $class = get_class($model);
+
+ $this->message = "Model [{$class}] does not exist.";
+
+ return $this;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelNotFoundException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelNotFoundException.php
new file mode 100644
index 0000000..be88bab
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/ModelNotFoundException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use LdapRecord\Query\ObjectNotFoundException;
+
+class ModelNotFoundException extends ObjectNotFoundException
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Entry.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Entry.php
new file mode 100644
index 0000000..b7ad37a
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Entry.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+use LdapRecord\Connection;
+use LdapRecord\Models\Entry as BaseEntry;
+use LdapRecord\Models\OpenLDAP\Scopes\AddEntryUuidToSelects;
+use LdapRecord\Models\Types\OpenLDAP;
+use LdapRecord\Query\Model\OpenLdapBuilder;
+
+/** @mixin OpenLdapBuilder */
+class Entry extends BaseEntry implements OpenLDAP
+{
+ /**
+ * The attribute key that contains the models object GUID.
+ *
+ * @var string
+ */
+ protected $guidKey = 'entryuuid';
+
+ /**
+ * @inheritdoc
+ */
+ protected static function boot()
+ {
+ parent::boot();
+
+ // Here we'll add a global scope to all OpenLDAP models to ensure the
+ // Entry UUID is always selected on each query. This attribute is
+ // virtual, so it must be manually selected to be included.
+ static::addGlobalScope(new AddEntryUuidToSelects());
+ }
+
+ /**
+ * Create a new query builder.
+ *
+ * @param Connection $connection
+ *
+ * @return OpenLdapBuilder
+ */
+ public function newQueryBuilder(Connection $connection)
+ {
+ return new OpenLdapBuilder($connection);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Group.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Group.php
new file mode 100644
index 0000000..2d8d94e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Group.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+class Group extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'groupofuniquenames',
+ ];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/OrganizationalUnit.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/OrganizationalUnit.php
new file mode 100644
index 0000000..7ae0a37
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/OrganizationalUnit.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+class OrganizationalUnit extends Entry
+{
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'organizationalunit',
+ ];
+
+ /**
+ * Get the creatable RDN attribute name.
+ *
+ * @return string
+ */
+ public function getCreatableRdnAttribute()
+ {
+ return 'ou';
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Scopes/AddEntryUuidToSelects.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Scopes/AddEntryUuidToSelects.php
new file mode 100644
index 0000000..54376c2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/Scopes/AddEntryUuidToSelects.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP\Scopes;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Models\Scope;
+use LdapRecord\Query\Model\Builder;
+
+class AddEntryUuidToSelects implements Scope
+{
+ /**
+ * Add the entry UUID to the selected attributes.
+ *
+ * @param Builder $query
+ * @param Model $model
+ *
+ * @return void
+ */
+ public function apply(Builder $query, Model $model)
+ {
+ empty($query->columns)
+ ? $query->addSelect(['*', $model->getGuidKey()])
+ : $query->addSelect($model->getGuidKey());
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/User.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/User.php
new file mode 100644
index 0000000..b37f390
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/OpenLDAP/User.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace LdapRecord\Models\OpenLDAP;
+
+use Illuminate\Contracts\Auth\Authenticatable;
+use LdapRecord\Models\Concerns\CanAuthenticate;
+use LdapRecord\Models\Concerns\HasPassword;
+
+class User extends Entry implements Authenticatable
+{
+ use HasPassword;
+ use CanAuthenticate;
+
+ /**
+ * The password's attribute name.
+ *
+ * @var string
+ */
+ protected $passwordAttribute = 'userpassword';
+
+ /**
+ * The password's hash method.
+ *
+ * @var string
+ */
+ protected $passwordHashMethod = 'ssha';
+
+ /**
+ * The object classes of the LDAP model.
+ *
+ * @var array
+ */
+ public static $objectClasses = [
+ 'top',
+ 'person',
+ 'organizationalperson',
+ 'inetorgperson',
+ ];
+
+ /**
+ * The groups relationship.
+ *
+ * Retrieves groups that the user is apart of.
+ *
+ * @return \LdapRecord\Models\Relations\HasMany
+ */
+ public function groups()
+ {
+ return $this->hasMany(Group::class, 'memberuid', 'uid');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasMany.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasMany.php
new file mode 100644
index 0000000..d8dfa08
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasMany.php
@@ -0,0 +1,344 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use Closure;
+use LdapRecord\DetectsErrors;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Model;
+use LdapRecord\Models\ModelNotFoundException;
+use LdapRecord\Query\Collection;
+
+class HasMany extends OneToMany
+{
+ use DetectsErrors;
+
+ /**
+ * The model to use for attaching / detaching.
+ *
+ * @var Model
+ */
+ protected $using;
+
+ /**
+ * The attribute key to use for attaching / detaching.
+ *
+ * @var string
+ */
+ protected $usingKey;
+
+ /**
+ * The pagination page size.
+ *
+ * @var int
+ */
+ protected $pageSize = 1000;
+
+ /**
+ * The exceptions to bypass for each relation operation.
+ *
+ * @var array
+ */
+ protected $bypass = [
+ 'attach' => [
+ 'Already exists', 'Type or value exists',
+ ],
+ 'detach' => [
+ 'No such attribute', 'Server is unwilling to perform',
+ ],
+ ];
+
+ /**
+ * Set the model and attribute to use for attaching / detaching.
+ *
+ * @param Model $using
+ * @param string $usingKey
+ *
+ * @return $this
+ */
+ public function using(Model $using, $usingKey)
+ {
+ $this->using = $using;
+ $this->usingKey = $usingKey;
+
+ return $this;
+ }
+
+ /**
+ * Set the pagination page size of the relation query.
+ *
+ * @param int $pageSize
+ *
+ * @return $this
+ */
+ public function setPageSize($pageSize)
+ {
+ $this->pageSize = $pageSize;
+
+ return $this;
+ }
+
+ /**
+ * Paginate the relation using the given page size.
+ *
+ * @param int $pageSize
+ *
+ * @return Collection
+ */
+ public function paginate($pageSize = 1000)
+ {
+ return $this->paginateOnceUsing($pageSize);
+ }
+
+ /**
+ * Paginate the relation using the page size once.
+ *
+ * @param int $pageSize
+ *
+ * @return Collection
+ */
+ protected function paginateOnceUsing($pageSize)
+ {
+ $size = $this->pageSize;
+
+ $result = $this->setPageSize($pageSize)->get();
+
+ $this->pageSize = $size;
+
+ return $result;
+ }
+
+ /**
+ * Chunk the relation results using the given callback.
+ *
+ * @param int $pageSize
+ * @param Closure $callback
+ *
+ * @return void
+ */
+ public function chunk($pageSize, Closure $callback)
+ {
+ $this->getRelationQuery()->chunk($pageSize, function ($entries) use ($callback) {
+ $callback($this->transformResults($entries));
+ });
+ }
+
+ /**
+ * Get the relationships results.
+ *
+ * @return Collection
+ */
+ public function getRelationResults()
+ {
+ return $this->transformResults(
+ $this->getRelationQuery()->paginate($this->pageSize)
+ );
+ }
+
+ /**
+ * Get the prepared relationship query.
+ *
+ * @return \LdapRecord\Query\Model\Builder
+ */
+ public function getRelationQuery()
+ {
+ $columns = $this->query->getSelects();
+
+ // We need to select the proper key to be able to retrieve its
+ // value from LDAP results. If we don't, we won't be able
+ // to properly attach / detach models from relation
+ // query results as the attribute will not exist.
+ $key = $this->using ? $this->usingKey : $this->relationKey;
+
+ // If the * character is missing from the attributes to select,
+ // we will add the key to the attributes to select and also
+ // validate that the key isn't already being selected
+ // to prevent stacking on multiple relation calls.
+ if (! in_array('*', $columns) && ! in_array($key, $columns)) {
+ $this->query->addSelect($key);
+ }
+
+ return $this->query->whereRaw(
+ $this->relationKey,
+ '=',
+ $this->getEscapedForeignValueFromModel($this->parent)
+ );
+ }
+
+ /**
+ * Attach a model to the relation.
+ *
+ * @param Model|string $model
+ *
+ * @return Model|string|false
+ */
+ public function attach($model)
+ {
+ return $this->attemptFailableOperation(
+ $this->buildAttachCallback($model),
+ $this->bypass['attach'],
+ $model
+ );
+ }
+
+ /**
+ * Build the attach callback.
+ *
+ * @param Model|string $model
+ *
+ * @return \Closure
+ */
+ protected function buildAttachCallback($model)
+ {
+ return function () use ($model) {
+ $foreign = $this->getAttachableForeignValue($model);
+
+ if ($this->using) {
+ return $this->using->createAttribute($this->usingKey, $foreign);
+ }
+
+ if (! $model instanceof Model) {
+ $model = $this->getForeignModelByValueOrFail($model);
+ }
+
+ return $model->createAttribute($this->relationKey, $foreign);
+ };
+ }
+
+ /**
+ * Attach a collection of models to the parent instance.
+ *
+ * @param iterable $models
+ *
+ * @return iterable
+ */
+ public function attachMany($models)
+ {
+ foreach ($models as $model) {
+ $this->attach($model);
+ }
+
+ return $models;
+ }
+
+ /**
+ * Detach the model from the relation.
+ *
+ * @param Model|string $model
+ *
+ * @return Model|string|false
+ */
+ public function detach($model)
+ {
+ return $this->attemptFailableOperation(
+ $this->buildDetachCallback($model),
+ $this->bypass['detach'],
+ $model
+ );
+ }
+
+ /**
+ * Build the detach callback.
+ *
+ * @param Model|string $model
+ *
+ * @return \Closure
+ */
+ protected function buildDetachCallback($model)
+ {
+ return function () use ($model) {
+ $foreign = $this->getAttachableForeignValue($model);
+
+ if ($this->using) {
+ return $this->using->deleteAttribute([$this->usingKey => $foreign]);
+ }
+
+ if (! $model instanceof Model) {
+ $model = $this->getForeignModelByValueOrFail($model);
+ }
+
+ return $model->deleteAttribute([$this->relationKey => $foreign]);
+ };
+ }
+
+ /**
+ * Get the attachable foreign value from the model.
+ *
+ * @param Model|string $model
+ *
+ * @return string
+ */
+ protected function getAttachableForeignValue($model)
+ {
+ if ($model instanceof Model) {
+ return $this->using
+ ? $this->getForeignValueFromModel($model)
+ : $this->getParentForeignValue();
+ }
+
+ return $this->using ? $model : $this->getParentForeignValue();
+ }
+
+ /**
+ * Get the foreign model by the given value, or fail.
+ *
+ * @param string $model
+ *
+ * @throws ModelNotFoundException
+ *
+ * @return Model
+ */
+ protected function getForeignModelByValueOrFail($model)
+ {
+ if (! is_null($model = $this->getForeignModelByValue($model))) {
+ return $model;
+ }
+
+ throw ModelNotFoundException::forQuery(
+ $this->query->getUnescapedQuery(),
+ $this->query->getDn()
+ );
+ }
+
+ /**
+ * Attempt a failable operation and return the value if successful.
+ *
+ * If a bypassable exception is encountered, the value will be returned.
+ *
+ * @param callable $operation
+ * @param string|array $bypass
+ * @param mixed $value
+ *
+ * @throws LdapRecordException
+ *
+ * @return mixed
+ */
+ protected function attemptFailableOperation($operation, $bypass, $value)
+ {
+ try {
+ $operation();
+
+ return $value;
+ } catch (LdapRecordException $e) {
+ if ($this->errorContainsMessage($e->getMessage(), $bypass)) {
+ return $value;
+ }
+
+ throw $e;
+ }
+ }
+
+ /**
+ * Detach all relation models.
+ *
+ * @return Collection
+ */
+ public function detachAll()
+ {
+ return $this->onceWithoutMerging(function () {
+ return $this->get()->each(function (Model $model) {
+ $this->detach($model);
+ });
+ });
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasManyIn.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasManyIn.php
new file mode 100644
index 0000000..303a144
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasManyIn.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Query\Collection;
+
+class HasManyIn extends OneToMany
+{
+ /**
+ * Get the relationships results.
+ *
+ * @return Collection
+ */
+ public function getRelationResults()
+ {
+ $results = $this->parent->newCollection();
+
+ foreach ((array) $this->parent->getAttribute($this->relationKey) as $value) {
+ if ($foreign = $this->getForeignModelByValue($value)) {
+ $results->push($foreign);
+ }
+ }
+
+ return $this->transformResults($results);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasOne.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasOne.php
new file mode 100644
index 0000000..9a9b2f9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/HasOne.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Models\Model;
+
+class HasOne extends Relation
+{
+ /**
+ * Get the results of the relationship.
+ *
+ * @return \LdapRecord\Query\Collection
+ */
+ public function getResults()
+ {
+ $model = $this->getForeignModelByValue(
+ $this->getFirstAttributeValue($this->parent, $this->relationKey)
+ );
+
+ return $this->transformResults(
+ $this->parent->newCollection($model ? [$model] : null)
+ );
+ }
+
+ /**
+ * Attach a model instance to the parent model.
+ *
+ * @param Model|string $model
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return Model|string
+ */
+ public function attach($model)
+ {
+ $foreign = $model instanceof Model
+ ? $this->getForeignValueFromModel($model)
+ : $model;
+
+ $this->parent->setAttribute($this->relationKey, $foreign)->save();
+
+ return $model;
+ }
+
+ /**
+ * Detach the related model from the parent.
+ *
+ * @throws \LdapRecord\LdapRecordException
+ *
+ * @return void
+ */
+ public function detach()
+ {
+ $this->parent->setAttribute($this->relationKey, null)->save();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/OneToMany.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/OneToMany.php
new file mode 100644
index 0000000..d0a407c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/OneToMany.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Models\Model;
+use LdapRecord\Query\Collection;
+use LdapRecord\Query\Model\Builder;
+
+abstract class OneToMany extends Relation
+{
+ /**
+ * The relation to merge results with.
+ *
+ * @var OneToMany|null
+ */
+ protected $with;
+
+ /**
+ * The name of the relationship.
+ *
+ * @var string
+ */
+ protected $relationName;
+
+ /**
+ * Whether to include recursive results.
+ *
+ * @var bool
+ */
+ protected $recursive = false;
+
+ /**
+ * Constructor.
+ *
+ * @param Builder $query
+ * @param Model $parent
+ * @param string $related
+ * @param string $relationKey
+ * @param string $foreignKey
+ * @param string $relationName
+ */
+ public function __construct(Builder $query, Model $parent, $related, $relationKey, $foreignKey, $relationName)
+ {
+ $this->relationName = $relationName;
+
+ parent::__construct($query, $parent, $related, $relationKey, $foreignKey);
+ }
+
+ /**
+ * Set the relation to load with its parent.
+ *
+ * @param OneToMany $relation
+ *
+ * @return $this
+ */
+ public function with(Relation $relation)
+ {
+ $this->with = $relation;
+
+ return $this;
+ }
+
+ /**
+ * Whether to include recursive results.
+ *
+ * @param bool $enable
+ *
+ * @return $this
+ */
+ public function recursive($enable = true)
+ {
+ $this->recursive = $enable;
+
+ return $this;
+ }
+
+ /**
+ * Get the immediate relationships results.
+ *
+ * @return Collection
+ */
+ abstract public function getRelationResults();
+
+ /**
+ * Get the results of the relationship.
+ *
+ * @return Collection
+ */
+ public function getResults()
+ {
+ $results = $this->recursive
+ ? $this->getRecursiveResults()
+ : $this->getRelationResults();
+
+ return $results->merge(
+ $this->getMergingRelationResults()
+ );
+ }
+
+ /**
+ * Execute the callback excluding the merged query result.
+ *
+ * @param callable $callback
+ *
+ * @return mixed
+ */
+ protected function onceWithoutMerging($callback)
+ {
+ $merging = $this->with;
+
+ $this->with = null;
+
+ $result = $callback();
+
+ $this->with = $merging;
+
+ return $result;
+ }
+
+ /**
+ * Get the relation name.
+ *
+ * @return string
+ */
+ public function getRelationName()
+ {
+ return $this->relationName;
+ }
+
+ /**
+ * Get the results of the merging 'with' relation.
+ *
+ * @return Collection
+ */
+ protected function getMergingRelationResults()
+ {
+ return $this->with
+ ? $this->with->recursive($this->recursive)->get()
+ : $this->parent->newCollection();
+ }
+
+ /**
+ * Get the results for the models relation recursively.
+ *
+ * @param string[] $loaded The distinguished names of models already loaded
+ *
+ * @return Collection
+ */
+ protected function getRecursiveResults(array $loaded = [])
+ {
+ $results = $this->getRelationResults()->reject(function (Model $model) use ($loaded) {
+ // Here we will exclude the models that we have already
+ // loaded the recursive results for so we don't run
+ // into issues with circular relations in LDAP.
+ return in_array($model->getDn(), $loaded);
+ });
+
+ foreach ($results as $model) {
+ $loaded[] = $model->getDn();
+
+ // Finally, we will fetch the related models relations,
+ // passing along our loaded models, to ensure we do
+ // not attempt fetching already loaded relations.
+ $results = $results->merge(
+ $this->getRecursiveRelationResults($model, $loaded)
+ );
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get the recursive relation results for given model.
+ *
+ * @param Model $model
+ * @param array $loaded
+ *
+ * @return Collection
+ */
+ protected function getRecursiveRelationResults(Model $model, array $loaded)
+ {
+ return method_exists($model, $this->relationName)
+ ? $model->{$this->relationName}()->getRecursiveResults($loaded)
+ : $model->newCollection();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/Relation.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/Relation.php
new file mode 100644
index 0000000..1b108fd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Relations/Relation.php
@@ -0,0 +1,368 @@
+<?php
+
+namespace LdapRecord\Models\Relations;
+
+use LdapRecord\Models\Entry;
+use LdapRecord\Models\Model;
+use LdapRecord\Query\Collection;
+use LdapRecord\Query\Model\Builder;
+
+/**
+ * @method bool exists($models = null) Determine if the relation contains all of the given models, or any models
+ * @method bool contains($models) Determine if any of the given models are contained in the relation
+ */
+abstract class Relation
+{
+ /**
+ * The underlying LDAP query.
+ *
+ * @var Builder
+ */
+ protected $query;
+
+ /**
+ * The parent model instance.
+ *
+ * @var Model
+ */
+ protected $parent;
+
+ /**
+ * The related models.
+ *
+ * @var array
+ */
+ protected $related;
+
+ /**
+ * The relation key.
+ *
+ * @var string
+ */
+ protected $relationKey;
+
+ /**
+ * The foreign key.
+ *
+ * @var string
+ */
+ protected $foreignKey;
+
+ /**
+ * The default relation model.
+ *
+ * @var string
+ */
+ protected $default = Entry::class;
+
+ /**
+ * Constructor.
+ *
+ * @param Builder $query
+ * @param Model $parent
+ * @param mixed $related
+ * @param string $relationKey
+ * @param string $foreignKey
+ */
+ public function __construct(Builder $query, Model $parent, $related, $relationKey, $foreignKey)
+ {
+ $this->query = $query;
+ $this->parent = $parent;
+ $this->related = (array) $related;
+ $this->relationKey = $relationKey;
+ $this->foreignKey = $foreignKey;
+
+ $this->initRelation();
+ }
+
+ /**
+ * Handle dynamic method calls to the relationship.
+ *
+ * @param string $method
+ * @param array $parameters
+ *
+ * @return mixed
+ */
+ public function __call($method, $parameters)
+ {
+ if (in_array($method, ['exists', 'contains'])) {
+ return $this->get('objectclass')->$method(...$parameters);
+ }
+
+ $result = $this->query->$method(...$parameters);
+
+ if ($result === $this->query) {
+ return $this;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get the results of the relationship.
+ *
+ * @return Collection
+ */
+ abstract public function getResults();
+
+ /**
+ * Execute the relationship query.
+ *
+ * @param array|string $columns
+ *
+ * @return Collection
+ */
+ public function get($columns = ['*'])
+ {
+ return $this->getResultsWithColumns($columns);
+ }
+
+ /**
+ * Get the results of the relationship while selecting the given columns.
+ *
+ * If the query columns are empty, the given columns are applied.
+ *
+ * @param array $columns
+ *
+ * @return Collection
+ */
+ protected function getResultsWithColumns($columns)
+ {
+ if (is_null($this->query->columns)) {
+ $this->query->select($columns);
+ }
+
+ return $this->getResults();
+ }
+
+ /**
+ * Get the first result of the relationship.
+ *
+ * @param array|string $columns
+ *
+ * @return Model|null
+ */
+ public function first($columns = ['*'])
+ {
+ return $this->get($columns)->first();
+ }
+
+ /**
+ * Prepare the relation query.
+ *
+ * @return static
+ */
+ public function initRelation()
+ {
+ $this->query
+ ->clearFilters()
+ ->withoutGlobalScopes()
+ ->setModel($this->getNewDefaultModel());
+
+ return $this;
+ }
+
+ /**
+ * Get the underlying query for the relation.
+ *
+ * @return Builder
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Get the parent model of the relation.
+ *
+ * @return Model
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Get the relation attribute key.
+ *
+ * @return string
+ */
+ public function getRelationKey()
+ {
+ return $this->relationKey;
+ }
+
+ /**
+ * Get the related model classes for the relation.
+ *
+ * @return array
+ */
+ public function getRelated()
+ {
+ return $this->related;
+ }
+
+ /**
+ * Get the relation foreign attribute key.
+ *
+ * @return string
+ */
+ public function getForeignKey()
+ {
+ return $this->foreignKey;
+ }
+
+ /**
+ * Get the class name of the default model.
+ *
+ * @return string
+ */
+ public function getDefaultModel()
+ {
+ return $this->default;
+ }
+
+ /**
+ * Get a new instance of the default model on the relation.
+ *
+ * @return Model
+ */
+ public function getNewDefaultModel()
+ {
+ $model = new $this->default();
+
+ $model->setConnection($this->parent->getConnectionName());
+
+ return $model;
+ }
+
+ /**
+ * Get the foreign model by the given value.
+ *
+ * @param string $value
+ *
+ * @return Model|null
+ */
+ protected function getForeignModelByValue($value)
+ {
+ return $this->foreignKeyIsDistinguishedName()
+ ? $this->query->find($value)
+ : $this->query->findBy($this->foreignKey, $value);
+ }
+
+ /**
+ * Returns the escaped foreign key value for use in an LDAP filter from the model.
+ *
+ * @param Model $model
+ *
+ * @return string
+ */
+ protected function getEscapedForeignValueFromModel(Model $model)
+ {
+ return $this->query->escape(
+ $this->getForeignValueFromModel($model)
+ )->both();
+ }
+
+ /**
+ * Get the relation parents foreign value.
+ *
+ * @return string
+ */
+ protected function getParentForeignValue()
+ {
+ return $this->getForeignValueFromModel($this->parent);
+ }
+
+ /**
+ * Get the foreign key value from the model.
+ *
+ * @param Model $model
+ *
+ * @return string
+ */
+ protected function getForeignValueFromModel(Model $model)
+ {
+ return $this->foreignKeyIsDistinguishedName()
+ ? $model->getDn()
+ : $this->getFirstAttributeValue($model, $this->foreignKey);
+ }
+
+ /**
+ * Get the first attribute value from the model.
+ *
+ * @param Model $model
+ * @param string $attribute
+ *
+ * @return string|null
+ */
+ protected function getFirstAttributeValue(Model $model, $attribute)
+ {
+ return $model->getFirstAttribute($attribute);
+ }
+
+ /**
+ * Transforms the results by converting the models into their related.
+ *
+ * @param Collection $results
+ *
+ * @return Collection
+ */
+ protected function transformResults(Collection $results)
+ {
+ $related = [];
+
+ foreach ($this->related as $relation) {
+ $related[$relation] = $relation::$objectClasses;
+ }
+
+ return $results->transform(function (Model $entry) use ($related) {
+ $model = $this->determineModelFromRelated($entry, $related);
+
+ return class_exists($model) ? $entry->convert(new $model()) : $entry;
+ });
+ }
+
+ /**
+ * Determines if the foreign key is a distinguished name.
+ *
+ * @return bool
+ */
+ protected function foreignKeyIsDistinguishedName()
+ {
+ return in_array($this->foreignKey, ['dn', 'distinguishedname']);
+ }
+
+ /**
+ * Determines the model from the given relations.
+ *
+ * @param Model $model
+ * @param array $related
+ *
+ * @return string|bool
+ */
+ protected function determineModelFromRelated(Model $model, array $related)
+ {
+ // We must normalize all the related models object class
+ // names to the same case so we are able to properly
+ // determine the owning model from search results.
+ return array_search(
+ $this->normalizeObjectClasses($model->getObjectClasses()),
+ array_map([$this, 'normalizeObjectClasses'], $related)
+ );
+ }
+
+ /**
+ * Sort and normalize the object classes.
+ *
+ * @param array $classes
+ *
+ * @return array
+ */
+ protected function normalizeObjectClasses($classes)
+ {
+ sort($classes);
+
+ return array_map('strtolower', $classes);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Scope.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Scope.php
new file mode 100644
index 0000000..321cae3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Scope.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace LdapRecord\Models;
+
+use LdapRecord\Query\Model\Builder;
+
+interface Scope
+{
+ /**
+ * Apply the scope to the given query.
+ *
+ * @param Builder $query
+ * @param Model $model
+ *
+ * @return void
+ */
+ public function apply(Builder $query, Model $model);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/ActiveDirectory.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/ActiveDirectory.php
new file mode 100644
index 0000000..61d21ef
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/ActiveDirectory.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface ActiveDirectory extends TypeInterface
+{
+ /**
+ * Returns the models object SID key.
+ *
+ * @return string
+ */
+ public function getObjectSidKey();
+
+ /**
+ * Returns the model's hex object SID.
+ *
+ * @see https://msdn.microsoft.com/en-us/library/ms679024(v=vs.85).aspx
+ *
+ * @return string
+ */
+ public function getObjectSid();
+
+ /**
+ * Returns the model's SID.
+ *
+ * @return string|null
+ */
+ public function getConvertedSid();
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/FreeIPA.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/FreeIPA.php
new file mode 100644
index 0000000..6831318
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/FreeIPA.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface FreeIPA extends TypeInterface
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/OpenLDAP.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/OpenLDAP.php
new file mode 100644
index 0000000..e63076e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/OpenLDAP.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface OpenLDAP extends TypeInterface
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/TypeInterface.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/TypeInterface.php
new file mode 100644
index 0000000..8a45f32
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Models/Types/TypeInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Models\Types;
+
+interface TypeInterface
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ArrayCacheStore.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ArrayCacheStore.php
new file mode 100644
index 0000000..d7480a7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ArrayCacheStore.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use Psr\SimpleCache\CacheInterface;
+
+class ArrayCacheStore implements CacheInterface
+{
+ use InteractsWithTime;
+
+ /**
+ * An array of stored values.
+ *
+ * @var array
+ */
+ protected $storage = [];
+
+ /**
+ * @inheritdoc
+ */
+ public function get($key, $default = null)
+ {
+ if (! isset($this->storage[$key])) {
+ return $default;
+ }
+
+ $item = $this->storage[$key];
+
+ $expiresAt = $item['expiresAt'] ?? 0;
+
+ if ($expiresAt !== 0 && $this->currentTime() > $expiresAt) {
+ $this->delete($key);
+
+ return $default;
+ }
+
+ return $item['value'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function set($key, $value, $ttl = null)
+ {
+ $this->storage[$key] = [
+ 'value' => $value,
+ 'expiresAt' => $this->calculateExpiration($ttl),
+ ];
+
+ return true;
+ }
+
+ /**
+ * Get the expiration time of the key.
+ *
+ * @param int $seconds
+ *
+ * @return int
+ */
+ protected function calculateExpiration($seconds)
+ {
+ return $this->toTimestamp($seconds);
+ }
+
+ /**
+ * Get the UNIX timestamp for the given number of seconds.
+ *
+ * @param int $seconds
+ *
+ * @return int
+ */
+ protected function toTimestamp($seconds)
+ {
+ return $seconds > 0 ? $this->availableAt($seconds) : 0;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function delete($key)
+ {
+ unset($this->storage[$key]);
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function clear()
+ {
+ $this->storage = [];
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMultiple($keys, $default = null)
+ {
+ $values = [];
+
+ foreach ($keys as $key) {
+ $values[$key] = $this->get($key, $default);
+ }
+
+ return $values;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setMultiple($values, $ttl = null)
+ {
+ foreach ($values as $key => $value) {
+ $this->set($key, $value, $ttl);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function deleteMultiple($keys)
+ {
+ foreach ($keys as $key) {
+ $this->delete($key);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function has($key)
+ {
+ return isset($this->storage[$key]);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Builder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Builder.php
new file mode 100644
index 0000000..c75afa2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Builder.php
@@ -0,0 +1,1903 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use BadMethodCallException;
+use Closure;
+use DateTimeInterface;
+use InvalidArgumentException;
+use LdapRecord\Connection;
+use LdapRecord\Container;
+use LdapRecord\EscapesValues;
+use LdapRecord\LdapInterface;
+use LdapRecord\LdapRecordException;
+use LdapRecord\Models\Model;
+use LdapRecord\Query\Events\QueryExecuted;
+use LdapRecord\Query\Model\Builder as ModelBuilder;
+use LdapRecord\Query\Pagination\LazyPaginator;
+use LdapRecord\Query\Pagination\Paginator;
+use LdapRecord\Support\Arr;
+use LdapRecord\Utilities;
+
+class Builder
+{
+ use EscapesValues;
+
+ /**
+ * The selected columns to retrieve on the query.
+ *
+ * @var array
+ */
+ public $columns;
+
+ /**
+ * The query filters.
+ *
+ * @var array
+ */
+ public $filters = [
+ 'and' => [],
+ 'or' => [],
+ 'raw' => [],
+ ];
+
+ /**
+ * The LDAP server controls to be sent.
+ *
+ * @var array
+ */
+ public $controls = [];
+
+ /**
+ * The size limit of the query.
+ *
+ * @var int
+ */
+ public $limit = 0;
+
+ /**
+ * Determines whether the current query is paginated.
+ *
+ * @var bool
+ */
+ public $paginated = false;
+
+ /**
+ * The distinguished name to perform searches upon.
+ *
+ * @var string|null
+ */
+ protected $dn;
+
+ /**
+ * The base distinguished name to perform searches inside.
+ *
+ * @var string|null
+ */
+ protected $baseDn;
+
+ /**
+ * The default query type.
+ *
+ * @var string
+ */
+ protected $type = 'search';
+
+ /**
+ * Determines whether the query is nested.
+ *
+ * @var bool
+ */
+ protected $nested = false;
+
+ /**
+ * Determines whether the query should be cached.
+ *
+ * @var bool
+ */
+ protected $caching = false;
+
+ /**
+ * How long the query should be cached until.
+ *
+ * @var DateTimeInterface|null
+ */
+ protected $cacheUntil = null;
+
+ /**
+ * Determines whether the query cache must be flushed.
+ *
+ * @var bool
+ */
+ protected $flushCache = false;
+
+ /**
+ * The current connection instance.
+ *
+ * @var Connection
+ */
+ protected $connection;
+
+ /**
+ * The current grammar instance.
+ *
+ * @var Grammar
+ */
+ protected $grammar;
+
+ /**
+ * The current cache instance.
+ *
+ * @var Cache|null
+ */
+ protected $cache;
+
+ /**
+ * Constructor.
+ *
+ * @param Connection $connection
+ */
+ public function __construct(Connection $connection)
+ {
+ $this->connection = $connection;
+ $this->grammar = new Grammar();
+ }
+
+ /**
+ * Set the current connection.
+ *
+ * @param Connection $connection
+ *
+ * @return $this
+ */
+ public function setConnection(Connection $connection)
+ {
+ $this->connection = $connection;
+
+ return $this;
+ }
+
+ /**
+ * Set the current filter grammar.
+ *
+ * @param Grammar $grammar
+ *
+ * @return $this
+ */
+ public function setGrammar(Grammar $grammar)
+ {
+ $this->grammar = $grammar;
+
+ return $this;
+ }
+
+ /**
+ * Set the cache to store query results.
+ *
+ * @param Cache|null $cache
+ *
+ * @return $this
+ */
+ public function setCache(Cache $cache = null)
+ {
+ $this->cache = $cache;
+
+ return $this;
+ }
+
+ /**
+ * Returns a new Query Builder instance.
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function newInstance($baseDn = null)
+ {
+ // We'll set the base DN of the new Builder so
+ // developers don't need to do this manually.
+ $dn = is_null($baseDn) ? $this->getDn() : $baseDn;
+
+ return (new static($this->connection))->setDn($dn);
+ }
+
+ /**
+ * Returns a new nested Query Builder instance.
+ *
+ * @param Closure|null $closure
+ *
+ * @return $this
+ */
+ public function newNestedInstance(Closure $closure = null)
+ {
+ $query = $this->newInstance()->nested();
+
+ if ($closure) {
+ $closure($query);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Executes the LDAP query.
+ *
+ * @param string|array $columns
+ *
+ * @return Collection|array
+ */
+ public function get($columns = ['*'])
+ {
+ return $this->onceWithColumns(Arr::wrap($columns), function () {
+ return $this->query($this->getQuery());
+ });
+ }
+
+ /**
+ * Execute the given callback while selecting the given columns.
+ *
+ * After running the callback, the columns are reset to the original value.
+ *
+ * @param array $columns
+ * @param callable $callback
+ *
+ * @return mixed
+ */
+ protected function onceWithColumns($columns, $callback)
+ {
+ $original = $this->columns;
+
+ if (is_null($original)) {
+ $this->columns = $columns;
+ }
+
+ $result = $callback();
+
+ $this->columns = $original;
+
+ return $result;
+ }
+
+ /**
+ * Compiles and returns the current query string.
+ *
+ * @return string
+ */
+ public function getQuery()
+ {
+ // We need to ensure we have at least one filter, as
+ // no query results will be returned otherwise.
+ if (count(array_filter($this->filters)) === 0) {
+ $this->whereHas('objectclass');
+ }
+
+ return $this->grammar->compile($this);
+ }
+
+ /**
+ * Returns the unescaped query.
+ *
+ * @return string
+ */
+ public function getUnescapedQuery()
+ {
+ return Utilities::unescape($this->getQuery());
+ }
+
+ /**
+ * Returns the current Grammar instance.
+ *
+ * @return Grammar
+ */
+ public function getGrammar()
+ {
+ return $this->grammar;
+ }
+
+ /**
+ * Returns the current Cache instance.
+ *
+ * @return Cache|null
+ */
+ public function getCache()
+ {
+ return $this->cache;
+ }
+
+ /**
+ * Returns the current Connection instance.
+ *
+ * @return Connection
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Returns the query type.
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Set the base distinguished name of the query.
+ *
+ * @param Model|string $dn
+ *
+ * @return $this
+ */
+ public function setBaseDn($dn)
+ {
+ $this->baseDn = $this->substituteBaseInDn($dn);
+
+ return $this;
+ }
+
+ /**
+ * Get the base distinguished name of the query.
+ *
+ * @return string|null
+ */
+ public function getBaseDn()
+ {
+ return $this->baseDn;
+ }
+
+ /**
+ * Get the distinguished name of the query.
+ *
+ * @return string
+ */
+ public function getDn()
+ {
+ return $this->dn;
+ }
+
+ /**
+ * Set the distinguished name for the query.
+ *
+ * @param string|Model|null $dn
+ *
+ * @return $this
+ */
+ public function setDn($dn = null)
+ {
+ $this->dn = $this->substituteBaseInDn($dn);
+
+ return $this;
+ }
+
+ /**
+ * Substitute the base DN string template for the current base.
+ *
+ * @param Model|string $dn
+ *
+ * @return string
+ */
+ protected function substituteBaseInDn($dn)
+ {
+ return str_replace(
+ '{base}',
+ $this->baseDn,
+ $dn instanceof Model ? $dn->getDn() : $dn
+ );
+ }
+
+ /**
+ * Alias for setting the distinguished name for the query.
+ *
+ * @param string|Model|null $dn
+ *
+ * @return $this
+ */
+ public function in($dn = null)
+ {
+ return $this->setDn($dn);
+ }
+
+ /**
+ * Set the size limit of the current query.
+ *
+ * @param int $limit
+ *
+ * @return $this
+ */
+ public function limit($limit = 0)
+ {
+ $this->limit = $limit;
+
+ return $this;
+ }
+
+ /**
+ * Returns a new query for the given model.
+ *
+ * @param Model $model
+ *
+ * @return ModelBuilder
+ */
+ public function model(Model $model)
+ {
+ return $model->newQueryBuilder($this->connection)
+ ->setCache($this->connection->getCache())
+ ->setBaseDn($this->baseDn)
+ ->setModel($model);
+ }
+
+ /**
+ * Performs the specified query on the current LDAP connection.
+ *
+ * @param string $query
+ *
+ * @return Collection|array
+ */
+ public function query($query)
+ {
+ $start = microtime(true);
+
+ // Here we will create the execution callback. This allows us
+ // to only execute an LDAP request if caching is disabled
+ // or if no cache of the given query exists yet.
+ $callback = function () use ($query) {
+ return $this->parse($this->run($query));
+ };
+
+ $results = $this->getCachedResponse($query, $callback);
+
+ $this->logQuery($this, $this->type, $this->getElapsedTime($start));
+
+ return $this->process($results);
+ }
+
+ /**
+ * Paginates the current LDAP query.
+ *
+ * @param int $pageSize
+ * @param bool $isCritical
+ *
+ * @return Collection|array
+ */
+ public function paginate($pageSize = 1000, $isCritical = false)
+ {
+ $this->paginated = true;
+
+ $start = microtime(true);
+
+ $query = $this->getQuery();
+
+ // Here we will create the pagination callback. This allows us
+ // to only execute an LDAP request if caching is disabled
+ // or if no cache of the given query exists yet.
+ $callback = function () use ($query, $pageSize, $isCritical) {
+ return $this->runPaginate($query, $pageSize, $isCritical);
+ };
+
+ $pages = $this->getCachedResponse($query, $callback);
+
+ $this->logQuery($this, 'paginate', $this->getElapsedTime($start));
+
+ return $this->process($pages);
+ }
+
+ /**
+ * Runs the paginate operation with the given filter.
+ *
+ * @param string $filter
+ * @param int $perPage
+ * @param bool $isCritical
+ *
+ * @return array
+ */
+ protected function runPaginate($filter, $perPage, $isCritical)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
+ return (new Paginator($this, $filter, $perPage, $isCritical))->execute($ldap);
+ });
+ }
+
+ /**
+ * Chunk the results of a paginated LDAP query.
+ *
+ * @param int $pageSize
+ * @param Closure $callback
+ * @param bool $isCritical
+ *
+ * @return void
+ */
+ public function chunk($pageSize, Closure $callback, $isCritical = false)
+ {
+ $start = microtime(true);
+
+ $query = $this->getQuery();
+
+ foreach ($this->runChunk($query, $pageSize, $isCritical) as $chunk) {
+ $callback($this->process($chunk));
+ }
+
+ $this->logQuery($this, 'chunk', $this->getElapsedTime($start));
+ }
+
+ /**
+ * Runs the chunk operation with the given filter.
+ *
+ * @param string $filter
+ * @param int $perPage
+ * @param bool $isCritical
+ *
+ * @return array
+ */
+ protected function runChunk($filter, $perPage, $isCritical)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($filter, $perPage, $isCritical) {
+ return (new LazyPaginator($this, $filter, $perPage, $isCritical))->execute($ldap);
+ });
+ }
+
+ /**
+ * Processes and converts the given LDAP results into models.
+ *
+ * @param array $results
+ *
+ * @return array
+ */
+ protected function process(array $results)
+ {
+ unset($results['count']);
+
+ return $this->paginated ? $this->flattenPages($results) : $results;
+ }
+
+ /**
+ * Flattens LDAP paged results into a single array.
+ *
+ * @param array $pages
+ *
+ * @return array
+ */
+ protected function flattenPages(array $pages)
+ {
+ $records = [];
+
+ foreach ($pages as $page) {
+ unset($page['count']);
+
+ $records = array_merge($records, $page);
+ }
+
+ return $records;
+ }
+
+ /**
+ * Get the cached response or execute and cache the callback value.
+ *
+ * @param string $query
+ * @param Closure $callback
+ *
+ * @return mixed
+ */
+ protected function getCachedResponse($query, Closure $callback)
+ {
+ // If caching is enabled and we have a cache instance available,
+ // we will try to retrieve the cached results instead.
+ if ($this->caching && $this->cache) {
+ $key = $this->getCacheKey($query);
+
+ if ($this->flushCache) {
+ $this->cache->delete($key);
+ }
+
+ return $this->cache->remember($key, $this->cacheUntil, $callback);
+ }
+
+ // Otherwise, we will simply execute the callback.
+ return $callback();
+ }
+
+ /**
+ * Runs the query operation with the given filter.
+ *
+ * @param string $filter
+ *
+ * @return resource
+ */
+ public function run($filter)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($filter) {
+ // We will avoid setting the controls during any pagination
+ // requests as it will clear the cookie we need to send
+ // to the server upon retrieving every page.
+ if (! $this->paginated) {
+ // Before running the query, we will set the LDAP server controls. This
+ // allows the controls to be automatically reset upon each new query
+ // that is conducted on the same connection during each request.
+ $ldap->setOption(LDAP_OPT_SERVER_CONTROLS, $this->controls);
+ }
+
+ return $ldap->{$this->type}(
+ $this->dn ?? $this->baseDn,
+ $filter,
+ $this->getSelects(),
+ $onlyAttributes = false,
+ $this->limit
+ );
+ });
+ }
+
+ /**
+ * Parses the given LDAP resource by retrieving its entries.
+ *
+ * @param resource $resource
+ *
+ * @return array
+ */
+ public function parse($resource)
+ {
+ if (! $resource) {
+ return [];
+ }
+
+ return $this->connection->run(function (LdapInterface $ldap) use ($resource) {
+ $entries = $ldap->getEntries($resource);
+
+ // Free up memory.
+ if (is_resource($resource)) {
+ $ldap->freeResult($resource);
+ }
+
+ return $entries;
+ });
+ }
+
+ /**
+ * Returns the cache key.
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ protected function getCacheKey($query)
+ {
+ $host = $this->connection->getLdapConnection()->getHost();
+
+ $key = $host
+ .$this->type
+ .$this->getDn()
+ .$query
+ .implode($this->getSelects())
+ .$this->limit
+ .$this->paginated;
+
+ return md5($key);
+ }
+
+ /**
+ * Returns the first entry in a search result.
+ *
+ * @param array|string $columns
+ *
+ * @return Model|null
+ */
+ public function first($columns = ['*'])
+ {
+ return Arr::get($this->limit(1)->get($columns), 0);
+ }
+
+ /**
+ * Returns the first entry in a search result.
+ *
+ * If no entry is found, an exception is thrown.
+ *
+ * @param array|string $columns
+ *
+ * @throws ObjectNotFoundException
+ *
+ * @return Model|static
+ */
+ public function firstOrFail($columns = ['*'])
+ {
+ if (! $record = $this->first($columns)) {
+ $this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
+ }
+
+ return $record;
+ }
+
+ /**
+ * Throws a not found exception.
+ *
+ * @param string $query
+ * @param string $dn
+ *
+ * @throws ObjectNotFoundException
+ */
+ protected function throwNotFoundException($query, $dn)
+ {
+ throw ObjectNotFoundException::forQuery($query, $dn);
+ }
+
+ /**
+ * Finds a record by the specified attribute and value.
+ *
+ * @param string $attribute
+ * @param string $value
+ * @param array|string $columns
+ *
+ * @return Model|static|null
+ */
+ public function findBy($attribute, $value, $columns = ['*'])
+ {
+ try {
+ return $this->findByOrFail($attribute, $value, $columns);
+ } catch (ObjectNotFoundException $e) {
+ return;
+ }
+ }
+
+ /**
+ * Finds a record by the specified attribute and value.
+ *
+ * If no record is found an exception is thrown.
+ *
+ * @param string $attribute
+ * @param string $value
+ * @param array|string $columns
+ *
+ * @throws ObjectNotFoundException
+ *
+ * @return Model
+ */
+ public function findByOrFail($attribute, $value, $columns = ['*'])
+ {
+ return $this->whereEquals($attribute, $value)->firstOrFail($columns);
+ }
+
+ /**
+ * Find many records by distinguished name.
+ *
+ * @param array $dns
+ * @param array $columns
+ *
+ * @return array|Collection
+ */
+ public function findMany($dns, $columns = ['*'])
+ {
+ if (empty($dns)) {
+ return $this->process([]);
+ }
+
+ $objects = [];
+
+ foreach ($dns as $dn) {
+ if (! is_null($object = $this->find($dn, $columns))) {
+ $objects[] = $object;
+ }
+ }
+
+ return $this->process($objects);
+ }
+
+ /**
+ * Finds many records by the specified attribute.
+ *
+ * @param string $attribute
+ * @param array $values
+ * @param array $columns
+ *
+ * @return Collection
+ */
+ public function findManyBy($attribute, array $values = [], $columns = ['*'])
+ {
+ $query = $this->select($columns);
+
+ foreach ($values as $value) {
+ $query->orWhere([$attribute => $value]);
+ }
+
+ return $query->get();
+ }
+
+ /**
+ * Finds a record by its distinguished name.
+ *
+ * @param string|array $dn
+ * @param array|string $columns
+ *
+ * @return Model|static|array|Collection|null
+ */
+ public function find($dn, $columns = ['*'])
+ {
+ if (is_array($dn)) {
+ return $this->findMany($dn, $columns);
+ }
+
+ try {
+ return $this->findOrFail($dn, $columns);
+ } catch (ObjectNotFoundException $e) {
+ return;
+ }
+ }
+
+ /**
+ * Finds a record by its distinguished name.
+ *
+ * Fails upon no records returned.
+ *
+ * @param string $dn
+ * @param array|string $columns
+ *
+ * @throws ObjectNotFoundException
+ *
+ * @return Model|static
+ */
+ public function findOrFail($dn, $columns = ['*'])
+ {
+ return $this->setDn($dn)
+ ->read()
+ ->whereHas('objectclass')
+ ->firstOrFail($columns);
+ }
+
+ /**
+ * Adds the inserted fields to query on the current LDAP connection.
+ *
+ * @param array|string $columns
+ *
+ * @return $this
+ */
+ public function select($columns = ['*'])
+ {
+ $columns = is_array($columns) ? $columns : func_get_args();
+
+ if (! empty($columns)) {
+ $this->columns = $columns;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a new select column to the query.
+ *
+ * @param array|mixed $column
+ *
+ * @return $this
+ */
+ public function addSelect($column)
+ {
+ $column = is_array($column) ? $column : func_get_args();
+
+ $this->columns = array_merge((array) $this->columns, $column);
+
+ return $this;
+ }
+
+ /**
+ * Adds a raw filter to the current query.
+ *
+ * @param array|string $filters
+ *
+ * @return $this
+ */
+ public function rawFilter($filters = [])
+ {
+ $filters = is_array($filters) ? $filters : func_get_args();
+
+ foreach ($filters as $filter) {
+ $this->filters['raw'][] = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds a nested 'and' filter to the current query.
+ *
+ * @param Closure $closure
+ *
+ * @return $this
+ */
+ public function andFilter(Closure $closure)
+ {
+ $query = $this->newNestedInstance($closure);
+
+ return $this->rawFilter(
+ $this->grammar->compileAnd($query->getQuery())
+ );
+ }
+
+ /**
+ * Adds a nested 'or' filter to the current query.
+ *
+ * @param Closure $closure
+ *
+ * @return $this
+ */
+ public function orFilter(Closure $closure)
+ {
+ $query = $this->newNestedInstance($closure);
+
+ return $this->rawFilter(
+ $this->grammar->compileOr($query->getQuery())
+ );
+ }
+
+ /**
+ * Adds a nested 'not' filter to the current query.
+ *
+ * @param Closure $closure
+ *
+ * @return $this
+ */
+ public function notFilter(Closure $closure)
+ {
+ $query = $this->newNestedInstance($closure);
+
+ return $this->rawFilter(
+ $this->grammar->compileNot($query->getQuery())
+ );
+ }
+
+ /**
+ * Adds a where clause to the current query.
+ *
+ * @param string|array $field
+ * @param string $operator
+ * @param string $value
+ * @param string $boolean
+ * @param bool $raw
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return $this
+ */
+ public function where($field, $operator = null, $value = null, $boolean = 'and', $raw = false)
+ {
+ if (is_array($field)) {
+ // If the field is an array, we will assume we have been
+ // provided with an array of key-value pairs and can
+ // add them each as their own seperate where clause.
+ return $this->addArrayOfWheres($field, $boolean, $raw);
+ }
+
+ // If we have been provided with two arguments not a "has" or
+ // "not has" operator, we'll assume the developer is creating
+ // an "equals" clause and set the proper operator in place.
+ if (func_num_args() === 2 && ! in_array($operator, ['*', '!*'])) {
+ [$value, $operator] = [$operator, '='];
+ }
+
+ if (! in_array($operator, $this->grammar->getOperators())) {
+ throw new InvalidArgumentException("Invalid LDAP filter operator [$operator]");
+ }
+
+ // We'll escape the value if raw isn't requested.
+ $value = $this->prepareWhereValue($field, $value, $raw);
+
+ $field = $this->escape($field)->both()->get();
+
+ $this->addFilter($boolean, compact('field', 'operator', 'value'));
+
+ return $this;
+ }
+
+ /**
+ * Prepare the value for being queried.
+ *
+ * @param string $field
+ * @param string $value
+ * @param bool $raw
+ *
+ * @return string
+ */
+ protected function prepareWhereValue($field, $value, $raw = false)
+ {
+ return $raw ? $value : $this->escape($value);
+ }
+
+ /**
+ * Adds a raw where clause to the current query.
+ *
+ * Values given to this method are not escaped.
+ *
+ * @param string|array $field
+ * @param string $operator
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereRaw($field, $operator = null, $value = null)
+ {
+ return $this->where($field, $operator, $value, 'and', true);
+ }
+
+ /**
+ * Adds a 'where equals' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereEquals($field, $value)
+ {
+ return $this->where($field, '=', $value);
+ }
+
+ /**
+ * Adds a 'where not equals' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereNotEquals($field, $value)
+ {
+ return $this->where($field, '!', $value);
+ }
+
+ /**
+ * Adds a 'where approximately equals' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereApproximatelyEquals($field, $value)
+ {
+ return $this->where($field, '~=', $value);
+ }
+
+ /**
+ * Adds a 'where has' clause to the current query.
+ *
+ * @param string $field
+ *
+ * @return $this
+ */
+ public function whereHas($field)
+ {
+ return $this->where($field, '*');
+ }
+
+ /**
+ * Adds a 'where not has' clause to the current query.
+ *
+ * @param string $field
+ *
+ * @return $this
+ */
+ public function whereNotHas($field)
+ {
+ return $this->where($field, '!*');
+ }
+
+ /**
+ * Adds a 'where contains' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereContains($field, $value)
+ {
+ return $this->where($field, 'contains', $value);
+ }
+
+ /**
+ * Adds a 'where contains' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereNotContains($field, $value)
+ {
+ return $this->where($field, 'not_contains', $value);
+ }
+
+ /**
+ * Query for entries that match any of the values provided for the given field.
+ *
+ * @param string $field
+ * @param array $values
+ *
+ * @return $this
+ */
+ public function whereIn($field, array $values)
+ {
+ return $this->orFilter(function (self $query) use ($field, $values) {
+ foreach ($values as $value) {
+ $query->whereEquals($field, $value);
+ }
+ });
+ }
+
+ /**
+ * Adds a 'between' clause to the current query.
+ *
+ * @param string $field
+ * @param array $values
+ *
+ * @return $this
+ */
+ public function whereBetween($field, array $values)
+ {
+ return $this->where([
+ [$field, '>=', $values[0]],
+ [$field, '<=', $values[1]],
+ ]);
+ }
+
+ /**
+ * Adds a 'where starts with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereStartsWith($field, $value)
+ {
+ return $this->where($field, 'starts_with', $value);
+ }
+
+ /**
+ * Adds a 'where *not* starts with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereNotStartsWith($field, $value)
+ {
+ return $this->where($field, 'not_starts_with', $value);
+ }
+
+ /**
+ * Adds a 'where ends with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereEndsWith($field, $value)
+ {
+ return $this->where($field, 'ends_with', $value);
+ }
+
+ /**
+ * Adds a 'where *not* ends with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function whereNotEndsWith($field, $value)
+ {
+ return $this->where($field, 'not_ends_with', $value);
+ }
+
+ /**
+ * Only include deleted models in the results.
+ *
+ * @return $this
+ */
+ public function whereDeleted()
+ {
+ return $this->withDeleted()->whereEquals('isDeleted', 'TRUE');
+ }
+
+ /**
+ * Set the LDAP control option to include deleted LDAP models.
+ *
+ * @return $this
+ */
+ public function withDeleted()
+ {
+ return $this->addControl(LdapInterface::OID_SERVER_SHOW_DELETED, $isCritical = true);
+ }
+
+ /**
+ * Add a server control to the query.
+ *
+ * @param string $oid
+ * @param bool $isCritical
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function addControl($oid, $isCritical = false, $value = null)
+ {
+ $this->controls[$oid] = compact('oid', 'isCritical', 'value');
+
+ return $this;
+ }
+
+ /**
+ * Determine if the server control exists on the query.
+ *
+ * @param string $oid
+ *
+ * @return bool
+ */
+ public function hasControl($oid)
+ {
+ return array_key_exists($oid, $this->controls);
+ }
+
+ /**
+ * Adds an 'or where' clause to the current query.
+ *
+ * @param array|string $field
+ * @param string|null $operator
+ * @param string|null $value
+ *
+ * @return $this
+ */
+ public function orWhere($field, $operator = null, $value = null)
+ {
+ return $this->where($field, $operator, $value, 'or');
+ }
+
+ /**
+ * Adds a raw or where clause to the current query.
+ *
+ * Values given to this method are not escaped.
+ *
+ * @param string $field
+ * @param string $operator
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereRaw($field, $operator = null, $value = null)
+ {
+ return $this->where($field, $operator, $value, 'or', true);
+ }
+
+ /**
+ * Adds an 'or where has' clause to the current query.
+ *
+ * @param string $field
+ *
+ * @return $this
+ */
+ public function orWhereHas($field)
+ {
+ return $this->orWhere($field, '*');
+ }
+
+ /**
+ * Adds a 'where not has' clause to the current query.
+ *
+ * @param string $field
+ *
+ * @return $this
+ */
+ public function orWhereNotHas($field)
+ {
+ return $this->orWhere($field, '!*');
+ }
+
+ /**
+ * Adds an 'or where equals' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereEquals($field, $value)
+ {
+ return $this->orWhere($field, '=', $value);
+ }
+
+ /**
+ * Adds an 'or where not equals' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereNotEquals($field, $value)
+ {
+ return $this->orWhere($field, '!', $value);
+ }
+
+ /**
+ * Adds a 'or where approximately equals' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereApproximatelyEquals($field, $value)
+ {
+ return $this->orWhere($field, '~=', $value);
+ }
+
+ /**
+ * Adds an 'or where contains' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereContains($field, $value)
+ {
+ return $this->orWhere($field, 'contains', $value);
+ }
+
+ /**
+ * Adds an 'or where *not* contains' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereNotContains($field, $value)
+ {
+ return $this->orWhere($field, 'not_contains', $value);
+ }
+
+ /**
+ * Adds an 'or where starts with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereStartsWith($field, $value)
+ {
+ return $this->orWhere($field, 'starts_with', $value);
+ }
+
+ /**
+ * Adds an 'or where *not* starts with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereNotStartsWith($field, $value)
+ {
+ return $this->orWhere($field, 'not_starts_with', $value);
+ }
+
+ /**
+ * Adds an 'or where ends with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereEndsWith($field, $value)
+ {
+ return $this->orWhere($field, 'ends_with', $value);
+ }
+
+ /**
+ * Adds an 'or where *not* ends with' clause to the current query.
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function orWhereNotEndsWith($field, $value)
+ {
+ return $this->orWhere($field, 'not_ends_with', $value);
+ }
+
+ /**
+ * Adds a filter binding onto the current query.
+ *
+ * @param string $type The type of filter to add.
+ * @param array $bindings The bindings of the filter.
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return $this
+ */
+ public function addFilter($type, array $bindings)
+ {
+ if (! array_key_exists($type, $this->filters)) {
+ throw new InvalidArgumentException("Filter type: [$type] is invalid.");
+ }
+
+ // Each filter clause require key bindings to be set. We
+ // will validate this here to ensure all of them have
+ // been provided, or throw an exception otherwise.
+ if ($missing = $this->missingBindingKeys($bindings)) {
+ $keys = implode(', ', $missing);
+
+ throw new InvalidArgumentException("Invalid filter bindings. Missing: [$keys] keys.");
+ }
+
+ $this->filters[$type][] = $bindings;
+
+ return $this;
+ }
+
+ /**
+ * Extract any missing required binding keys.
+ *
+ * @param array $bindings
+ *
+ * @return array
+ */
+ protected function missingBindingKeys($bindings)
+ {
+ $required = array_flip(['field', 'operator', 'value']);
+
+ $existing = array_intersect_key($required, $bindings);
+
+ return array_keys(array_diff_key($required, $existing));
+ }
+
+ /**
+ * Get all the filters on the query.
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+
+ /**
+ * Clear the query filters.
+ *
+ * @return $this
+ */
+ public function clearFilters()
+ {
+ foreach (array_keys($this->filters) as $type) {
+ $this->filters[$type] = [];
+ }
+
+ return $this;
+ }
+
+ /**
+ * Determine if the query has attributes selected.
+ *
+ * @return bool
+ */
+ public function hasSelects()
+ {
+ return count($this->columns) > 0;
+ }
+
+ /**
+ * Get the attributes to select on the search.
+ *
+ * @return array
+ */
+ public function getSelects()
+ {
+ $selects = $this->columns ?? ['*'];
+
+ if (in_array('*', $selects)) {
+ return $selects;
+ }
+
+ if (in_array('objectclass', $selects)) {
+ return $selects;
+ }
+
+ // If the * character is not provided in the selected columns,
+ // we need to ensure we always select the object class, as
+ // this is used for constructing models properly.
+ $selects[] = 'objectclass';
+
+ return $selects;
+ }
+
+ /**
+ * Set the query to search on the base distinguished name.
+ *
+ * This will result in one record being returned.
+ *
+ * @return $this
+ */
+ public function read()
+ {
+ $this->type = 'read';
+
+ return $this;
+ }
+
+ /**
+ * Set the query to search one level on the base distinguished name.
+ *
+ * @return $this
+ */
+ public function listing()
+ {
+ $this->type = 'listing';
+
+ return $this;
+ }
+
+ /**
+ * Set the query to search the entire directory on the base distinguished name.
+ *
+ * @return $this
+ */
+ public function recursive()
+ {
+ $this->type = 'search';
+
+ return $this;
+ }
+
+ /**
+ * Whether to mark the current query as nested.
+ *
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function nested($nested = true)
+ {
+ $this->nested = (bool) $nested;
+
+ return $this;
+ }
+
+ /**
+ * Enables caching on the current query until the given date.
+ *
+ * If flushing is enabled, the query cache will be flushed and then re-cached.
+ *
+ * @param DateTimeInterface $until When to expire the query cache.
+ * @param bool $flush Whether to force-flush the query cache.
+ *
+ * @return $this
+ */
+ public function cache(DateTimeInterface $until = null, $flush = false)
+ {
+ $this->caching = true;
+ $this->cacheUntil = $until;
+ $this->flushCache = $flush;
+
+ return $this;
+ }
+
+ /**
+ * Determine if the query is nested.
+ *
+ * @return bool
+ */
+ public function isNested()
+ {
+ return $this->nested === true;
+ }
+
+ /**
+ * Determine whether the query is paginated.
+ *
+ * @return bool
+ */
+ public function isPaginated()
+ {
+ return $this->paginated;
+ }
+
+ /**
+ * Insert an entry into the directory.
+ *
+ * @param string $dn
+ * @param array $attributes
+ *
+ * @throws LdapRecordException
+ *
+ * @return bool
+ */
+ public function insert($dn, array $attributes)
+ {
+ if (empty($dn)) {
+ throw new LdapRecordException('A new LDAP object must have a distinguished name (dn).');
+ }
+
+ if (! array_key_exists('objectclass', $attributes)) {
+ throw new LdapRecordException(
+ 'A new LDAP object must contain at least one object class (objectclass) to be created.'
+ );
+ }
+
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+ return $ldap->add($dn, $attributes);
+ });
+ }
+
+ /**
+ * Create attributes on the entry in the directory.
+ *
+ * @param string $dn
+ * @param array $attributes
+ *
+ * @return bool
+ */
+ public function insertAttributes($dn, array $attributes)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+ return $ldap->modAdd($dn, $attributes);
+ });
+ }
+
+ /**
+ * Update the entry with the given modifications.
+ *
+ * @param string $dn
+ * @param array $modifications
+ *
+ * @return bool
+ */
+ public function update($dn, array $modifications)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn, $modifications) {
+ return $ldap->modifyBatch($dn, $modifications);
+ });
+ }
+
+ /**
+ * Update an entries attribute in the directory.
+ *
+ * @param string $dn
+ * @param array $attributes
+ *
+ * @return bool
+ */
+ public function updateAttributes($dn, array $attributes)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+ return $ldap->modReplace($dn, $attributes);
+ });
+ }
+
+ /**
+ * Delete an entry from the directory.
+ *
+ * @param string $dn
+ *
+ * @return bool
+ */
+ public function delete($dn)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn) {
+ return $ldap->delete($dn);
+ });
+ }
+
+ /**
+ * Delete attributes on the entry in the directory.
+ *
+ * @param string $dn
+ * @param array $attributes
+ *
+ * @return bool
+ */
+ public function deleteAttributes($dn, array $attributes)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn, $attributes) {
+ return $ldap->modDelete($dn, $attributes);
+ });
+ }
+
+ /**
+ * Rename an entry in the directory.
+ *
+ * @param string $dn
+ * @param string $rdn
+ * @param string $newParentDn
+ * @param bool $deleteOldRdn
+ *
+ * @return bool
+ */
+ public function rename($dn, $rdn, $newParentDn, $deleteOldRdn = true)
+ {
+ return $this->connection->run(function (LdapInterface $ldap) use ($dn, $rdn, $newParentDn, $deleteOldRdn) {
+ return $ldap->rename($dn, $rdn, $newParentDn, $deleteOldRdn);
+ });
+ }
+
+ /**
+ * Handle dynamic method calls on the query builder.
+ *
+ * @param string $method
+ * @param array $parameters
+ *
+ * @throws BadMethodCallException
+ *
+ * @return mixed
+ */
+ public function __call($method, $parameters)
+ {
+ // If the beginning of the method being called contains
+ // 'where', we will assume a dynamic 'where' clause is
+ // being performed and pass the parameters to it.
+ if (substr($method, 0, 5) === 'where') {
+ return $this->dynamicWhere($method, $parameters);
+ }
+
+ throw new BadMethodCallException(sprintf(
+ 'Call to undefined method %s::%s()',
+ static::class,
+ $method
+ ));
+ }
+
+ /**
+ * Handles dynamic "where" clauses to the query.
+ *
+ * @param string $method
+ * @param array $parameters
+ *
+ * @return $this
+ */
+ public function dynamicWhere($method, $parameters)
+ {
+ $finder = substr($method, 5);
+
+ $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ // The connector variable will determine which connector will be used for the
+ // query condition. We will change it as we come across new boolean values
+ // in the dynamic method strings, which could contain a number of these.
+ $connector = 'and';
+
+ $index = 0;
+
+ foreach ($segments as $segment) {
+ // If the segment is not a boolean connector, we can assume it is a column's name
+ // and we will add it to the query as a new constraint as a where clause, then
+ // we can keep iterating through the dynamic method string's segments again.
+ if ($segment != 'And' && $segment != 'Or') {
+ $this->addDynamic($segment, $connector, $parameters, $index);
+
+ $index++;
+ }
+
+ // Otherwise, we will store the connector so we know how the next where clause we
+ // find in the query should be connected to the previous ones, meaning we will
+ // have the proper boolean connector to connect the next where clause found.
+ else {
+ $connector = $segment;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds an array of wheres to the current query.
+ *
+ * @param array $wheres
+ * @param string $boolean
+ * @param bool $raw
+ *
+ * @return $this
+ */
+ protected function addArrayOfWheres($wheres, $boolean, $raw)
+ {
+ foreach ($wheres as $key => $value) {
+ if (is_numeric($key) && is_array($value)) {
+ // If the key is numeric and the value is an array, we'll
+ // assume we've been given an array with conditionals.
+ [$field, $condition] = $value;
+
+ // Since a value is optional for some conditionals, we will
+ // try and retrieve the third parameter from the array,
+ // but is entirely optional.
+ $value = Arr::get($value, 2);
+
+ $this->where($field, $condition, $value, $boolean);
+ } else {
+ // If the value is not an array, we will assume an equals clause.
+ $this->where($key, '=', $value, $boolean, $raw);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a single dynamic where clause statement to the query.
+ *
+ * @param string $segment
+ * @param string $connector
+ * @param array $parameters
+ * @param int $index
+ *
+ * @return void
+ */
+ protected function addDynamic($segment, $connector, $parameters, $index)
+ {
+ // If no parameters were given to the dynamic where clause,
+ // we can assume a "has" attribute filter is being added.
+ if (count($parameters) === 0) {
+ $this->where(strtolower($segment), '*', null, strtolower($connector));
+ } else {
+ $this->where(strtolower($segment), '=', $parameters[$index], strtolower($connector));
+ }
+ }
+
+ /**
+ * Logs the given executed query information by firing its query event.
+ *
+ * @param Builder $query
+ * @param string $type
+ * @param null|float $time
+ *
+ * @return void
+ */
+ protected function logQuery($query, $type, $time = null)
+ {
+ $args = [$query, $time];
+
+ switch ($type) {
+ case 'listing':
+ $event = new Events\Listing(...$args);
+ break;
+ case 'read':
+ $event = new Events\Read(...$args);
+ break;
+ case 'chunk':
+ $event = new Events\Chunk(...$args);
+ break;
+ case 'paginate':
+ $event = new Events\Paginate(...$args);
+ break;
+ default:
+ $event = new Events\Search(...$args);
+ break;
+ }
+
+ $this->fireQueryEvent($event);
+ }
+
+ /**
+ * Fires the given query event.
+ *
+ * @param QueryExecuted $event
+ *
+ * @return void
+ */
+ protected function fireQueryEvent(QueryExecuted $event)
+ {
+ Container::getInstance()->getEventDispatcher()->fire($event);
+ }
+
+ /**
+ * Get the elapsed time since a given starting point.
+ *
+ * @param int $start
+ *
+ * @return float
+ */
+ protected function getElapsedTime($start)
+ {
+ return round((microtime(true) - $start) * 1000, 2);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Cache.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Cache.php
new file mode 100644
index 0000000..dfbf8cd
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Cache.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use Closure;
+use DateInterval;
+use DateTimeInterface;
+use Psr\SimpleCache\CacheInterface;
+
+class Cache
+{
+ use InteractsWithTime;
+
+ /**
+ * The cache driver.
+ *
+ * @var CacheInterface
+ */
+ protected $store;
+
+ /**
+ * Constructor.
+ *
+ * @param CacheInterface $store
+ */
+ public function __construct(CacheInterface $store)
+ {
+ $this->store = $store;
+ }
+
+ /**
+ * Get an item from the cache.
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function get($key)
+ {
+ return $this->store->get($key);
+ }
+
+ /**
+ * Store an item in the cache.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param DateTimeInterface|DateInterval|int|null $ttl
+ *
+ * @return bool
+ */
+ public function put($key, $value, $ttl = null)
+ {
+ $seconds = $this->secondsUntil($ttl);
+
+ if ($seconds <= 0) {
+ return $this->delete($key);
+ }
+
+ return $this->store->set($key, $value, $seconds);
+ }
+
+ /**
+ * Get an item from the cache, or execute the given Closure and store the result.
+ *
+ * @param string $key
+ * @param DateTimeInterface|DateInterval|int|null $ttl
+ * @param Closure $callback
+ *
+ * @return mixed
+ */
+ public function remember($key, $ttl, Closure $callback)
+ {
+ $value = $this->get($key);
+
+ if (! is_null($value)) {
+ return $value;
+ }
+
+ $this->put($key, $value = $callback(), $ttl);
+
+ return $value;
+ }
+
+ /**
+ * Delete an item from the cache.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function delete($key)
+ {
+ return $this->store->delete($key);
+ }
+
+ /**
+ * Get the underlying cache store.
+ *
+ * @return CacheInterface
+ */
+ public function store()
+ {
+ return $this->store;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Collection.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Collection.php
new file mode 100644
index 0000000..a02146d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Collection.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use LdapRecord\Models\Model;
+use Tightenco\Collect\Support\Collection as BaseCollection;
+
+class Collection extends BaseCollection
+{
+ /**
+ * @inheritdoc
+ */
+ protected function valueRetriever($value)
+ {
+ if ($this->useAsCallable($value)) {
+ return $value;
+ }
+
+ return function ($item) use ($value) {
+ return $item instanceof Model
+ ? $item->getFirstAttribute($value)
+ : data_get($item, $value);
+ };
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Chunk.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Chunk.php
new file mode 100644
index 0000000..3cd36d8
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Chunk.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Chunk extends QueryExecuted
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Listing.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Listing.php
new file mode 100644
index 0000000..2b88ad9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Listing.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Listing extends QueryExecuted
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Paginate.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Paginate.php
new file mode 100644
index 0000000..6f8f262
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Paginate.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Paginate extends QueryExecuted
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/QueryExecuted.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/QueryExecuted.php
new file mode 100644
index 0000000..f13ddeb
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/QueryExecuted.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+use LdapRecord\Query\Builder;
+
+class QueryExecuted
+{
+ /**
+ * The LDAP filter that was used for the query.
+ *
+ * @var string
+ */
+ protected $query;
+
+ /**
+ * The number of milliseconds it took to execute the query.
+ *
+ * @var float
+ */
+ protected $time;
+
+ /**
+ * Constructor.
+ *
+ * @param Builder $query
+ * @param null|float $time
+ */
+ public function __construct(Builder $query, $time = null)
+ {
+ $this->query = $query;
+ $this->time = $time;
+ }
+
+ /**
+ * Returns the LDAP filter that was used for the query.
+ *
+ * @return Builder
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Returns the number of milliseconds it took to execute the query.
+ *
+ * @return float|null
+ */
+ public function getTime()
+ {
+ return $this->time;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Read.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Read.php
new file mode 100644
index 0000000..510c4ca
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Read.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Read extends QueryExecuted
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Search.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Search.php
new file mode 100644
index 0000000..5132316
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Events/Search.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace LdapRecord\Query\Events;
+
+class Search extends QueryExecuted
+{
+ //
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Grammar.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Grammar.php
new file mode 100644
index 0000000..3217173
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Grammar.php
@@ -0,0 +1,549 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use UnexpectedValueException;
+
+class Grammar
+{
+ /**
+ * The query operators and their method names.
+ *
+ * @var array
+ */
+ public $operators = [
+ '*' => 'has',
+ '!*' => 'notHas',
+ '=' => 'equals',
+ '!' => 'doesNotEqual',
+ '!=' => 'doesNotEqual',
+ '>=' => 'greaterThanOrEquals',
+ '<=' => 'lessThanOrEquals',
+ '~=' => 'approximatelyEquals',
+ 'starts_with' => 'startsWith',
+ 'not_starts_with' => 'notStartsWith',
+ 'ends_with' => 'endsWith',
+ 'not_ends_with' => 'notEndsWith',
+ 'contains' => 'contains',
+ 'not_contains' => 'notContains',
+ ];
+
+ /**
+ * The query wrapper.
+ *
+ * @var string|null
+ */
+ protected $wrapper;
+
+ /**
+ * Get all the available operators.
+ *
+ * @return array
+ */
+ public function getOperators()
+ {
+ return array_keys($this->operators);
+ }
+
+ /**
+ * Wraps a query string in brackets.
+ *
+ * Produces: (query)
+ *
+ * @param string $query
+ * @param string $prefix
+ * @param string $suffix
+ *
+ * @return string
+ */
+ public function wrap($query, $prefix = '(', $suffix = ')')
+ {
+ return $prefix.$query.$suffix;
+ }
+
+ /**
+ * Compiles the Builder instance into an LDAP query string.
+ *
+ * @param Builder $query
+ *
+ * @return string
+ */
+ public function compile(Builder $query)
+ {
+ if ($this->queryMustBeWrapped($query)) {
+ $this->wrapper = 'and';
+ }
+
+ $filter = $this->compileRaws($query)
+ .$this->compileWheres($query)
+ .$this->compileOrWheres($query);
+
+ switch ($this->wrapper) {
+ case 'and':
+ return $this->compileAnd($filter);
+ case 'or':
+ return $this->compileOr($filter);
+ default:
+ return $filter;
+ }
+ }
+
+ /**
+ * Determine if the query must be wrapped in an encapsulating statement.
+ *
+ * @param Builder $query
+ *
+ * @return bool
+ */
+ protected function queryMustBeWrapped(Builder $query)
+ {
+ return ! $query->isNested() && $this->hasMultipleFilters($query);
+ }
+
+ /**
+ * Assembles all of the "raw" filters on the query.
+ *
+ * @param Builder $builder
+ *
+ * @return string
+ */
+ protected function compileRaws(Builder $builder)
+ {
+ return $this->concatenate($builder->filters['raw']);
+ }
+
+ /**
+ * Assembles all where clauses in the current wheres property.
+ *
+ * @param Builder $builder
+ * @param string $type
+ *
+ * @return string
+ */
+ protected function compileWheres(Builder $builder, $type = 'and')
+ {
+ $filter = '';
+
+ foreach ($builder->filters[$type] as $where) {
+ $filter .= $this->compileWhere($where);
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Assembles all or where clauses in the current orWheres property.
+ *
+ * @param Builder $query
+ *
+ * @return string
+ */
+ protected function compileOrWheres(Builder $query)
+ {
+ $filter = $this->compileWheres($query, 'or');
+
+ if (! $this->hasMultipleFilters($query)) {
+ return $filter;
+ }
+
+ // Here we will detect whether the entire query can be
+ // wrapped inside of an "or" statement by checking
+ // how many filter statements exist for each type.
+ if ($this->queryCanBeWrappedInSingleOrStatement($query)) {
+ $this->wrapper = 'or';
+ } else {
+ $filter = $this->compileOr($filter);
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Determine if the query can be wrapped in a single or statement.
+ *
+ * @param Builder $query
+ *
+ * @return bool
+ */
+ protected function queryCanBeWrappedInSingleOrStatement(Builder $query)
+ {
+ return $this->has($query, 'or', '>=', 1) &&
+ $this->has($query, 'and', '<=', 1) &&
+ $this->has($query, 'raw', '=', 0);
+ }
+
+ /**
+ * Concatenates filters into a single string.
+ *
+ * @param array $bindings
+ *
+ * @return string
+ */
+ public function concatenate(array $bindings = [])
+ {
+ // Filter out empty query segments.
+ return implode(
+ array_filter($bindings, [$this, 'bindingValueIsNotEmpty'])
+ );
+ }
+
+ /**
+ * Determine if the binding value is not empty.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ protected function bindingValueIsNotEmpty($value)
+ {
+ return ! empty($value);
+ }
+
+ /**
+ * Determine if the query is using multiple filters.
+ *
+ * @param Builder $query
+ *
+ * @return bool
+ */
+ protected function hasMultipleFilters(Builder $query)
+ {
+ return $this->has($query, ['and', 'or', 'raw'], '>', 1);
+ }
+
+ /**
+ * Determine if the query contains the given filter statement type.
+ *
+ * @param Builder $query
+ * @param string|array $type
+ * @param string $operator
+ * @param int $count
+ *
+ * @return bool
+ */
+ protected function has(Builder $query, $type, $operator = '>=', $count = 1)
+ {
+ $types = (array) $type;
+
+ $filters = 0;
+
+ foreach ($types as $type) {
+ $filters += count($query->filters[$type]);
+ }
+
+ switch ($operator) {
+ case '>':
+ return $filters > $count;
+ case '>=':
+ return $filters >= $count;
+ case '<':
+ return $filters < $count;
+ case '<=':
+ return $filters <= $count;
+ default:
+ return $filters == $count;
+ }
+ }
+
+ /**
+ * Returns a query string for equals.
+ *
+ * Produces: (field=value)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileEquals($field, $value)
+ {
+ return $this->wrap($field.'='.$value);
+ }
+
+ /**
+ * Returns a query string for does not equal.
+ *
+ * Produces: (!(field=value))
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileDoesNotEqual($field, $value)
+ {
+ return $this->compileNot(
+ $this->compileEquals($field, $value)
+ );
+ }
+
+ /**
+ * Alias for does not equal operator (!=) operator.
+ *
+ * Produces: (!(field=value))
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileDoesNotEqualAlias($field, $value)
+ {
+ return $this->compileDoesNotEqual($field, $value);
+ }
+
+ /**
+ * Returns a query string for greater than or equals.
+ *
+ * Produces: (field>=value)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileGreaterThanOrEquals($field, $value)
+ {
+ return $this->wrap("$field>=$value");
+ }
+
+ /**
+ * Returns a query string for less than or equals.
+ *
+ * Produces: (field<=value)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileLessThanOrEquals($field, $value)
+ {
+ return $this->wrap("$field<=$value");
+ }
+
+ /**
+ * Returns a query string for approximately equals.
+ *
+ * Produces: (field~=value)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileApproximatelyEquals($field, $value)
+ {
+ return $this->wrap("$field~=$value");
+ }
+
+ /**
+ * Returns a query string for starts with.
+ *
+ * Produces: (field=value*)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileStartsWith($field, $value)
+ {
+ return $this->wrap("$field=$value*");
+ }
+
+ /**
+ * Returns a query string for does not start with.
+ *
+ * Produces: (!(field=*value))
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileNotStartsWith($field, $value)
+ {
+ return $this->compileNot(
+ $this->compileStartsWith($field, $value)
+ );
+ }
+
+ /**
+ * Returns a query string for ends with.
+ *
+ * Produces: (field=*value)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileEndsWith($field, $value)
+ {
+ return $this->wrap("$field=*$value");
+ }
+
+ /**
+ * Returns a query string for does not end with.
+ *
+ * Produces: (!(field=value*))
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileNotEndsWith($field, $value)
+ {
+ return $this->compileNot($this->compileEndsWith($field, $value));
+ }
+
+ /**
+ * Returns a query string for contains.
+ *
+ * Produces: (field=*value*)
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileContains($field, $value)
+ {
+ return $this->wrap("$field=*$value*");
+ }
+
+ /**
+ * Returns a query string for does not contain.
+ *
+ * Produces: (!(field=*value*))
+ *
+ * @param string $field
+ * @param string $value
+ *
+ * @return string
+ */
+ public function compileNotContains($field, $value)
+ {
+ return $this->compileNot(
+ $this->compileContains($field, $value)
+ );
+ }
+
+ /**
+ * Returns a query string for a where has.
+ *
+ * Produces: (field=*)
+ *
+ * @param string $field
+ *
+ * @return string
+ */
+ public function compileHas($field)
+ {
+ return $this->wrap("$field=*");
+ }
+
+ /**
+ * Returns a query string for a where does not have.
+ *
+ * Produces: (!(field=*))
+ *
+ * @param string $field
+ *
+ * @return string
+ */
+ public function compileNotHas($field)
+ {
+ return $this->compileNot(
+ $this->compileHas($field)
+ );
+ }
+
+ /**
+ * Wraps the inserted query inside an AND operator.
+ *
+ * Produces: (&query)
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ public function compileAnd($query)
+ {
+ return $query ? $this->wrap($query, '(&') : '';
+ }
+
+ /**
+ * Wraps the inserted query inside an OR operator.
+ *
+ * Produces: (|query)
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ public function compileOr($query)
+ {
+ return $query ? $this->wrap($query, '(|') : '';
+ }
+
+ /**
+ * Wraps the inserted query inside an NOT operator.
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ public function compileNot($query)
+ {
+ return $query ? $this->wrap($query, '(!') : '';
+ }
+
+ /**
+ * Assembles a single where query.
+ *
+ * @param array $where
+ *
+ * @throws UnexpectedValueException
+ *
+ * @return string
+ */
+ protected function compileWhere(array $where)
+ {
+ $method = $this->makeCompileMethod($where['operator']);
+
+ return $this->{$method}($where['field'], $where['value']);
+ }
+
+ /**
+ * Make the compile method name for the operator.
+ *
+ * @param string $operator
+ *
+ * @throws UnexpectedValueException
+ *
+ * @return string
+ */
+ protected function makeCompileMethod($operator)
+ {
+ if (! $this->operatorExists($operator)) {
+ throw new UnexpectedValueException("Invalid LDAP filter operator ['$operator']");
+ }
+
+ return 'compile'.ucfirst($this->operators[$operator]);
+ }
+
+ /**
+ * Determine if the operator exists.
+ *
+ * @param string $operator
+ *
+ * @return bool
+ */
+ protected function operatorExists($operator)
+ {
+ return array_key_exists($operator, $this->operators);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/InteractsWithTime.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/InteractsWithTime.php
new file mode 100644
index 0000000..1562ec0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/InteractsWithTime.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use Carbon\Carbon;
+use DateInterval;
+use DateTimeInterface;
+
+/**
+ * @author Taylor Otwell
+ *
+ * @see https://laravel.com
+ */
+trait InteractsWithTime
+{
+ /**
+ * Get the number of seconds until the given DateTime.
+ *
+ * @param DateTimeInterface|DateInterval|int $delay
+ *
+ * @return int
+ */
+ protected function secondsUntil($delay)
+ {
+ $delay = $this->parseDateInterval($delay);
+
+ return $delay instanceof DateTimeInterface
+ ? max(0, $delay->getTimestamp() - $this->currentTime())
+ : (int) $delay;
+ }
+
+ /**
+ * Get the "available at" UNIX timestamp.
+ *
+ * @param DateTimeInterface|DateInterval|int $delay
+ *
+ * @return int
+ */
+ protected function availableAt($delay = 0)
+ {
+ $delay = $this->parseDateInterval($delay);
+
+ return $delay instanceof DateTimeInterface
+ ? $delay->getTimestamp()
+ : Carbon::now()->addRealSeconds($delay)->getTimestamp();
+ }
+
+ /**
+ * If the given value is an interval, convert it to a DateTime instance.
+ *
+ * @param DateTimeInterface|DateInterval|int $delay
+ *
+ * @return DateTimeInterface|int
+ */
+ protected function parseDateInterval($delay)
+ {
+ if ($delay instanceof DateInterval) {
+ $delay = Carbon::now()->add($delay);
+ }
+
+ return $delay;
+ }
+
+ /**
+ * Get the current system time as a UNIX timestamp.
+ *
+ * @return int
+ */
+ protected function currentTime()
+ {
+ return Carbon::now()->getTimestamp();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/ActiveDirectoryBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/ActiveDirectoryBuilder.php
new file mode 100644
index 0000000..8923015
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/ActiveDirectoryBuilder.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+use Closure;
+use LdapRecord\LdapInterface;
+use LdapRecord\Models\Attributes\AccountControl;
+use LdapRecord\Models\ModelNotFoundException;
+
+class ActiveDirectoryBuilder extends Builder
+{
+ /**
+ * Finds a record by its Object SID.
+ *
+ * @param string $sid
+ * @param array|string $columns
+ *
+ * @return \LdapRecord\Models\ActiveDirectory\Entry|static|null
+ */
+ public function findBySid($sid, $columns = [])
+ {
+ try {
+ return $this->findBySidOrFail($sid, $columns);
+ } catch (ModelNotFoundException $e) {
+ return;
+ }
+ }
+
+ /**
+ * Finds a record by its Object SID.
+ *
+ * Fails upon no records returned.
+ *
+ * @param string $sid
+ * @param array|string $columns
+ *
+ * @throws ModelNotFoundException
+ *
+ * @return \LdapRecord\Models\ActiveDirectory\Entry|static
+ */
+ public function findBySidOrFail($sid, $columns = [])
+ {
+ return $this->findByOrFail('objectsid', $sid, $columns);
+ }
+
+ /**
+ * Adds a enabled filter to the current query.
+ *
+ * @return $this
+ */
+ public function whereEnabled()
+ {
+ return $this->notFilter(function ($query) {
+ return $query->whereDisabled();
+ });
+ }
+
+ /**
+ * Adds a disabled filter to the current query.
+ *
+ * @return $this
+ */
+ public function whereDisabled()
+ {
+ return $this->rawFilter(
+ (new AccountControl())->accountIsDisabled()->filter()
+ );
+ }
+
+ /**
+ * Adds a 'where member' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function whereMember($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->whereEquals($attribute, $dn);
+ }, 'member', $nested);
+ }
+
+ /**
+ * Adds an 'or where member' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function orWhereMember($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->orWhereEquals($attribute, $dn);
+ }, 'member', $nested);
+ }
+
+ /**
+ * Adds a 'where member of' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function whereMemberOf($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->whereEquals($attribute, $dn);
+ }, 'memberof', $nested);
+ }
+
+ /**
+ * Adds a 'where not member of' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function whereNotMemberof($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->whereNotEquals($attribute, $dn);
+ }, 'memberof', $nested);
+ }
+
+ /**
+ * Adds an 'or where member of' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function orWhereMemberOf($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->orWhereEquals($attribute, $dn);
+ }, 'memberof', $nested);
+ }
+
+ /**
+ * Adds a 'or where not member of' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function orWhereNotMemberof($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->orWhereNotEquals($attribute, $dn);
+ }, 'memberof', $nested);
+ }
+
+ /**
+ * Adds a 'where manager' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function whereManager($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->whereEquals($attribute, $dn);
+ }, 'manager', $nested);
+ }
+
+ /**
+ * Adds a 'where not manager' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function whereNotManager($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->whereNotEquals($attribute, $dn);
+ }, 'manager', $nested);
+ }
+
+ /**
+ * Adds an 'or where manager' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function orWhereManager($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->orWhereEquals($attribute, $dn);
+ }, 'manager', $nested);
+ }
+
+ /**
+ * Adds an 'or where not manager' filter to the current query.
+ *
+ * @param string $dn
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ public function orWhereNotManager($dn, $nested = false)
+ {
+ return $this->nestedMatchQuery(function ($attribute) use ($dn) {
+ return $this->orWhereNotEquals($attribute, $dn);
+ }, 'manager', $nested);
+ }
+
+ /**
+ * Execute the callback with a nested match attribute.
+ *
+ * @param Closure $callback
+ * @param string $attribute
+ * @param bool $nested
+ *
+ * @return $this
+ */
+ protected function nestedMatchQuery(Closure $callback, $attribute, $nested = false)
+ {
+ return $callback(
+ $nested ? $this->makeNestedMatchAttribute($attribute) : $attribute
+ );
+ }
+
+ /**
+ * Make a "nested match" filter attribute for querying descendants.
+ *
+ * @param string $attribute
+ *
+ * @return string
+ */
+ protected function makeNestedMatchAttribute($attribute)
+ {
+ return sprintf('%s:%s:', $attribute, LdapInterface::OID_MATCHING_RULE_IN_CHAIN);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/Builder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/Builder.php
new file mode 100644
index 0000000..eed5e91
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/Builder.php
@@ -0,0 +1,446 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+use Closure;
+use DateTime;
+use LdapRecord\Models\Model;
+use LdapRecord\Models\ModelNotFoundException;
+use LdapRecord\Models\Scope;
+use LdapRecord\Models\Types\ActiveDirectory;
+use LdapRecord\Query\Builder as BaseBuilder;
+use LdapRecord\Utilities;
+
+class Builder extends BaseBuilder
+{
+ /**
+ * The model being queried.
+ *
+ * @var Model
+ */
+ protected $model;
+
+ /**
+ * The global scopes to be applied.
+ *
+ * @var array
+ */
+ protected $scopes = [];
+
+ /**
+ * The removed global scopes.
+ *
+ * @var array
+ */
+ protected $removedScopes = [];
+
+ /**
+ * The applied global scopes.
+ *
+ * @var array
+ */
+ protected $appliedScopes = [];
+
+ /**
+ * Dynamically handle calls into the query instance.
+ *
+ * @param string $method
+ * @param array $parameters
+ *
+ * @return mixed
+ */
+ public function __call($method, $parameters)
+ {
+ if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
+ return $this->callScope([$this->model, $scope], $parameters);
+ }
+
+ return parent::__call($method, $parameters);
+ }
+
+ /**
+ * Apply the given scope on the current builder instance.
+ *
+ * @param callable $scope
+ * @param array $parameters
+ *
+ * @return mixed
+ */
+ protected function callScope(callable $scope, $parameters = [])
+ {
+ array_unshift($parameters, $this);
+
+ return $scope(...array_values($parameters)) ?? $this;
+ }
+
+ /**
+ * Get the attributes to select on the search.
+ *
+ * @return array
+ */
+ public function getSelects()
+ {
+ // Here we will ensure the models GUID attribute is always
+ // selected. In some LDAP directories, the attribute is
+ // virtual and must be requested for specifically.
+ return array_values(array_unique(
+ array_merge([$this->model->getGuidKey()], parent::getSelects())
+ ));
+ }
+
+ /**
+ * Set the model instance for the model being queried.
+ *
+ * @param Model $model
+ *
+ * @return $this
+ */
+ public function setModel(Model $model)
+ {
+ $this->model = $model;
+
+ return $this;
+ }
+
+ /**
+ * Returns the model being queried for.
+ *
+ * @return Model
+ */
+ public function getModel()
+ {
+ return $this->model;
+ }
+
+ /**
+ * Get a new model query builder instance.
+ *
+ * @param string|null $baseDn
+ *
+ * @return static
+ */
+ public function newInstance($baseDn = null)
+ {
+ return parent::newInstance($baseDn)->model($this->model);
+ }
+
+ /**
+ * Finds a model by its distinguished name.
+ *
+ * @param array|string $dn
+ * @param array|string|string[] $columns
+ *
+ * @return Model|\LdapRecord\Query\Collection|static|null
+ */
+ public function find($dn, $columns = ['*'])
+ {
+ return $this->afterScopes(function () use ($dn, $columns) {
+ return parent::find($dn, $columns);
+ });
+ }
+
+ /**
+ * Finds a record using ambiguous name resolution.
+ *
+ * @param string|array $value
+ * @param array|string $columns
+ *
+ * @return Model|\LdapRecord\Query\Collection|static|null
+ */
+ public function findByAnr($value, $columns = ['*'])
+ {
+ if (is_array($value)) {
+ return $this->findManyByAnr($value, $columns);
+ }
+
+ // If the model is not compatible with ANR filters,
+ // we must construct an equivalent filter that
+ // the current LDAP server does support.
+ if (! $this->modelIsCompatibleWithAnr()) {
+ return $this->prepareAnrEquivalentQuery($value)->first($columns);
+ }
+
+ return $this->findBy('anr', $value, $columns);
+ }
+
+ /**
+ * Determine if the current model is compatible with ANR filters.
+ *
+ * @return bool
+ */
+ protected function modelIsCompatibleWithAnr()
+ {
+ return $this->model instanceof ActiveDirectory;
+ }
+
+ /**
+ * Finds a record using ambiguous name resolution.
+ *
+ * If a record is not found, an exception is thrown.
+ *
+ * @param string $value
+ * @param array|string $columns
+ *
+ * @throws ModelNotFoundException
+ *
+ * @return Model
+ */
+ public function findByAnrOrFail($value, $columns = ['*'])
+ {
+ if (! $entry = $this->findByAnr($value, $columns)) {
+ $this->throwNotFoundException($this->getUnescapedQuery(), $this->dn);
+ }
+
+ return $entry;
+ }
+
+ /**
+ * Throws a not found exception.
+ *
+ * @param string $query
+ * @param string $dn
+ *
+ * @throws ModelNotFoundException
+ */
+ protected function throwNotFoundException($query, $dn)
+ {
+ throw ModelNotFoundException::forQuery($query, $dn);
+ }
+
+ /**
+ * Finds multiple records using ambiguous name resolution.
+ *
+ * @param array $values
+ * @param array $columns
+ *
+ * @return \LdapRecord\Query\Collection
+ */
+ public function findManyByAnr(array $values = [], $columns = ['*'])
+ {
+ $this->select($columns);
+
+ if (! $this->modelIsCompatibleWithAnr()) {
+ foreach ($values as $value) {
+ $this->prepareAnrEquivalentQuery($value);
+ }
+
+ return $this->get($columns);
+ }
+
+ return $this->findManyBy('anr', $values);
+ }
+
+ /**
+ * Creates an ANR equivalent query for LDAP distributions that do not support ANR.
+ *
+ * @param string $value
+ *
+ * @return $this
+ */
+ protected function prepareAnrEquivalentQuery($value)
+ {
+ return $this->orFilter(function (self $query) use ($value) {
+ foreach ($this->model->getAnrAttributes() as $attribute) {
+ $query->whereEquals($attribute, $value);
+ }
+ });
+ }
+
+ /**
+ * Finds a record by its string GUID.
+ *
+ * @param string $guid
+ * @param array|string $columns
+ *
+ * @return Model|static|null
+ */
+ public function findByGuid($guid, $columns = ['*'])
+ {
+ try {
+ return $this->findByGuidOrFail($guid, $columns);
+ } catch (ModelNotFoundException $e) {
+ return;
+ }
+ }
+
+ /**
+ * Finds a record by its string GUID.
+ *
+ * Fails upon no records returned.
+ *
+ * @param string $guid
+ * @param array|string $columns
+ *
+ * @throws ModelNotFoundException
+ *
+ * @return Model|static
+ */
+ public function findByGuidOrFail($guid, $columns = ['*'])
+ {
+ if ($this->model instanceof ActiveDirectory) {
+ $guid = Utilities::stringGuidToHex($guid);
+ }
+
+ return $this->whereRaw([
+ $this->model->getGuidKey() => $guid,
+ ])->firstOrFail($columns);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQuery()
+ {
+ return $this->afterScopes(function () {
+ return parent::getQuery();
+ });
+ }
+
+ /**
+ * Apply the query scopes and execute the callback.
+ *
+ * @param Closure $callback
+ *
+ * @return mixed
+ */
+ protected function afterScopes(Closure $callback)
+ {
+ $this->applyScopes();
+
+ return $callback();
+ }
+
+ /**
+ * Apply the global query scopes.
+ *
+ * @return $this
+ */
+ public function applyScopes()
+ {
+ if (! $this->scopes) {
+ return $this;
+ }
+
+ foreach ($this->scopes as $identifier => $scope) {
+ if (isset($this->appliedScopes[$identifier])) {
+ continue;
+ }
+
+ $scope instanceof Scope
+ ? $scope->apply($this, $this->getModel())
+ : $scope($this);
+
+ $this->appliedScopes[$identifier] = $scope;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Register a new global scope.
+ *
+ * @param string $identifier
+ * @param Scope|\Closure $scope
+ *
+ * @return $this
+ */
+ public function withGlobalScope($identifier, $scope)
+ {
+ $this->scopes[$identifier] = $scope;
+
+ return $this;
+ }
+
+ /**
+ * Remove a registered global scope.
+ *
+ * @param Scope|string $scope
+ *
+ * @return $this
+ */
+ public function withoutGlobalScope($scope)
+ {
+ if (! is_string($scope)) {
+ $scope = get_class($scope);
+ }
+
+ unset($this->scopes[$scope]);
+
+ $this->removedScopes[] = $scope;
+
+ return $this;
+ }
+
+ /**
+ * Remove all or passed registered global scopes.
+ *
+ * @param array|null $scopes
+ *
+ * @return $this
+ */
+ public function withoutGlobalScopes(array $scopes = null)
+ {
+ if (! is_array($scopes)) {
+ $scopes = array_keys($this->scopes);
+ }
+
+ foreach ($scopes as $scope) {
+ $this->withoutGlobalScope($scope);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get an array of global scopes that were removed from the query.
+ *
+ * @return array
+ */
+ public function removedScopes()
+ {
+ return $this->removedScopes;
+ }
+
+ /**
+ * Get an array of the global scopes that were applied to the query.
+ *
+ * @return array
+ */
+ public function appliedScopes()
+ {
+ return $this->appliedScopes;
+ }
+
+ /**
+ * Processes and converts the given LDAP results into models.
+ *
+ * @param array $results
+ *
+ * @return \LdapRecord\Query\Collection
+ */
+ protected function process(array $results)
+ {
+ return $this->model->hydrate(parent::process($results));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function prepareWhereValue($field, $value, $raw = false)
+ {
+ if ($value instanceof DateTime) {
+ $field = $this->model->normalizeAttributeKey($field);
+
+ if (! $this->model->isDateAttribute($field)) {
+ throw new \UnexpectedValueException(
+ "Cannot convert field [$field] to an LDAP timestamp. You must add this field as a model date."
+ .' Refer to https://ldaprecord.com/docs/model-mutators/#date-mutators'
+ );
+ }
+
+ $value = $this->model->fromDateTime($this->model->getDates()[$field], $value);
+ }
+
+ return parent::prepareWhereValue($field, $value, $raw);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/FreeIpaBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/FreeIpaBuilder.php
new file mode 100644
index 0000000..5e3d43f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/FreeIpaBuilder.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+class FreeIpaBuilder extends Builder
+{
+ /**
+ * Adds a enabled filter to the current query.
+ *
+ * @return $this
+ */
+ public function whereEnabled()
+ {
+ return $this->rawFilter('(!(pwdAccountLockedTime=*))');
+ }
+
+ /**
+ * Adds a disabled filter to the current query.
+ *
+ * @return $this
+ */
+ public function whereDisabled()
+ {
+ return $this->rawFilter('(pwdAccountLockedTime=*)');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/OpenLdapBuilder.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/OpenLdapBuilder.php
new file mode 100644
index 0000000..dd41344
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Model/OpenLdapBuilder.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace LdapRecord\Query\Model;
+
+class OpenLdapBuilder extends Builder
+{
+ /**
+ * Adds a enabled filter to the current query.
+ *
+ * @return $this
+ */
+ public function whereEnabled()
+ {
+ return $this->rawFilter('(!(pwdAccountLockedTime=*))');
+ }
+
+ /**
+ * Adds a disabled filter to the current query.
+ *
+ * @return $this
+ */
+ public function whereDisabled()
+ {
+ return $this->rawFilter('(pwdAccountLockedTime=*)');
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ObjectNotFoundException.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ObjectNotFoundException.php
new file mode 100644
index 0000000..b2dec28
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/ObjectNotFoundException.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace LdapRecord\Query;
+
+use LdapRecord\LdapRecordException;
+
+class ObjectNotFoundException extends LdapRecordException
+{
+ /**
+ * The query filter that was used.
+ *
+ * @var string
+ */
+ protected $query;
+
+ /**
+ * The base DN of the query that was used.
+ *
+ * @var string
+ */
+ protected $baseDn;
+
+ /**
+ * Create a new exception for the executed filter.
+ *
+ * @param string $query
+ * @param null $baseDn
+ *
+ * @return static
+ */
+ public static function forQuery($query, $baseDn = null)
+ {
+ return (new static())->setQuery($query, $baseDn);
+ }
+
+ /**
+ * Set the query that was used.
+ *
+ * @param string $query
+ * @param string|null $baseDn
+ *
+ * @return $this
+ */
+ public function setQuery($query, $baseDn = null)
+ {
+ $this->query = $query;
+ $this->baseDn = $baseDn;
+ $this->message = "No LDAP query results for filter: [$query] in: [$baseDn]";
+
+ return $this;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/AbstractPaginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/AbstractPaginator.php
new file mode 100644
index 0000000..3dfd3f1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/AbstractPaginator.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+use LdapRecord\Query\Builder;
+
+abstract class AbstractPaginator
+{
+ /**
+ * The query builder instance.
+ *
+ * @var Builder
+ */
+ protected $query;
+
+ /**
+ * The filter to execute.
+ *
+ * @var string
+ */
+ protected $filter;
+
+ /**
+ * The amount of objects to fetch per page.
+ *
+ * @var int
+ */
+ protected $perPage;
+
+ /**
+ * Whether the operation is critical.
+ *
+ * @var bool
+ */
+ protected $isCritical;
+
+ /**
+ * Constructor.
+ *
+ * @param Builder $query
+ */
+ public function __construct(Builder $query, $filter, $perPage, $isCritical)
+ {
+ $this->query = $query;
+ $this->filter = $filter;
+ $this->perPage = $perPage;
+ $this->isCritical = $isCritical;
+ }
+
+ /**
+ * Execute the pagination request.
+ *
+ * @param LdapInterface $ldap
+ *
+ * @return array
+ */
+ public function execute(LdapInterface $ldap)
+ {
+ $pages = [];
+
+ $this->prepareServerControls();
+
+ do {
+ $this->applyServerControls($ldap);
+
+ if (! $resource = $this->query->run($this->filter)) {
+ break;
+ }
+
+ $this->updateServerControls($ldap, $resource);
+
+ $pages[] = $this->query->parse($resource);
+ } while (! empty($this->fetchCookie()));
+
+ $this->resetServerControls($ldap);
+
+ return $pages;
+ }
+
+ /**
+ * Fetch the pagination cookie.
+ *
+ * @return string
+ */
+ abstract protected function fetchCookie();
+
+ /**
+ * Prepare the server controls before executing the pagination request.
+ *
+ * @return void
+ */
+ abstract protected function prepareServerControls();
+
+ /**
+ * Apply the server controls.
+ *
+ * @param LdapInterface $ldap
+ *
+ * @return void
+ */
+ abstract protected function applyServerControls(LdapInterface $ldap);
+
+ /**
+ * Reset the server controls.
+ *
+ * @param LdapInterface $ldap
+ *
+ * @return mixed
+ */
+ abstract protected function resetServerControls(LdapInterface $ldap);
+
+ /**
+ * Update the server controls.
+ *
+ * @param LdapInterface $ldap
+ * @param resource $resource
+ *
+ * @return void
+ */
+ abstract protected function updateServerControls(LdapInterface $ldap, $resource);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/DeprecatedPaginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/DeprecatedPaginator.php
new file mode 100644
index 0000000..b4a7f8d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/DeprecatedPaginator.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+
+/**
+ * @deprecated since v2.5.0
+ */
+class DeprecatedPaginator extends AbstractPaginator
+{
+ /**
+ * The pagination cookie.
+ *
+ * @var string
+ */
+ protected $cookie = '';
+
+ /**
+ * @inheritdoc
+ */
+ protected function fetchCookie()
+ {
+ return $this->cookie;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function prepareServerControls()
+ {
+ $this->cookie = '';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function applyServerControls(LdapInterface $ldap)
+ {
+ $ldap->controlPagedResult($this->perPage, $this->isCritical, $this->cookie);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function updateServerControls(LdapInterface $ldap, $resource)
+ {
+ $ldap->controlPagedResultResponse($resource, $this->cookie);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function resetServerControls(LdapInterface $ldap)
+ {
+ $ldap->controlPagedResult();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/LazyPaginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/LazyPaginator.php
new file mode 100644
index 0000000..2974b8f
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/LazyPaginator.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+
+class LazyPaginator extends Paginator
+{
+ /**
+ * Execute the pagination request.
+ *
+ * @param LdapInterface $ldap
+ *
+ * @return Generator
+ */
+ public function execute(LdapInterface $ldap)
+ {
+ $this->prepareServerControls();
+
+ do {
+ $this->applyServerControls($ldap);
+
+ if (! $resource = $this->query->run($this->filter)) {
+ break;
+ }
+
+ $this->updateServerControls($ldap, $resource);
+
+ yield $this->query->parse($resource);
+ } while (! empty($this->fetchCookie()));
+
+ $this->resetServerControls($ldap);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/Paginator.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/Paginator.php
new file mode 100644
index 0000000..9ab6e67
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Query/Pagination/Paginator.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace LdapRecord\Query\Pagination;
+
+use LdapRecord\LdapInterface;
+
+class Paginator extends AbstractPaginator
+{
+ /**
+ * @inheritdoc
+ */
+ protected function fetchCookie()
+ {
+ return $this->query->controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? null;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function prepareServerControls()
+ {
+ $this->query->addControl(LDAP_CONTROL_PAGEDRESULTS, $this->isCritical, [
+ 'size' => $this->perPage, 'cookie' => '',
+ ]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function applyServerControls(LdapInterface $ldap)
+ {
+ $ldap->setOption(LDAP_OPT_SERVER_CONTROLS, $this->query->controls);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function updateServerControls(LdapInterface $ldap, $resource)
+ {
+ $errorCode = $dn = $errorMessage = $refs = null;
+
+ $ldap->parseResult(
+ $resource,
+ $errorCode,
+ $dn,
+ $errorMessage,
+ $refs,
+ $this->query->controls
+ );
+
+ $this->resetPageSize();
+ }
+
+ /**
+ * Reset the page control page size.
+ *
+ * @return void
+ */
+ protected function resetPageSize()
+ {
+ $this->query->controls[LDAP_CONTROL_PAGEDRESULTS]['value']['size'] = $this->perPage;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function resetServerControls(LdapInterface $ldap)
+ {
+ $this->query->controls = [];
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Arr.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Arr.php
new file mode 100644
index 0000000..8fa87a2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Arr.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace LdapRecord\Support;
+
+use ArrayAccess;
+
+class Arr
+{
+ /**
+ * Determine whether the given value is array accessible.
+ *
+ * @param mixed $value
+ *
+ * @return bool
+ */
+ public static function accessible($value)
+ {
+ return is_array($value) || $value instanceof ArrayAccess;
+ }
+
+ /**
+ * Determine if the given key exists in the provided array.
+ *
+ * @param \ArrayAccess|array $array
+ * @param string|int $key
+ *
+ * @return bool
+ */
+ public static function exists($array, $key)
+ {
+ if ($array instanceof ArrayAccess) {
+ return $array->offsetExists($key);
+ }
+
+ return array_key_exists($key, $array);
+ }
+
+ /**
+ * If the given value is not an array and not null, wrap it in one.
+ *
+ * @param mixed $value
+ *
+ * @return array
+ */
+ public static function wrap($value)
+ {
+ if (is_null($value)) {
+ return [];
+ }
+
+ return is_array($value) ? $value : [$value];
+ }
+
+ /**
+ * Return the first element in an array passing a given truth test.
+ *
+ * @param iterable $array
+ * @param callable|null $callback
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public static function first($array, callable $callback = null, $default = null)
+ {
+ if (is_null($callback)) {
+ if (empty($array)) {
+ return Helpers::value($default);
+ }
+
+ foreach ($array as $item) {
+ return $item;
+ }
+ }
+
+ foreach ($array as $key => $value) {
+ if ($callback($value, $key)) {
+ return $value;
+ }
+ }
+
+ return Helpers::value($default);
+ }
+
+ /**
+ * Return the last element in an array passing a given truth test.
+ *
+ * @param array $array
+ * @param callable|null $callback
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public static function last($array, callable $callback = null, $default = null)
+ {
+ if (is_null($callback)) {
+ return empty($array) ? Helpers::value($default) : end($array);
+ }
+
+ return static::first(array_reverse($array, true), $callback, $default);
+ }
+
+ /**
+ * Get an item from an array using "dot" notation.
+ *
+ * @param ArrayAccess|array $array
+ * @param string|int|null $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public static function get($array, $key, $default = null)
+ {
+ if (! static::accessible($array)) {
+ return Helpers::value($default);
+ }
+
+ if (is_null($key)) {
+ return $array;
+ }
+
+ if (static::exists($array, $key)) {
+ return $array[$key];
+ }
+
+ if (strpos($key, '.') === false) {
+ return $array[$key] ?? Helpers::value($default);
+ }
+
+ foreach (explode('.', $key) as $segment) {
+ if (static::accessible($array) && static::exists($array, $segment)) {
+ $array = $array[$segment];
+ } else {
+ return Helpers::value($default);
+ }
+ }
+
+ return $array;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Helpers.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Helpers.php
new file mode 100644
index 0000000..a55d1d2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Support/Helpers.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace LdapRecord\Support;
+
+use Closure;
+
+class Helpers
+{
+ /**
+ * Return the default value of the given value.
+ *
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ public static function value($value)
+ {
+ return $value instanceof Closure ? $value() : $value;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/AuthGuardFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/AuthGuardFake.php
new file mode 100644
index 0000000..4a69150
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/AuthGuardFake.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\Auth\Guard;
+
+class AuthGuardFake extends Guard
+{
+ /**
+ * Always allow binding as configured user.
+ *
+ * @return bool
+ */
+ public function bindAsConfiguredUser()
+ {
+ return true;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/ConnectionFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/ConnectionFake.php
new file mode 100644
index 0000000..0aa12a1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/ConnectionFake.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\Auth\Guard;
+use LdapRecord\Connection;
+use LdapRecord\Models\Model;
+
+class ConnectionFake extends Connection
+{
+ /**
+ * The underlying fake LDAP connection.
+ *
+ * @var LdapFake
+ */
+ protected $ldap;
+
+ /**
+ * Whether the fake is connected.
+ *
+ * @var bool
+ */
+ protected $connected = false;
+
+ /**
+ * Make a new fake LDAP connection instance.
+ *
+ * @param array $config
+ * @param string $ldap
+ *
+ * @return static
+ */
+ public static function make(array $config = [], $ldap = LdapFake::class)
+ {
+ $connection = new static($config, new $ldap());
+
+ $connection->configure();
+
+ return $connection;
+ }
+
+ /**
+ * Set the user to authenticate as.
+ *
+ * @param Model|string $user
+ *
+ * @return $this
+ */
+ public function actingAs($user)
+ {
+ $this->ldap->shouldAuthenticateWith(
+ $user instanceof Model ? $user->getDn() : $user
+ );
+
+ return $this;
+ }
+
+ /**
+ * Set the connection to bypass bind attempts as the configured user.
+ *
+ * @return $this
+ */
+ public function shouldBeConnected()
+ {
+ $this->connected = true;
+
+ $this->authGuardResolver = function () {
+ return new AuthGuardFake($this->ldap, $this->configuration);
+ };
+
+ return $this;
+ }
+
+ /**
+ * Set the connection to attempt binding as the configured user.
+ *
+ * @return $this
+ */
+ public function shouldNotBeConnected()
+ {
+ $this->connected = false;
+
+ $this->authGuardResolver = function () {
+ return new Guard($this->ldap, $this->configuration);
+ };
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isConnected()
+ {
+ return $this->connected ?: parent::isConnected();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/DirectoryFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/DirectoryFake.php
new file mode 100644
index 0000000..70640af
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/DirectoryFake.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\Container;
+
+class DirectoryFake
+{
+ /**
+ * Setup the fake connection.
+ *
+ * @param string|null $name
+ *
+ * @throws \LdapRecord\ContainerException
+ *
+ * @return ConnectionFake
+ */
+ public static function setup($name = null)
+ {
+ $connection = Container::getConnection($name);
+
+ $fake = static::makeConnectionFake(
+ $connection->getConfiguration()->all()
+ );
+
+ // Replace the connection with a fake.
+ Container::addConnection($fake, $name);
+
+ return $fake;
+ }
+
+ /**
+ * Reset the container.
+ *
+ * @return void
+ */
+ public static function tearDown()
+ {
+ Container::reset();
+ }
+
+ /**
+ * Make a connection fake.
+ *
+ * @param array $config
+ *
+ * @return ConnectionFake
+ */
+ public static function makeConnectionFake(array $config = [])
+ {
+ return ConnectionFake::make($config)->shouldBeConnected();
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapExpectation.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapExpectation.php
new file mode 100644
index 0000000..90a5fa2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapExpectation.php
@@ -0,0 +1,303 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use LdapRecord\LdapRecordException;
+use PHPUnit\Framework\Constraint\Constraint;
+use PHPUnit\Framework\Constraint\IsEqual;
+use UnexpectedValueException;
+
+class LdapExpectation
+{
+ /**
+ * The value to return from the expectation.
+ *
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * The exception to throw from the expectation.
+ *
+ * @var null|LdapRecordException|\Exception
+ */
+ protected $exception;
+
+ /**
+ * The amount of times the expectation should be called.
+ *
+ * @var int
+ */
+ protected $count = 1;
+
+ /**
+ * The method that the expectation belongs to.
+ *
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * The methods argument's.
+ *
+ * @var array
+ */
+ protected $args = [];
+
+ /**
+ * Whether the same expectation should be returned indefinitely.
+ *
+ * @var bool
+ */
+ protected $indefinitely = true;
+
+ /**
+ * Whether the expectation should return errors.
+ *
+ * @var bool
+ */
+ protected $errors = false;
+
+ /**
+ * The error number to return.
+ *
+ * @var int
+ */
+ protected $errorCode = 1;
+
+ /**
+ * The last error string to return.
+ *
+ * @var string
+ */
+ protected $errorMessage = '';
+
+ /**
+ * The diagnostic message string to return.
+ *
+ * @var string
+ */
+ protected $errorDiagnosticMessage = '';
+
+ /**
+ * Constructor.
+ *
+ * @param string $method
+ */
+ public function __construct($method)
+ {
+ $this->method = $method;
+ }
+
+ /**
+ * Set the arguments that the operation should receive.
+ *
+ * @param mixed $args
+ *
+ * @return $this
+ */
+ public function with($args)
+ {
+ $args = is_array($args) ? $args : func_get_args();
+
+ foreach ($args as $key => $arg) {
+ if (! $arg instanceof Constraint) {
+ $args[$key] = new IsEqual($arg);
+ }
+ }
+
+ $this->args = $args;
+
+ return $this;
+ }
+
+ /**
+ * Set the expected value to return.
+ *
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function andReturn($value)
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ /**
+ * The error message to return from the expectation.
+ *
+ * @param int $code
+ * @param string $error
+ * @param string $diagnosticMessage
+ *
+ * @return $this
+ */
+ public function andReturnError($code = 1, $error = '', $diagnosticMessage = '')
+ {
+ $this->errors = true;
+
+ $this->errorCode = $code;
+ $this->errorMessage = $error;
+ $this->errorDiagnosticMessage = $diagnosticMessage;
+
+ return $this;
+ }
+
+ /**
+ * Set the expected exception to throw.
+ *
+ * @param string|\Exception|LdapRecordException $exception
+ *
+ * @return $this
+ */
+ public function andThrow($exception)
+ {
+ if (is_string($exception)) {
+ $exception = new LdapRecordException($exception);
+ }
+
+ $this->exception = $exception;
+
+ return $this;
+ }
+
+ /**
+ * Set the expectation to be only called once.
+ *
+ * @return $this
+ */
+ public function once()
+ {
+ return $this->times(1);
+ }
+
+ /**
+ * Set the expectation to be only called twice.
+ *
+ * @return $this
+ */
+ public function twice()
+ {
+ return $this->times(2);
+ }
+
+ /**
+ * Set the expectation to be called the given number of times.
+ *
+ * @param int $count
+ *
+ * @return $this
+ */
+ public function times($count = 1)
+ {
+ $this->indefinitely = false;
+
+ $this->count = $count;
+
+ return $this;
+ }
+
+ /**
+ * Get the method the expectation belongs to.
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ if (is_null($this->method)) {
+ throw new UnexpectedValueException('An expectation must have a method.');
+ }
+
+ return $this->method;
+ }
+
+ /**
+ * Get the expected call count.
+ *
+ * @return int
+ */
+ public function getExpectedCount()
+ {
+ return $this->count;
+ }
+
+ /**
+ * Get the expected arguments.
+ *
+ * @return Constraint[]
+ */
+ public function getExpectedArgs()
+ {
+ return $this->args;
+ }
+
+ /**
+ * Get the expected exception.
+ *
+ * @return null|\Exception|LdapRecordException
+ */
+ public function getExpectedException()
+ {
+ return $this->exception;
+ }
+
+ /**
+ * Get the expected value.
+ *
+ * @return mixed
+ */
+ public function getExpectedValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Determine whether the expectation is returning an error.
+ *
+ * @return bool
+ */
+ public function isReturningError()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * @return int
+ */
+ public function getExpectedErrorCode()
+ {
+ return $this->errorCode;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExpectedErrorMessage()
+ {
+ return $this->errorMessage;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExpectedErrorDiagnosticMessage()
+ {
+ return $this->errorDiagnosticMessage;
+ }
+
+ /**
+ * Decrement the call count of the expectation.
+ *
+ * @return $this
+ */
+ public function decrementCallCount()
+ {
+ if (! $this->indefinitely) {
+ $this->count -= 1;
+ }
+
+ return $this;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapFake.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapFake.php
new file mode 100644
index 0000000..7ba0e15
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Testing/LdapFake.php
@@ -0,0 +1,525 @@
+<?php
+
+namespace LdapRecord\Testing;
+
+use Exception;
+use LdapRecord\DetailedError;
+use LdapRecord\DetectsErrors;
+use LdapRecord\HandlesConnection;
+use LdapRecord\LdapInterface;
+use LdapRecord\Support\Arr;
+use PHPUnit\Framework\Assert as PHPUnit;
+use PHPUnit\Framework\Constraint\Constraint;
+
+class LdapFake implements LdapInterface
+{
+ use HandlesConnection, DetectsErrors;
+
+ /**
+ * The expectations of the LDAP fake.
+ *
+ * @var array
+ */
+ protected $expectations = [];
+
+ /**
+ * The default fake error number.
+ *
+ * @var int
+ */
+ protected $errNo = 1;
+
+ /**
+ * The default fake last error string.
+ *
+ * @var string
+ */
+ protected $lastError = '';
+
+ /**
+ * The default fake diagnostic message string.
+ *
+ * @var string
+ */
+ protected $diagnosticMessage = '';
+
+ /**
+ * Create a new expected operation.
+ *
+ * @param string $method
+ *
+ * @return LdapExpectation
+ */
+ public static function operation($method)
+ {
+ return new LdapExpectation($method);
+ }
+
+ /**
+ * Set the user that will pass binding.
+ *
+ * @param string $dn
+ *
+ * @return $this
+ */
+ public function shouldAuthenticateWith($dn)
+ {
+ return $this->expect(
+ static::operation('bind')->with($dn, PHPUnit::anything())->andReturn(true)
+ );
+ }
+
+ /**
+ * Add an LDAP method expectation.
+ *
+ * @param LdapExpectation|array $expectations
+ *
+ * @return $this
+ */
+ public function expect($expectations = [])
+ {
+ $expectations = Arr::wrap($expectations);
+
+ foreach ($expectations as $key => $expectation) {
+ // If the key is non-numeric, we will assume
+ // that the string is the method name and
+ // the expectation is the return value.
+ if (! is_numeric($key)) {
+ $expectation = static::operation($key)->andReturn($expectation);
+ }
+
+ if (! $expectation instanceof LdapExpectation) {
+ $expectation = static::operation($expectation);
+ }
+
+ $this->expectations[$expectation->getMethod()][] = $expectation;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Determine if the method has any expectations.
+ *
+ * @param string $method
+ *
+ * @return bool
+ */
+ public function hasExpectations($method)
+ {
+ return count($this->getExpectations($method)) > 0;
+ }
+
+ /**
+ * Get expectations by method.
+ *
+ * @param string $method
+ *
+ * @return LdapExpectation[]|mixed
+ */
+ public function getExpectations($method)
+ {
+ return $this->expectations[$method] ?? [];
+ }
+
+ /**
+ * Remove an expectation by method and key.
+ *
+ * @param string $method
+ * @param int $key
+ *
+ * @return void
+ */
+ public function removeExpectation($method, $key)
+ {
+ unset($this->expectations[$method][$key]);
+ }
+
+ /**
+ * Set the error number of a failed bind attempt.
+ *
+ * @param int $number
+ *
+ * @return $this
+ */
+ public function shouldReturnErrorNumber($number = 1)
+ {
+ $this->errNo = $number;
+
+ return $this;
+ }
+
+ /**
+ * Set the last error of a failed bind attempt.
+ *
+ * @param string $message
+ *
+ * @return $this
+ */
+ public function shouldReturnError($message = '')
+ {
+ $this->lastError = $message;
+
+ return $this;
+ }
+
+ /**
+ * Set the diagnostic message of a failed bind attempt.
+ *
+ * @param string $message
+ *
+ * @return $this
+ */
+ public function shouldReturnDiagnosticMessage($message = '')
+ {
+ $this->diagnosticMessage = $message;
+
+ return $this;
+ }
+
+ /**
+ * Return a fake error number.
+ *
+ * @return int
+ */
+ public function errNo()
+ {
+ return $this->errNo;
+ }
+
+ /**
+ * Return a fake error.
+ *
+ * @return string
+ */
+ public function getLastError()
+ {
+ return $this->lastError;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDiagnosticMessage()
+ {
+ return $this->diagnosticMessage;
+ }
+
+ /**
+ * Return a fake detailed error.
+ *
+ * @return DetailedError
+ */
+ public function getDetailedError()
+ {
+ return new DetailedError(
+ $this->errNo(),
+ $this->getLastError(),
+ $this->getDiagnosticMessage()
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getEntries($searchResults)
+ {
+ return $searchResults;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isUsingSSL()
+ {
+ return $this->hasExpectations('isUsingSSL')
+ ? $this->resolveExpectation('isUsingSSL')
+ : $this->useSSL;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isUsingTLS()
+ {
+ return $this->hasExpectations('isUsingTLS')
+ ? $this->resolveExpectation('isUsingTLS')
+ : $this->useTLS;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isBound()
+ {
+ return $this->hasExpectations('isBound')
+ ? $this->resolveExpectation('isBound')
+ : $this->bound;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setOption($option, $value)
+ {
+ return $this->hasExpectations('setOption')
+ ? $this->resolveExpectation('setOption', func_get_args())
+ : true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getOption($option, &$value = null)
+ {
+ return $this->resolveExpectation('getOption', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function startTLS()
+ {
+ return $this->resolveExpectation('startTLS', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function connect($hosts = [], $port = 389)
+ {
+ $this->bound = false;
+
+ $this->host = $this->makeConnectionUris($hosts, $port);
+
+ return $this->connection = $this->hasExpectations('connect')
+ ? $this->resolveExpectation('connect', func_get_args())
+ : true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function close()
+ {
+ $this->connection = null;
+ $this->bound = false;
+ $this->host = null;
+
+ return $this->hasExpectations('close')
+ ? $this->resolveExpectation('close')
+ : true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function bind($username, $password)
+ {
+ return $this->bound = $this->resolveExpectation('bind', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+ {
+ return $this->resolveExpectation('search', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+ {
+ return $this->resolveExpectation('listing', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0, $deref = null, $serverControls = [])
+ {
+ return $this->resolveExpectation('read', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
+ {
+ return $this->resolveExpectation('parseResult', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function add($dn, array $entry)
+ {
+ return $this->resolveExpectation('add', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function delete($dn)
+ {
+ return $this->resolveExpectation('delete', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
+ {
+ return $this->resolveExpectation('rename', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modify($dn, array $entry)
+ {
+ return $this->resolveExpectation('modify', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modifyBatch($dn, array $values)
+ {
+ return $this->resolveExpectation('modifyBatch', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modAdd($dn, array $entry)
+ {
+ return $this->resolveExpectation('modAdd', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modReplace($dn, array $entry)
+ {
+ return $this->resolveExpectation('modReplace', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modDelete($dn, array $entry)
+ {
+ return $this->resolveExpectation('modDelete', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
+ {
+ return $this->resolveExpectation('controlPagedResult', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function controlPagedResultResponse($result, &$cookie)
+ {
+ return $this->resolveExpectation('controlPagedResultResponse', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function freeResult($result)
+ {
+ return $this->resolveExpectation('freeResult', func_get_args());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function err2Str($number)
+ {
+ return $this->resolveExpectation('err2Str', func_get_args());
+ }
+
+ /**
+ * Resolve the methods expectations.
+ *
+ * @param string $method
+ * @param array $args
+ *
+ * @throws Exception
+ *
+ * @return mixed
+ */
+ protected function resolveExpectation($method, array $args = [])
+ {
+ foreach ($this->getExpectations($method) as $key => $expectation) {
+ $this->assertMethodArgumentsMatch($method, $expectation->getExpectedArgs(), $args);
+
+ $expectation->decrementCallCount();
+
+ if ($expectation->getExpectedCount() === 0) {
+ $this->removeExpectation($method, $key);
+ }
+
+ if (! is_null($exception = $expectation->getExpectedException())) {
+ throw $exception;
+ }
+
+ if ($expectation->isReturningError()) {
+ $this->applyExpectationError($expectation);
+ }
+
+ return $expectation->getExpectedValue();
+ }
+
+ throw new Exception("LDAP method [$method] was unexpected.");
+ }
+
+ /**
+ * Apply the expectation error to the fake.
+ *
+ * @param LdapExpectation $expectation
+ *
+ * @return void
+ */
+ protected function applyExpectationError(LdapExpectation $expectation)
+ {
+ $this->shouldReturnError($expectation->getExpectedErrorMessage());
+ $this->shouldReturnErrorNumber($expectation->getExpectedErrorCode());
+ $this->shouldReturnDiagnosticMessage($expectation->getExpectedErrorDiagnosticMessage());
+ }
+
+ /**
+ * Assert that the expected arguments match the operations arguments.
+ *
+ * @param string $method
+ * @param Constraint[] $expectedArgs
+ * @param array $methodArgs
+ *
+ * @return void
+ */
+ protected function assertMethodArgumentsMatch($method, array $expectedArgs = [], array $methodArgs = [])
+ {
+ foreach ($expectedArgs as $key => $constraint) {
+ $argNumber = $key + 1;
+
+ PHPUnit::assertArrayHasKey(
+ $key,
+ $methodArgs,
+ "LDAP method [$method] argument #{$argNumber} does not exist."
+ );
+
+ $constraint->evaluate(
+ $methodArgs[$key],
+ "LDAP method [$method] expectation failed."
+ );
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Utilities.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Utilities.php
new file mode 100644
index 0000000..0f0ca3c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/vendor/directorytree/ldaprecord/src/Utilities.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace LdapRecord;
+
+class Utilities
+{
+ /**
+ * Converts a DN string into an array of RDNs.
+ *
+ * This will also decode hex characters into their true
+ * UTF-8 representation embedded inside the DN as well.
+ *
+ * @param string $dn
+ * @param bool $removeAttributePrefixes
+ *
+ * @return array|false
+ */
+ public static function explodeDn($dn, $removeAttributePrefixes = true)
+ {
+ $dn = ldap_explode_dn($dn, ($removeAttributePrefixes ? 1 : 0));
+
+ if (! is_array($dn)) {
+ return false;
+ }
+
+ if (! array_key_exists('count', $dn)) {
+ return false;
+ }
+
+ unset($dn['count']);
+
+ foreach ($dn as $rdn => $value) {
+ $dn[$rdn] = static::unescape($value);
+ }
+
+ return $dn;
+ }
+
+ /**
+ * Un-escapes a hexadecimal string into its original string representation.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public static function unescape($value)
+ {
+ return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
+ return chr(hexdec($matches[1]));
+ }, $value);
+ }
+
+ /**
+ * Convert a binary SID to a string SID.
+ *
+ * @author Chad Sikorra
+ *
+ * @see https://github.com/ChadSikorra
+ * @see https://stackoverflow.com/questions/39533560/php-ldap-get-user-sid
+ *
+ * @param string $value The Binary SID
+ *
+ * @return string|null
+ */
+ public static function binarySidToString($value)
+ {
+ // Revision - 8bit unsigned int (C1)
+ // Count - 8bit unsigned int (C1)
+ // 2 null bytes
+ // ID - 32bit unsigned long, big-endian order
+ $sid = @unpack('C1rev/C1count/x2/N1id', $value);
+
+ if (! isset($sid['id']) || ! isset($sid['rev'])) {
+ return;
+ }
+
+ $revisionLevel = $sid['rev'];
+
+ $identifierAuthority = $sid['id'];
+
+ $subs = isset($sid['count']) ? $sid['count'] : 0;
+
+ $sidHex = $subs ? bin2hex($value) : '';
+
+ $subAuthorities = [];
+
+ // The sub-authorities depend on the count, so only get as
+ // many as the count, regardless of data beyond it.
+ for ($i = 0; $i < $subs; $i++) {
+ $data = implode(array_reverse(
+ str_split(
+ substr($sidHex, 16 + ($i * 8), 8),
+ 2
+ )
+ ));
+
+ $subAuthorities[] = hexdec($data);
+ }
+
+ // Tack on the 'S-' and glue it all together...
+ return 'S-'.$revisionLevel.'-'.$identifierAuthority.implode(
+ preg_filter('/^/', '-', $subAuthorities)
+ );
+ }
+
+ /**
+ * Convert a binary GUID to a string GUID.
+ *
+ * @param string $binGuid
+ *
+ * @return string|null
+ */
+ public static function binaryGuidToString($binGuid)
+ {
+ if (trim($binGuid) == '' || is_null($binGuid)) {
+ return;
+ }
+
+ $hex = unpack('H*hex', $binGuid)['hex'];
+
+ $hex1 = substr($hex, -26, 2).substr($hex, -28, 2).substr($hex, -30, 2).substr($hex, -32, 2);
+ $hex2 = substr($hex, -22, 2).substr($hex, -24, 2);
+ $hex3 = substr($hex, -18, 2).substr($hex, -20, 2);
+ $hex4 = substr($hex, -16, 4);
+ $hex5 = substr($hex, -12, 12);
+
+ return sprintf('%s-%s-%s-%s-%s', $hex1, $hex2, $hex3, $hex4, $hex5);
+ }
+
+ /**
+ * Converts a string GUID to it's hex variant.
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function stringGuidToHex($string)
+ {
+ $hex = '\\'.substr($string, 6, 2).'\\'.substr($string, 4, 2).'\\'.substr($string, 2, 2).'\\'.substr($string, 0, 2);
+ $hex = $hex.'\\'.substr($string, 11, 2).'\\'.substr($string, 9, 2);
+ $hex = $hex.'\\'.substr($string, 16, 2).'\\'.substr($string, 14, 2);
+ $hex = $hex.'\\'.substr($string, 19, 2).'\\'.substr($string, 21, 2);
+ $hex = $hex.'\\'.substr($string, 24, 2).'\\'.substr($string, 26, 2).'\\'.substr($string, 28, 2).'\\'.substr($string, 30, 2).'\\'.substr($string, 32, 2).'\\'.substr($string, 34, 2);
+
+ return $hex;
+ }
+
+ /**
+ * Round a Windows timestamp down to seconds and remove
+ * the seconds between 1601-01-01 and 1970-01-01.
+ *
+ * @param float $windowsTime
+ *
+ * @return float
+ */
+ public static function convertWindowsTimeToUnixTime($windowsTime)
+ {
+ return round($windowsTime / 10000000) - 11644473600;
+ }
+
+ /**
+ * Convert a Unix timestamp to Windows timestamp.
+ *
+ * @param float $unixTime
+ *
+ * @return float
+ */
+ public static function convertUnixTimeToWindowsTime($unixTime)
+ {
+ return ($unixTime + 11644473600) * 10000000;
+ }
+
+ /**
+ * Validates that the inserted string is an object SID.
+ *
+ * @param string $sid
+ *
+ * @return bool
+ */
+ public static function isValidSid($sid)
+ {
+ return (bool) preg_match("/^S-\d(-\d{1,10}){1,16}$/i", $sid);
+ }
+
+ /**
+ * Validates that the inserted string is an object GUID.
+ *
+ * @param string $guid
+ *
+ * @return bool
+ */
+ public static function isValidGuid($guid)
+ {
+ return (bool) preg_match('/^([0-9a-fA-F]){8}(-([0-9a-fA-F]){4}){3}-([0-9a-fA-F]){12}$/', $guid);
+ }
+}