From 460ea5a4790fae9afa3da184db512335371c8ba7 Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Tue, 15 Nov 2022 18:29:01 +0100 Subject: [PATCH 1/9] first commit, not completely working at the moment --- plugins/ldap-mail-accounts/LdapConfig.php | 77 ++++ plugins/ldap-mail-accounts/LdapException.php | 5 + .../ldap-mail-accounts/LdapMailAccounts.php | 344 ++++++++++++++++++ plugins/ldap-mail-accounts/index.php | 142 ++++++++ 4 files changed, 568 insertions(+) create mode 100644 plugins/ldap-mail-accounts/LdapConfig.php create mode 100644 plugins/ldap-mail-accounts/LdapException.php create mode 100644 plugins/ldap-mail-accounts/LdapMailAccounts.php create mode 100644 plugins/ldap-mail-accounts/index.php diff --git a/plugins/ldap-mail-accounts/LdapConfig.php b/plugins/ldap-mail-accounts/LdapConfig.php new file mode 100644 index 000000000..794640a61 --- /dev/null +++ b/plugins/ldap-mail-accounts/LdapConfig.php @@ -0,0 +1,77 @@ +server = trim($config->Get("plugin", self::CONFIG_SERVER)); + $ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3)); + $ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER)); + $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); + $ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE)); + $ldap->user_objectclass = trim($config->Get("plugin", self::CONFIG_USER_OBJECTCLASS)); + $ldap->user_field_name = trim($config->Get("plugin", self::CONFIG_USER_FIELD_NAME)); + $ldap->user_field_search = trim($config->Get("plugin", self::CONFIG_USER_FIELD_SEARCH)); + $ldap->user_field_mail = trim($config->Get("plugin", self::CONFIG_USER_FIELD_MAIL)); + + /* Not needed at the moment + $ldap->group_get = (bool)trim($config->Get("plugin", self::CONFIG_GROUP_GET)); + $ldap->group_base = trim($config->Get("plugin", self::CONFIG_GROUP_BASE)); + $ldap->group_objectclass = trim($config->Get("plugin", self::CONFIG_GROUP_OBJECTCLASS)); + $ldap->group_field_name = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_NAME)); + $ldap->group_field_member = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MEMBER)); + $ldap->group_field_mail = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MAIL)); + $ldap->group_sender_format = trim($config->Get("plugin", self::CONFIG_GROUP_SENDER_FORMAT)); + */ + + return $ldap; + } +} \ No newline at end of file diff --git a/plugins/ldap-mail-accounts/LdapException.php b/plugins/ldap-mail-accounts/LdapException.php new file mode 100644 index 000000000..df489e577 --- /dev/null +++ b/plugins/ldap-mail-accounts/LdapException.php @@ -0,0 +1,5 @@ +config = $config; + $this->logger = $logger; + + // Check if LDAP is available + if (!extension_loaded('ldap') || !function_exists('ldap_connect')) { + $this->ldapAvailable = false; + $logger->Write("The LDAP extension is not available!", \LOG_WARNING, self::LOG_KEY); + return; + } + + $this->Connect(); + } + + /** + * @inheritDoc + * + * @param Account $oAccount + * @return bool $success + */ + public function AddLdapMailAccounts(Account $oAccount): bool + { + try { + $this->EnsureBound(); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + // Try to get account information. Login() returns the username of the user and removes the domainname + // if this was configured inside the domain config. + $username = @ldap_escape($oAccount->Login(), "", LDAP_ESCAPE_FILTER); + + try { + $mailAddressResults = $this->FindLdapResults( + $this->config->user_field_search, + $username, + $this->config->user_base, + $this->config->user_objectclass, + $this->config->user_field_name, + $this->config->user_field_mail + ); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + if (count($mailAddressResults) < 1) { + $this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY); + return []; + } else if (count($mailAddressResults) == 1) { + $this->logger->Write("Found only one match for user $username, no additional mail adresses found", \LOG_NOTICE, self::LOG_KEY); + } + + //From: https://github.com/the-djmaze/snappymail/issues/616 + + $oActions = \RainLoop\Api::Actions(); + $oMainAccount = $oActions->getMainAccountFromToken(); + + if (!$oActions->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { + return $oActions->FalseResponse(__FUNCTION__); + } + + $aAccounts = $oActions->GetAccounts($oMainAccount); + + $sPassword = $oActions->GetActionParam('Password', ''); + $bNew = '1' === (string)$oActions->GetActionParam('New', '1'); + + foreach($mailAddressResults as $mailAddressResult) + { + $sUsername = $mailAddressResult->$username; + $sEmail = \MailSo\Base\Utils::IdnToAscii($sUsername, true); + if ($bNew && ($oMainAccount->Email() === $sUsername || isset($aAccounts[$sUsername]))) { + //Account already exists + return false; + } + + if ($bNew || $sPassword) { + $oNewAccount = $oActions->LoginProcess($sUsername, $sPassword, false, false); + $aAccounts[$sUsername] = $oNewAccount->asTokenArray($oMainAccount); + } else { + $aAccounts[$sUsername] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sUsername]); + } + + if ($aAccounts[$sUsername]) { + $aAccounts[$sUsername]['name'] = $mailAddressResult->$name; + $oActions->SetAccounts($oMainAccount, $aAccounts); + } + } + +/* Not needed at the moment + if (!$this->config->group_get) + return $identities; + + try { + $groupResults = $this->FindLdapResults( + $this->config->group_field_member, + $userResult->dn, + $this->config->group_base, + $this->config->group_objectclass, + $this->config->group_field_name, + $this->config->group_field_mail + ); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + foreach ($groupResults as $group) { + foreach ($group->emails as $email) { + $name = $this->config->group_sender_format; + $name = str_replace("#USER#", $userResult->name, $name); + $name = str_replace("#GROUP#", $group->name, $name); + + $identity = new Identity($email, $email); + $identity->SetName($name); + $identity->SetBcc($email); + + $identities[] = $identity; + } + } +*/ + + return true; + } + + /** + * @inheritDoc + * @throws \RainLoop\Exceptions\ClientException + */ + public function SetIdentities(Account $account, array $identities): void + { + throw new \RainLoop\Exceptions\ClientException("Ldap identities provider does not support storage"); + } + + /** + * @inheritDoc + */ + public function SupportsStore(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function Name(): string + { + return "Ldap"; + } + + /** @throws LdapException */ + private function EnsureConnected(): void + { + if ($this->ldapConnected) return; + + $res = $this->Connect(); + if (!$res) + $this->HandleLdapError("Connect"); + } + + private function Connect(): bool + { + // Set up connection + $ldap = @ldap_connect($this->config->server); + if ($ldap === false) { + $this->ldapAvailable = false; + return false; + } + + // Set protocol version + $option = @ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, $this->config->protocol); + if (!$option) { + $this->ldapAvailable = false; + return false; + } + + $this->ldap = $ldap; + $this->ldapConnected = true; + return true; + } + + /** @throws LdapException */ + private function EnsureBound(): void + { + if ($this->ldapBound) return; + $this->EnsureConnected(); + + $res = $this->Bind(); + if (!$res) + $this->HandleLdapError("Bind"); + } + + private function Bind(): bool + { + // Bind to LDAP here + $bindResult = @ldap_bind($this->ldap, $this->config->bind_user, $this->config->bind_password); + if (!$bindResult) { + $this->ldapAvailable = false; + return false; + } + + $this->ldapBound = true; + return true; + } + + /** + * @param string $op + * @throws LdapException + */ + private function HandleLdapError(string $op = ""): void + { + // Obtain LDAP error and write logs + $errorNo = @ldap_errno($this->ldap); + $errorMsg = @ldap_error($this->ldap); + + $message = empty($op) ? "LDAP Error: {$errorMsg} ({$errorNo})" : "LDAP Error during {$op}: {$errorMsg} ({$errorNo})"; + $this->logger->Write($message, \LOG_ERR, self::LOG_KEY); + throw new LdapException($message, $errorNo); + } + + /** + * @param string $searchField + * @param string $searchValue + * @param string $searchBase + * @param string $objectClass + * @param string $nameField + * @param string $mailField + * @return LdapResult[] + * @throws LdapException + */ + private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField): array + { + $this->EnsureBound(); + + $nameField = strtolower($nameField); + $mailField = strtolower($mailField); + + //TODO: temporary fixed to concat with Base DN - should be variable + $filter = "(&(objectclass=$objectClass)($searchField=uid=$searchValue,$searchBase))"; + $this->logger->Write("Filter=$filter", \LOG_NOTICE, self::LOG_KEY); + + $ldapResult = @ldap_search($this->ldap, $searchBase, $filter, ['dn', $mailField, $nameField]); + if (!$ldapResult) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $entries = @ldap_get_entries($this->ldap, $ldapResult); + if (!$entries) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $results = []; + for ($i = 0; $i < $entries["count"]; $i++) { + $entry = $entries[$i]; + + $result = new LdapResult(); + $result->dn = $entry["dn"]; + $result->name = $this->LdapGetAttribute($entry, $nameField, true, true); + $result->username = $this->LdapGetAttribute($entry, $mailField, true, true); + + $results[] = $result; + } + + return $results; + } + + /** + * @param array $entry + * @param string $attribute + * @param bool $single + * @param bool $required + * @return string|string[] + */ + private function LdapGetAttribute(array $entry, string $attribute, bool $single = true, bool $required = false) + { + //INFO if $single=false a array is returned. needet for identities, but not for additional mail accounts / usernames + //TODO: remove line when not needed anymore + $this->logger->Write("Attribute=$attribute, DN={$entry['dn']}", \LOG_NOTICE, self::LOG_KEY); + + if (!isset($entry[$attribute])) { + if ($required) + $this->logger->Write("Attribute $attribute not found on object {$entry['dn']} while required", \LOG_NOTICE, self::LOG_KEY); + + return $single ? "" : []; + } + + if ($single) { + if ($entry[$attribute]["count"] > 1) + $this->logger->Write("Attribute $attribute is multivalues while only a single value is expected", \LOG_NOTICE, self::LOG_KEY); + + return $entry[$attribute][0]; + } + + $result = $entry[$attribute]; + unset($result["count"]); + return array_values($result); + } +} + +class LdapResult +{ + /** @var string */ + public $dn; + + /** @var string */ + public $name; + + /** @var string */ + public $username; +} diff --git a/plugins/ldap-mail-accounts/index.php b/plugins/ldap-mail-accounts/index.php new file mode 100644 index 000000000..b859e5975 --- /dev/null +++ b/plugins/ldap-mail-accounts/index.php @@ -0,0 +1,142 @@ +addHook("login.success", 'AddLdapMailAccounts'); + } + + public function AddLdapMailAccounts(Account $oAccount) + { + // Set up config + $config = LdapConfig::MakeConfig($this->Config()); + + $oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger()); + + $oldapMailAccounts->AddLdapMailAccounts($oAccount); + } + + protected function configMapping(): array + { + return [ + Property::NewInstance(LdapConfig::CONFIG_SERVER) + ->SetLabel("LDAP Server URL") + ->SetPlaceholder("ldap://server:port") + ->SetType(PluginPropertyType::STRING), + + Property::NewInstance(LdapConfig::CONFIG_PROTOCOL_VERSION) + ->SetLabel("LDAP Protocol Version") + ->SetType(PluginPropertyType::SELECTION) + ->SetDefaultValue([2, 3]), + + Property::NewInstance(LdapConfig::CONFIG_BIND_USER) + ->SetLabel("Bind User DN") + ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind") + ->SetType(PluginPropertyType::STRING), + + Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) + ->SetLabel("Bind User Password") + ->SetDescription("Leave empty for anonymous bind") + ->SetType(PluginPropertyType::PASSWORD), + + Property::NewInstance(LdapConfig::CONFIG_USER_OBJECTCLASS) + ->SetLabel("User object class") + ->SetType(PluginPropertyType::STRING) + ->SetDefaultValue("user"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_SEARCH) + ->SetLabel("User search field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The fieldname inside the user object to search for the email/username the user logged in with") + ->SetDefaultValue("member"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_MAIL) + ->SetLabel("Additional account mail field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field containing the mail address of found additional mail accounts") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_NAME) + ->SetLabel("Additional account name field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field containing the default sender name of the found additional mail accounts") + ->SetDefaultValue("displayName"), + + Property::NewInstance(LdapConfig::CONFIG_USER_BASE) + ->SetLabel("User base DN") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for users") + + /* Not needed at the moment + + Property::NewInstance(LdapConfig::CONFIG_GROUP_GET) + ->SetLabel("Find groups?") + ->SetType(PluginPropertyType::BOOL) + ->SetDescription("Whether or not to search for groups") + ->SetDefaultValue(true), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_OBJECTCLASS) + ->SetLabel("Group object class") + ->SetType(PluginPropertyType::STRING) + ->SetDefaultValue("group"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MAIL) + ->SetLabel("Group mail field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object listing all identities (email addresses) of the group") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_NAME) + ->SetLabel("Group name field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object with the name") + ->SetDefaultValue("cn"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MEMBER) + ->SetLabel("Group member field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object with all member DNs") + ->SetDefaultValue("member"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_SENDER_FORMAT) + ->SetLabel("Group mail sender format") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The sender name format for group addresses. Available template values: #USER# for the user name and #GROUP# for the group name") + ->SetDefaultValue("#USER# || #GROUP#"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_BASE) + ->SetLabel("Group base DN") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for groups") + */ + ]; + } +} From d8b9907e3217a7c0c4b3b2d9e7e0746055cb85bd Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Fri, 25 Nov 2022 10:42:14 +0100 Subject: [PATCH 2/9] working prototype --- plugins/ldap-mail-accounts/LdapConfig.php | 68 ++---- .../ldap-mail-accounts/LdapMailAccounts.php | 201 +++++++++++------- plugins/ldap-mail-accounts/index.php | 87 +++----- 3 files changed, 174 insertions(+), 182 deletions(-) diff --git a/plugins/ldap-mail-accounts/LdapConfig.php b/plugins/ldap-mail-accounts/LdapConfig.php index 794640a61..22361e416 100644 --- a/plugins/ldap-mail-accounts/LdapConfig.php +++ b/plugins/ldap-mail-accounts/LdapConfig.php @@ -11,43 +11,25 @@ class LdapConfig public const CONFIG_BIND_USER = "bind_user"; public const CONFIG_BIND_PASSWORD = "bind_password"; - public const CONFIG_USER_BASE = "user_base"; - public const CONFIG_USER_OBJECTCLASS = "user_objectclass"; - public const CONFIG_USER_FIELD_NAME = "user_field_name"; - public const CONFIG_USER_FIELD_SEARCH = "user_field_search"; - public const CONFIG_USER_FIELD_MAIL = "user_field_mail"; - - /* Not needed at the moment - - public const CONFIG_GROUP_GET = "group_get"; - public const CONFIG_GROUP_BASE = "group_base"; - public const CONFIG_GROUP_OBJECTCLASS = "group_objectclass"; - public const CONFIG_GROUP_FIELD_NAME = "group_field_name"; - public const CONFIG_GROUP_FIELD_MEMBER = "group_field_member"; - public const CONFIG_GROUP_FIELD_MAIL = "group_field_mail"; - public const CONFIG_GROUP_SENDER_FORMAT = "group_sender_format"; - */ + public const CONFIG_BASE = "base"; + public const CONFIG_OBJECTCLASS = "objectclass"; + public const CONFIG_FIELD_NAME = "field_name"; + public const CONFIG_FIELD_SEARCH = "field_search"; + public const CONFIG_FIELD_USERNAME = "field_username"; + public const CONFIG_SEARCH_STRING = "search_string"; + public const CONFIG_FIELD_MAIL_DOMAIN = "field_domain"; public $server; public $protocol; public $bind_user; public $bind_password; - public $user_base; - public $user_objectclass; - public $user_field_name; - public $user_field_search; - public $user_field_mail; - - /* Not needed at the moment - - public $group_get; - public $group_base; - public $group_objectclass; - public $group_field_name; - public $group_field_member; - public $group_field_mail; - public $group_sender_format; - */ + public $base; + public $objectclass; + public $field_name; + public $field_search; + public $field_username; + public $search_string; + public $field_domain; public static function MakeConfig(Plugin $config): LdapConfig { @@ -56,21 +38,13 @@ class LdapConfig $ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3)); $ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER)); $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); - $ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE)); - $ldap->user_objectclass = trim($config->Get("plugin", self::CONFIG_USER_OBJECTCLASS)); - $ldap->user_field_name = trim($config->Get("plugin", self::CONFIG_USER_FIELD_NAME)); - $ldap->user_field_search = trim($config->Get("plugin", self::CONFIG_USER_FIELD_SEARCH)); - $ldap->user_field_mail = trim($config->Get("plugin", self::CONFIG_USER_FIELD_MAIL)); - - /* Not needed at the moment - $ldap->group_get = (bool)trim($config->Get("plugin", self::CONFIG_GROUP_GET)); - $ldap->group_base = trim($config->Get("plugin", self::CONFIG_GROUP_BASE)); - $ldap->group_objectclass = trim($config->Get("plugin", self::CONFIG_GROUP_OBJECTCLASS)); - $ldap->group_field_name = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_NAME)); - $ldap->group_field_member = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MEMBER)); - $ldap->group_field_mail = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MAIL)); - $ldap->group_sender_format = trim($config->Get("plugin", self::CONFIG_GROUP_SENDER_FORMAT)); - */ + $ldap->base = trim($config->Get("plugin", self::CONFIG_BASE)); + $ldap->objectclass = trim($config->Get("plugin", self::CONFIG_OBJECTCLASS)); + $ldap->field_name = trim($config->Get("plugin", self::CONFIG_FIELD_NAME)); + $ldap->field_search = trim($config->Get("plugin", self::CONFIG_FIELD_SEARCH)); + $ldap->field_username = trim($config->Get("plugin", self::CONFIG_FIELD_USERNAME)); + $ldap->search_string = trim($config->Get("plugin", self::CONFIG_SEARCH_STRING)); + $ldap->field_domain = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_DOMAIN)); return $ldap; } diff --git a/plugins/ldap-mail-accounts/LdapMailAccounts.php b/plugins/ldap-mail-accounts/LdapMailAccounts.php index 9f02fd6fd..e5db799bd 100644 --- a/plugins/ldap-mail-accounts/LdapMailAccounts.php +++ b/plugins/ldap-mail-accounts/LdapMailAccounts.php @@ -57,104 +57,87 @@ class LdapMailAccounts try { $this->EnsureBound(); } catch (LdapException $e) { - return []; // exceptions are only thrown from the handleerror function that does logging already + return false; // exceptions are only thrown from the handleerror function that does logging already } - // Try to get account information. Login() returns the username of the user and removes the domainname - // if this was configured inside the domain config. + // Try to get account information. Login() returns the username of the user + // and removes the domainname if this was configured inside the domain config. $username = @ldap_escape($oAccount->Login(), "", LDAP_ESCAPE_FILTER); + $searchString = $this->config->search_string; + + // Replace placeholders inside the ldap search string with actual values + $searchString = str_replace("#USERNAME#", $username, $searchString); + $searchString = str_replace("#BASE_DN#", $this->config->base, $searchString); + + $this->logger->Write("ldap search string after replacement of placeholders: $searchString", \LOG_NOTICE, self::LOG_KEY); + try { $mailAddressResults = $this->FindLdapResults( - $this->config->user_field_search, - $username, - $this->config->user_base, - $this->config->user_objectclass, - $this->config->user_field_name, - $this->config->user_field_mail + $this->config->field_search, + $searchString, + $this->config->base, + $this->config->objectclass, + $this->config->field_name, + $this->config->field_username, + $this->config->field_domain ); - } catch (LdapException $e) { - return []; // exceptions are only thrown from the handleerror function that does logging already + } + catch (LdapException $e) { + return false; // exceptions are only thrown from the handleerror function that does logging already } - if (count($mailAddressResults) < 1) { $this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY); - return []; + return false; } else if (count($mailAddressResults) == 1) { $this->logger->Write("Found only one match for user $username, no additional mail adresses found", \LOG_NOTICE, self::LOG_KEY); + return true; } - //From: https://github.com/the-djmaze/snappymail/issues/616 + //Basing on https://github.com/the-djmaze/snappymail/issues/616 $oActions = \RainLoop\Api::Actions(); - $oMainAccount = $oActions->getMainAccountFromToken(); + //Check if SnappyMail is configured to allow additional accounts if (!$oActions->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { return $oActions->FalseResponse(__FUNCTION__); } - $aAccounts = $oActions->GetAccounts($oMainAccount); - - $sPassword = $oActions->GetActionParam('Password', ''); - $bNew = '1' === (string)$oActions->GetActionParam('New', '1'); + $aAccounts = $oActions->GetAccounts($oAccount); foreach($mailAddressResults as $mailAddressResult) { - $sUsername = $mailAddressResult->$username; - $sEmail = \MailSo\Base\Utils::IdnToAscii($sUsername, true); - if ($bNew && ($oMainAccount->Email() === $sUsername || isset($aAccounts[$sUsername]))) { - //Account already exists - return false; - } + $sUsername = $mailAddressResult->username; + $sDomain = $mailAddressResult->domain; - if ($bNew || $sPassword) { - $oNewAccount = $oActions->LoginProcess($sUsername, $sPassword, false, false); - $aAccounts[$sUsername] = $oNewAccount->asTokenArray($oMainAccount); - } else { - $aAccounts[$sUsername] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sUsername]); - } +$this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount->Login() . " E-Mail: " . $oAccount->Email(), \LOG_NOTICE, self::LOG_KEY); - if ($aAccounts[$sUsername]) { - $aAccounts[$sUsername]['name'] = $mailAddressResult->$name; - $oActions->SetAccounts($oMainAccount, $aAccounts); - } + //only execute if the found account isn't already in the list of additional accounts + //and if the found account is different from the main account + //https://github.com/the-djmaze/snappymail/issues/705 should solve the missing logging if a domain is found in ldap + //but is not defined in the admin panel of SnappyMail + if (!$aAccounts["$sUsername@$sDomain"] && $oAccount->Email() !== "$sUsername@$sDomain") + { +$this->logger->Write("test $sUsername@$sDomain", \LOG_NOTICE, self::LOG_KEY); + //Try to login the user with the same password as the primary account has + //if this fails the user will see the new mail addresses but will be asked for the correct password + $sPass = $oAccount->Password(); + $oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, "$sUsername@$sDomain", $sUsername, $sPass); + $aAccounts["$sUsername@$sDomain"] = $oNewAccount->asTokenArray($oAccount); + } } -/* Not needed at the moment - if (!$this->config->group_get) - return $identities; - - try { - $groupResults = $this->FindLdapResults( - $this->config->group_field_member, - $userResult->dn, - $this->config->group_base, - $this->config->group_objectclass, - $this->config->group_field_name, - $this->config->group_field_mail - ); - } catch (LdapException $e) { - return []; // exceptions are only thrown from the handleerror function that does logging already + if ($aAccounts) + { + $oActions->SetAccounts($oAccount, $aAccounts); + return true; } - foreach ($groupResults as $group) { - foreach ($group->emails as $email) { - $name = $this->config->group_sender_format; - $name = str_replace("#USER#", $userResult->name, $name); - $name = str_replace("#GROUP#", $group->name, $name); - - $identity = new Identity($email, $email); - $identity->SetName($name); - $identity->SetBcc($email); - - $identities[] = $identity; - } - } -*/ - - return true; + return false; } + + /** * @inheritDoc * @throws \RainLoop\Exceptions\ClientException @@ -252,26 +235,33 @@ class LdapMailAccounts /** * @param string $searchField - * @param string $searchValue + * @param string $searchString * @param string $searchBase * @param string $objectClass * @param string $nameField - * @param string $mailField + * @param string $usernameField + * @param string $domainField * @return LdapResult[] * @throws LdapException */ - private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField): array - { - $this->EnsureBound(); - + private function FindLdapResults( + string $searchField, + string $searchString, + string $searchBase, + string $objectClass, + string $nameField, + string $usernameField, + string $domainField): array + { + $this->EnsureBound(); $nameField = strtolower($nameField); - $mailField = strtolower($mailField); + $usernameField = strtolower($usernameField); + $domainField = strtolower($domainField); - //TODO: temporary fixed to concat with Base DN - should be variable - $filter = "(&(objectclass=$objectClass)($searchField=uid=$searchValue,$searchBase))"; - $this->logger->Write("Filter=$filter", \LOG_NOTICE, self::LOG_KEY); + $filter = "(&(objectclass=$objectClass)($searchField=$searchString))"; + $this->logger->Write("Used ldap filter to search for additional mail accounts: $filter", \LOG_NOTICE, self::LOG_KEY); - $ldapResult = @ldap_search($this->ldap, $searchBase, $filter, ['dn', $mailField, $nameField]); + $ldapResult = @ldap_search($this->ldap, $searchBase, $filter, ['dn', $usernameField, $nameField, $domainField]); if (!$ldapResult) { $this->HandleLdapError("Fetch $objectClass"); return []; @@ -283,6 +273,7 @@ class LdapMailAccounts return []; } + // Save the found ldap entries into a LdapResult object and return them $results = []; for ($i = 0; $i < $entries["count"]; $i++) { $entry = $entries[$i]; @@ -290,7 +281,12 @@ class LdapMailAccounts $result = new LdapResult(); $result->dn = $entry["dn"]; $result->name = $this->LdapGetAttribute($entry, $nameField, true, true); - $result->username = $this->LdapGetAttribute($entry, $mailField, true, true); + + $result->username = $this->LdapGetAttribute($entry, $usernameField, true, true); + $result->username = $this->RemoveEventualDomainPart($result->username); + + $result->domain = $this->LdapGetAttribute($entry, $domainField, true, true); + $result->domain = $this->RemoveEventualLocalPart($result->domain); $results[] = $result; } @@ -298,6 +294,52 @@ class LdapMailAccounts return $results; } + /** + * Removes an eventually found domain-part of an email address + * + * If the input string contains an '@' character the function returns the local-part before the '@'\ + * If no '@' character can be found the input string is returned. + * + * @param string $sInput + * @return string + */ + public static function RemoveEventualDomainPart(string $sInput) : string + { + // Copy of \MailSo\Base\Utils::GetAccountNameFromEmail to make sure that also after eventual future + // updates the input string gets returned when no '@' is found (GetDomainFromEmail already doesn't do this) + $sResult = ''; + if (\strlen($sInput)) + { + $iPos = \strrpos($sInput, '@'); + $sResult = (false === $iPos) ? $sInput : \substr($sInput, 0, $iPos); + } + + return $sResult; + } + + + /** + * Removes an eventually found local-part of an email address + * + * If the input string contains an '@' character the function returns the domain-part behind the '@'\ + * If no '@' character can be found the input string is returned. + * + * @param string $sInput + * @return string + */ + public static function RemoveEventualLocalPart(string $sInput) : string + { + $sResult = ''; + if (\strlen($sInput)) + { + $iPos = \strrpos($sInput, '@'); + $sResult = (false === $iPos) ? $sInput : \substr($sInput, $iPos + 1); + } + + return $sResult; + } + + /** * @param array $entry * @param string $attribute @@ -341,4 +383,7 @@ class LdapResult /** @var string */ public $username; + + /** @var string */ + public $domain; } diff --git a/plugins/ldap-mail-accounts/index.php b/plugins/ldap-mail-accounts/index.php index b859e5975..561f25519 100644 --- a/plugins/ldap-mail-accounts/index.php +++ b/plugins/ldap-mail-accounts/index.php @@ -34,6 +34,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin $this->addHook("login.success", 'AddLdapMailAccounts'); } + // Function gets called by RainLoop/Actions/User.php public function AddLdapMailAccounts(Account $oAccount) { // Set up config @@ -59,84 +60,56 @@ class LdapMailAccountsPlugin extends AbstractPlugin Property::NewInstance(LdapConfig::CONFIG_BIND_USER) ->SetLabel("Bind User DN") - ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind") + ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind.") ->SetType(PluginPropertyType::STRING), Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) ->SetLabel("Bind User Password") - ->SetDescription("Leave empty for anonymous bind") + ->SetDescription("Leave empty for anonymous bind.") ->SetType(PluginPropertyType::PASSWORD), - Property::NewInstance(LdapConfig::CONFIG_USER_OBJECTCLASS) - ->SetLabel("User object class") + Property::NewInstance(LdapConfig::CONFIG_OBJECTCLASS) + ->SetLabel("Object class") ->SetType(PluginPropertyType::STRING) ->SetDefaultValue("user"), - Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_SEARCH) - ->SetLabel("User search field") - ->SetType(PluginPropertyType::STRING) - ->SetDescription("The fieldname inside the user object to search for the email/username the user logged in with") - ->SetDefaultValue("member"), - - Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_MAIL) - ->SetLabel("Additional account mail field") - ->SetType(PluginPropertyType::STRING) - ->SetDescription("The field containing the mail address of found additional mail accounts") - ->SetDefaultValue("mail"), - - Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_NAME) - ->SetLabel("Additional account name field") - ->SetType(PluginPropertyType::STRING) - ->SetDescription("The field containing the default sender name of the found additional mail accounts") - ->SetDefaultValue("displayName"), - - Property::NewInstance(LdapConfig::CONFIG_USER_BASE) + Property::NewInstance(LdapConfig::CONFIG_BASE) ->SetLabel("User base DN") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The base DN to search in for users") + ->SetDescription("The base DN to search in for users."), - /* Not needed at the moment - - Property::NewInstance(LdapConfig::CONFIG_GROUP_GET) - ->SetLabel("Find groups?") - ->SetType(PluginPropertyType::BOOL) - ->SetDescription("Whether or not to search for groups") - ->SetDefaultValue(true), - - Property::NewInstance(LdapConfig::CONFIG_GROUP_OBJECTCLASS) - ->SetLabel("Group object class") + Property::NewInstance(LdapConfig::CONFIG_FIELD_SEARCH) + ->SetLabel("Search field") ->SetType(PluginPropertyType::STRING) - ->SetDefaultValue("group"), + ->SetDescription("The name of the ldap attribute that has to contain the set 'LDAP search string'.") + ->SetDefaultValue("member"), - Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MAIL) - ->SetLabel("Group mail field") + Property::NewInstance(LdapConfig::CONFIG_SEARCH_STRING) + ->SetLabel("LDAP search string") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The field in the group object listing all identities (email addresses) of the group") - ->SetDefaultValue("mail"), + ->SetDescription("The search string used to find ldap objects of mail accounts the user has access to. + \nPossible placeholers:\n#USERNAME# - replaced with the username of the actual SnappyMail user + \n#BASE_DN# - replaced with the value inside the field 'User base DN'."), - Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_NAME) - ->SetLabel("Group name field") + Property::NewInstance(LdapConfig::CONFIG_FIELD_USERNAME) + ->SetLabel("Username field of additional account") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The field in the group object with the name") - ->SetDefaultValue("cn"), + ->SetDescription("The field containing the username of the found additional mail account. + \nThis username gets used by SnappyMail to login to the additional mail account.") + ->SetDefaultValue("uid"), - Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MEMBER) - ->SetLabel("Group member field") + Property::NewInstance(LdapConfig::CONFIG_FIELD_MAIL_DOMAIN) + ->SetLabel("Domain name field of additional account") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The field in the group object with all member DNs") - ->SetDefaultValue("member"), + ->SetDescription("The field containing the domain name of the found additional mail account. + \nThis domain gets looked up by SnappyMail to choose the right connection parameters at logging in to the additional mail account.") + ->SetDefaultValue("mail"), - Property::NewInstance(LdapConfig::CONFIG_GROUP_SENDER_FORMAT) - ->SetLabel("Group mail sender format") + Property::NewInstance(LdapConfig::CONFIG_FIELD_NAME) + ->SetLabel("Additional account name field") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The sender name format for group addresses. Available template values: #USER# for the user name and #GROUP# for the group name") - ->SetDefaultValue("#USER# || #GROUP#"), - - Property::NewInstance(LdapConfig::CONFIG_GROUP_BASE) - ->SetLabel("Group base DN") - ->SetType(PluginPropertyType::STRING) - ->SetDescription("The base DN to search in for groups") - */ + ->SetDescription("The field containing the default sender name of the found additional mail account.") + ->SetDefaultValue("displayName") ]; } } From 73faa027953d0c067fbed4db6f2a916f8195523b Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:41:31 +0100 Subject: [PATCH 3/9] better description of config parameters --- plugins/ldap-mail-accounts/index.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/plugins/ldap-mail-accounts/index.php b/plugins/ldap-mail-accounts/index.php index 561f25519..58b2965d3 100644 --- a/plugins/ldap-mail-accounts/index.php +++ b/plugins/ldap-mail-accounts/index.php @@ -15,10 +15,10 @@ class LdapMailAccountsPlugin extends AbstractPlugin VERSION = '1.0', AUTHOR = 'cm-schl', URL = 'https://github.com/cm-sch', - RELEASE = '2022-11-11', + RELEASE = '2022-11-25', REQUIRED = '2.20.0', CATEGORY = 'Accounts', - DESCRIPTION = 'Adds functionality to import mail accounts from LDAP. Basing on the work of FWest98 (https://github.com/FWest98).'; + DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).'; public function __construct() { @@ -59,29 +59,30 @@ class LdapMailAccountsPlugin extends AbstractPlugin ->SetDefaultValue([2, 3]), Property::NewInstance(LdapConfig::CONFIG_BIND_USER) - ->SetLabel("Bind User DN") + ->SetLabel("LDAP Username") ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind.") ->SetType(PluginPropertyType::STRING), Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) - ->SetLabel("Bind User Password") + ->SetLabel("LDAP Password") ->SetDescription("Leave empty for anonymous bind.") ->SetType(PluginPropertyType::PASSWORD), Property::NewInstance(LdapConfig::CONFIG_OBJECTCLASS) ->SetLabel("Object class") ->SetType(PluginPropertyType::STRING) + ->SetDescription("The object class to use when searching for additional mail accounts of the logged in SnappyMail user") ->SetDefaultValue("user"), Property::NewInstance(LdapConfig::CONFIG_BASE) - ->SetLabel("User base DN") + ->SetLabel("Base DN") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The base DN to search in for users."), + ->SetDescription("The base DN to search in for additional mail accounts of the logged in SnappyMail user"), Property::NewInstance(LdapConfig::CONFIG_FIELD_SEARCH) ->SetLabel("Search field") ->SetType(PluginPropertyType::STRING) - ->SetDescription("The name of the ldap attribute that has to contain the set 'LDAP search string'.") + ->SetDescription("The name of the ldap attribute that has to contain the here defined 'LDAP search string'.") ->SetDefaultValue("member"), Property::NewInstance(LdapConfig::CONFIG_SEARCH_STRING) @@ -89,20 +90,23 @@ class LdapMailAccountsPlugin extends AbstractPlugin ->SetType(PluginPropertyType::STRING) ->SetDescription("The search string used to find ldap objects of mail accounts the user has access to. \nPossible placeholers:\n#USERNAME# - replaced with the username of the actual SnappyMail user - \n#BASE_DN# - replaced with the value inside the field 'User base DN'."), + \n#BASE_DN# - replaced with the value inside the field 'User base DN'.") + ->SetDefaultValue("uid=#USERNAME#"), Property::NewInstance(LdapConfig::CONFIG_FIELD_USERNAME) ->SetLabel("Username field of additional account") ->SetType(PluginPropertyType::STRING) ->SetDescription("The field containing the username of the found additional mail account. - \nThis username gets used by SnappyMail to login to the additional mail account.") + \nThis username gets used by SnappyMail to login to the additional mail account. + \nIf this field contains an email address, only the local-part before the @ is used.") ->SetDefaultValue("uid"), Property::NewInstance(LdapConfig::CONFIG_FIELD_MAIL_DOMAIN) ->SetLabel("Domain name field of additional account") ->SetType(PluginPropertyType::STRING) ->SetDescription("The field containing the domain name of the found additional mail account. - \nThis domain gets looked up by SnappyMail to choose the right connection parameters at logging in to the additional mail account.") + \nThis domain gets looked up by SnappyMail to choose the right connection parameters at logging in to the additional mail account. + \nIf this field contains an email address, only the domain-part after the @ is used.") ->SetDefaultValue("mail"), Property::NewInstance(LdapConfig::CONFIG_FIELD_NAME) From 7e3b81add97b6fe7b374a3380d5dde01b197d3b7 Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:31:27 +0100 Subject: [PATCH 4/9] remove not needet logging --- plugins/ldap-mail-accounts/LdapMailAccounts.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/ldap-mail-accounts/LdapMailAccounts.php b/plugins/ldap-mail-accounts/LdapMailAccounts.php index e5db799bd..dc1842d39 100644 --- a/plugins/ldap-mail-accounts/LdapMailAccounts.php +++ b/plugins/ldap-mail-accounts/LdapMailAccounts.php @@ -350,8 +350,6 @@ $this->logger->Write("test $sUsername@$sDomain", \LOG_NOTICE, self::LOG_KEY); private function LdapGetAttribute(array $entry, string $attribute, bool $single = true, bool $required = false) { //INFO if $single=false a array is returned. needet for identities, but not for additional mail accounts / usernames - //TODO: remove line when not needed anymore - $this->logger->Write("Attribute=$attribute, DN={$entry['dn']}", \LOG_NOTICE, self::LOG_KEY); if (!isset($entry[$attribute])) { if ($required) From e797d0e5ed7adbd3bcb52066f591ff9d64a94bba Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:37:00 +0100 Subject: [PATCH 5/9] cleanup and documentation --- plugins/ldap-mail-accounts/LdapMailAccounts.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/ldap-mail-accounts/LdapMailAccounts.php b/plugins/ldap-mail-accounts/LdapMailAccounts.php index dc1842d39..3e0714f99 100644 --- a/plugins/ldap-mail-accounts/LdapMailAccounts.php +++ b/plugins/ldap-mail-accounts/LdapMailAccounts.php @@ -49,6 +49,10 @@ class LdapMailAccounts /** * @inheritDoc * + * Add additional mail accounts to the given primary account by looking up the ldap directory + * + * The ldap lookup has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel) + * * @param Account $oAccount * @return bool $success */ @@ -95,7 +99,6 @@ class LdapMailAccounts } //Basing on https://github.com/the-djmaze/snappymail/issues/616 - $oActions = \RainLoop\Api::Actions(); //Check if SnappyMail is configured to allow additional accounts @@ -118,7 +121,6 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount //but is not defined in the admin panel of SnappyMail if (!$aAccounts["$sUsername@$sDomain"] && $oAccount->Email() !== "$sUsername@$sDomain") { -$this->logger->Write("test $sUsername@$sDomain", \LOG_NOTICE, self::LOG_KEY); //Try to login the user with the same password as the primary account has //if this fails the user will see the new mail addresses but will be asked for the correct password $sPass = $oAccount->Password(); From 3aa063d4753bdc160e49ebdd2d8a01b15ede00b9 Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:49:28 +0100 Subject: [PATCH 6/9] added check if found domain is also configured changed LOG_KEY variable --- .../ldap-mail-accounts/LdapMailAccounts.php | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/plugins/ldap-mail-accounts/LdapMailAccounts.php b/plugins/ldap-mail-accounts/LdapMailAccounts.php index 3e0714f99..a39fc8972 100644 --- a/plugins/ldap-mail-accounts/LdapMailAccounts.php +++ b/plugins/ldap-mail-accounts/LdapMailAccounts.php @@ -22,7 +22,7 @@ class LdapMailAccounts /** @var Logger */ private $logger; - private const LOG_KEY = "Ldap"; + private const LOG_KEY = "LDAP MAIL ACCOUNTS PLUGIN"; /** * LdapMailAccount constructor. @@ -99,6 +99,7 @@ class LdapMailAccounts } //Basing on https://github.com/the-djmaze/snappymail/issues/616 + $oActions = \RainLoop\Api::Actions(); //Check if SnappyMail is configured to allow additional accounts @@ -115,17 +116,22 @@ class LdapMailAccounts $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount->Login() . " E-Mail: " . $oAccount->Email(), \LOG_NOTICE, self::LOG_KEY); - //only execute if the found account isn't already in the list of additional accounts - //and if the found account is different from the main account - //https://github.com/the-djmaze/snappymail/issues/705 should solve the missing logging if a domain is found in ldap - //but is not defined in the admin panel of SnappyMail - if (!$aAccounts["$sUsername@$sDomain"] && $oAccount->Email() !== "$sUsername@$sDomain") + //Check if the domain of the found mail address is in the list of configured domains + if ($oActions->DomainProvider()->Load($sDomain, true)) { - //Try to login the user with the same password as the primary account has - //if this fails the user will see the new mail addresses but will be asked for the correct password - $sPass = $oAccount->Password(); - $oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, "$sUsername@$sDomain", $sUsername, $sPass); - $aAccounts["$sUsername@$sDomain"] = $oNewAccount->asTokenArray($oAccount); + //only execute if the found account isn't already in the list of additional accounts + //and if the found account is different from the main account + if (!$aAccounts["$sUsername@$sDomain"] && $oAccount->Email() !== "$sUsername@$sDomain") + { + //Try to login the user with the same password as the primary account has + //if this fails the user will see the new mail addresses but will be asked for the correct password + $sPass = $oAccount->Password(); + $oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, "$sUsername@$sDomain", $sUsername, $sPass); + $aAccounts["$sUsername@$sDomain"] = $oNewAccount->asTokenArray($oAccount); + } + } + else { + $this->logger->Write("Domain $sDomain is not part of configured domains in SnappyMail Admin Panel - mail address $sUsername@$sDomain will not be added.", \LOG_NOTICE, self::LOG_KEY); } } From 79191cf7a3fef1c2efb4efa7dbc573c786eb5be7 Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:28:59 +0100 Subject: [PATCH 7/9] capability to remove mailboxes when user looses right on them, some documentation and setting mailbox name by ldap query --- .../ldap-mail-accounts/LdapMailAccounts.php | 101 ++++++++++-------- plugins/ldap-mail-accounts/index.php | 13 ++- 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/plugins/ldap-mail-accounts/LdapMailAccounts.php b/plugins/ldap-mail-accounts/LdapMailAccounts.php index a39fc8972..313d9b1eb 100644 --- a/plugins/ldap-mail-accounts/LdapMailAccounts.php +++ b/plugins/ldap-mail-accounts/LdapMailAccounts.php @@ -2,6 +2,7 @@ use RainLoop\Enumerations\Capa; use MailSo\Log\Logger; +use RainLoop\Actions; use RainLoop\Model\Account; class LdapMailAccounts @@ -27,9 +28,8 @@ class LdapMailAccounts /** * LdapMailAccount constructor. * - * @param LdapConfig $config - * @param Account $oAccount - * @param Logger $logger + * @param LdapConfig $config LdapConfig object containing the admin configuration for this plugin + * @param Logger $logger Used to write to the logfile */ public function __construct(LdapConfig $config, Logger $logger) { @@ -54,7 +54,7 @@ class LdapMailAccounts * The ldap lookup has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel) * * @param Account $oAccount - * @return bool $success + * @return bool true if additional accounts have been added or no additional accounts where found in . false if an error occured */ public function AddLdapMailAccounts(Account $oAccount): bool { @@ -101,7 +101,7 @@ class LdapMailAccounts //Basing on https://github.com/the-djmaze/snappymail/issues/616 $oActions = \RainLoop\Api::Actions(); - + //Check if SnappyMail is configured to allow additional accounts if (!$oActions->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { return $oActions->FalseResponse(__FUNCTION__); @@ -109,25 +109,43 @@ class LdapMailAccounts $aAccounts = $oActions->GetAccounts($oAccount); + //Search for accounts with suffix " (LDAP)" at the end of the name that where created by this plugin and initially remove them from the + //account array. This only removes the visibility but does not delete the config done by the user. So if a user looses access to a + //mailbox the user will not see the account anymore but the configuration can be restored when the user regains access to it + foreach($aAccounts as $key => $aAccount) + { + if (preg_match("/\s\(LDAP\)$/", $aAccount['name'])) + { + unset($aAccounts[$key]); + } + } + foreach($mailAddressResults as $mailAddressResult) { $sUsername = $mailAddressResult->username; $sDomain = $mailAddressResult->domain; - -$this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount->Login() . " E-Mail: " . $oAccount->Email(), \LOG_NOTICE, self::LOG_KEY); + $sName = $mailAddressResult->name; //Check if the domain of the found mail address is in the list of configured domains if ($oActions->DomainProvider()->Load($sDomain, true)) { //only execute if the found account isn't already in the list of additional accounts //and if the found account is different from the main account - if (!$aAccounts["$sUsername@$sDomain"] && $oAccount->Email() !== "$sUsername@$sDomain") + if (!isset($aAccounts["$sUsername@$sDomain"]) && $oAccount->Email() !== "$sUsername@$sDomain") { //Try to login the user with the same password as the primary account has //if this fails the user will see the new mail addresses but will be asked for the correct password $sPass = $oAccount->Password(); + $oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, "$sUsername@$sDomain", $sUsername, $sPass); - $aAccounts["$sUsername@$sDomain"] = $oNewAccount->asTokenArray($oAccount); + + $aAccounts["$sUsername@$sDomain"] = $oNewAccount->asTokenArray($oAccount); + } + + //Always inject/update the found mailbox names into the array (also if the mailbox already existed) + if (isset($aAccounts["$sUsername@$sDomain"])) + { + $aAccounts["$sUsername@$sDomain"]['name'] = $sName . " (LDAP)"; } } else { @@ -144,34 +162,12 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount return false; } - - /** - * @inheritDoc - * @throws \RainLoop\Exceptions\ClientException - */ - public function SetIdentities(Account $account, array $identities): void - { - throw new \RainLoop\Exceptions\ClientException("Ldap identities provider does not support storage"); - } - - /** - * @inheritDoc - */ - public function SupportsStore(): bool - { - return false; - } - - /** - * @inheritDoc - */ - public function Name(): string - { - return "Ldap"; - } - - /** @throws LdapException */ + * Checks if a connection to the LDAP was possible + * + * @throws LdapException + * + * */ private function EnsureConnected(): void { if ($this->ldapConnected) return; @@ -181,6 +177,9 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount $this->HandleLdapError("Connect"); } + /** + * Connect to the LDAP using the server address and protocol version defined inside the configuration of the plugin + */ private function Connect(): bool { // Set up connection @@ -202,7 +201,12 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount return true; } - /** @throws LdapException */ + /** + * Ensures the plugin has been authenticated at the LDAP + * + * @throws LdapException + * + * */ private function EnsureBound(): void { if ($this->ldapBound) return; @@ -213,6 +217,11 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount $this->HandleLdapError("Bind"); } + /** + * Authenticates the plugin at the LDAP using the username and password defined inside the configuration of the plugin + * + * @return bool true if authentication was successful + */ private function Bind(): bool { // Bind to LDAP here @@ -227,6 +236,8 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount } /** + * Handles and logs an eventual LDAP error + * * @param string $op * @throws LdapException */ @@ -242,6 +253,10 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount } /** + * Looks up the LDAP for additional mail accounts + * + * The search for additional mail accounts is done by a ldap search using the defined fields inside the configuration of the plugin (SnappyMail Admin Panel) + * * @param string $searchField * @param string $searchString * @param string $searchBase @@ -349,16 +364,16 @@ $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount /** - * @param array $entry - * @param string $attribute - * @param bool $single - * @param bool $required + * Gets LDAP attributes out of the input array + * + * @param array $entry Array containing the result of a ldap search + * @param string $attribute The name of the attribute to return + * @param bool $single If true the function checks if exact one value for this attribute is inside the input array. If false an array is returned. Default true. + * @param bool $required If true the attribute has to exist inside the input array. Default false. * @return string|string[] */ private function LdapGetAttribute(array $entry, string $attribute, bool $single = true, bool $required = false) { - //INFO if $single=false a array is returned. needet for identities, but not for additional mail accounts / usernames - if (!isset($entry[$attribute])) { if ($required) $this->logger->Write("Attribute $attribute not found on object {$entry['dn']} while required", \LOG_NOTICE, self::LOG_KEY); diff --git a/plugins/ldap-mail-accounts/index.php b/plugins/ldap-mail-accounts/index.php index 58b2965d3..a7f19bc9a 100644 --- a/plugins/ldap-mail-accounts/index.php +++ b/plugins/ldap-mail-accounts/index.php @@ -31,11 +31,17 @@ class LdapMailAccountsPlugin extends AbstractPlugin public function Init(): void { - $this->addHook("login.success", 'AddLdapMailAccounts'); + $this->addHook("login.success", 'AddAdditionalLdapMailAccounts'); } // Function gets called by RainLoop/Actions/User.php - public function AddLdapMailAccounts(Account $oAccount) + /** + * Add additional mail accounts to the webinterface of the user by looking up the ldap directory + * + * @param Account $oAccount + */ + + public function AddAdditionalLdapMailAccounts(Account $oAccount) { // Set up config $config = LdapConfig::MakeConfig($this->Config()); @@ -45,6 +51,9 @@ class LdapMailAccountsPlugin extends AbstractPlugin $oldapMailAccounts->AddLdapMailAccounts($oAccount); } + /** + * Defines the content of the plugin configuration page inside the Admin Panel of SnappyMail + */ protected function configMapping(): array { return [ From 71069a8cc949b5c3bd851c25ca7a2781e6f6be07 Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:51:54 +0100 Subject: [PATCH 8/9] renaming of files and classes resolve conflict with plugin ldap-identities --- plugins/ldap-mail-accounts/LdapException.php | 5 ---- .../ldap-mail-accounts/LdapMailAccounts.php | 28 +++++++++--------- ...pConfig.php => LdapMailAccountsConfig.php} | 4 +-- .../LdapMailAccountsException.php | 5 ++++ plugins/ldap-mail-accounts/index.php | 29 +++++++++---------- 5 files changed, 35 insertions(+), 36 deletions(-) delete mode 100644 plugins/ldap-mail-accounts/LdapException.php rename plugins/ldap-mail-accounts/{LdapConfig.php => LdapMailAccountsConfig.php} (94%) create mode 100644 plugins/ldap-mail-accounts/LdapMailAccountsException.php diff --git a/plugins/ldap-mail-accounts/LdapException.php b/plugins/ldap-mail-accounts/LdapException.php deleted file mode 100644 index df489e577..000000000 --- a/plugins/ldap-mail-accounts/LdapException.php +++ /dev/null @@ -1,5 +0,0 @@ -config = $config; $this->logger = $logger; @@ -60,7 +60,7 @@ class LdapMailAccounts { try { $this->EnsureBound(); - } catch (LdapException $e) { + } catch (LdapMailAccountsException $e) { return false; // exceptions are only thrown from the handleerror function that does logging already } @@ -87,7 +87,7 @@ class LdapMailAccounts $this->config->field_domain ); } - catch (LdapException $e) { + catch (LdapMailAccountsException $e) { return false; // exceptions are only thrown from the handleerror function that does logging already } if (count($mailAddressResults) < 1) { @@ -165,7 +165,7 @@ class LdapMailAccounts /** * Checks if a connection to the LDAP was possible * - * @throws LdapException + * @throws LdapMailAccountsException * * */ private function EnsureConnected(): void @@ -204,7 +204,7 @@ class LdapMailAccounts /** * Ensures the plugin has been authenticated at the LDAP * - * @throws LdapException + * @throws LdapMailAccountsException * * */ private function EnsureBound(): void @@ -239,7 +239,7 @@ class LdapMailAccounts * Handles and logs an eventual LDAP error * * @param string $op - * @throws LdapException + * @throws LdapMailAccountsException */ private function HandleLdapError(string $op = ""): void { @@ -249,7 +249,7 @@ class LdapMailAccounts $message = empty($op) ? "LDAP Error: {$errorMsg} ({$errorNo})" : "LDAP Error during {$op}: {$errorMsg} ({$errorNo})"; $this->logger->Write($message, \LOG_ERR, self::LOG_KEY); - throw new LdapException($message, $errorNo); + throw new LdapMailAccountsException($message, $errorNo); } /** @@ -264,8 +264,8 @@ class LdapMailAccounts * @param string $nameField * @param string $usernameField * @param string $domainField - * @return LdapResult[] - * @throws LdapException + * @return LdapMailAccountResult[] + * @throws LdapMailAccountsException */ private function FindLdapResults( string $searchField, @@ -296,12 +296,12 @@ class LdapMailAccounts return []; } - // Save the found ldap entries into a LdapResult object and return them + // Save the found ldap entries into a LdapMailAccountResult object and return them $results = []; for ($i = 0; $i < $entries["count"]; $i++) { $entry = $entries[$i]; - $result = new LdapResult(); + $result = new LdapMailAccountResult(); $result->dn = $entry["dn"]; $result->name = $this->LdapGetAttribute($entry, $nameField, true, true); @@ -394,7 +394,7 @@ class LdapMailAccounts } } -class LdapResult +class LdapMailAccountResult { /** @var string */ public $dn; diff --git a/plugins/ldap-mail-accounts/LdapConfig.php b/plugins/ldap-mail-accounts/LdapMailAccountsConfig.php similarity index 94% rename from plugins/ldap-mail-accounts/LdapConfig.php rename to plugins/ldap-mail-accounts/LdapMailAccountsConfig.php index 22361e416..f1a62786c 100644 --- a/plugins/ldap-mail-accounts/LdapConfig.php +++ b/plugins/ldap-mail-accounts/LdapMailAccountsConfig.php @@ -3,7 +3,7 @@ use RainLoop\Config\Plugin; -class LdapConfig +class LdapMailAccountsConfig { public const CONFIG_SERVER = "server"; public const CONFIG_PROTOCOL_VERSION = "server_version"; @@ -31,7 +31,7 @@ class LdapConfig public $search_string; public $field_domain; - public static function MakeConfig(Plugin $config): LdapConfig + public static function MakeConfig(Plugin $config): LdapMailAccountsConfig { $ldap = new self(); $ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER)); diff --git a/plugins/ldap-mail-accounts/LdapMailAccountsException.php b/plugins/ldap-mail-accounts/LdapMailAccountsException.php new file mode 100644 index 000000000..3135cc14b --- /dev/null +++ b/plugins/ldap-mail-accounts/LdapMailAccountsException.php @@ -0,0 +1,5 @@ +Config()); + $config = LdapMailAccountsConfig::MakeConfig($this->Config()); $oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger()); @@ -57,44 +56,44 @@ class LdapMailAccountsPlugin extends AbstractPlugin protected function configMapping(): array { return [ - Property::NewInstance(LdapConfig::CONFIG_SERVER) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_SERVER) ->SetLabel("LDAP Server URL") ->SetPlaceholder("ldap://server:port") ->SetType(PluginPropertyType::STRING), - Property::NewInstance(LdapConfig::CONFIG_PROTOCOL_VERSION) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_PROTOCOL_VERSION) ->SetLabel("LDAP Protocol Version") ->SetType(PluginPropertyType::SELECTION) ->SetDefaultValue([2, 3]), - Property::NewInstance(LdapConfig::CONFIG_BIND_USER) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_USER) ->SetLabel("LDAP Username") ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind.") ->SetType(PluginPropertyType::STRING), - Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_PASSWORD) ->SetLabel("LDAP Password") ->SetDescription("Leave empty for anonymous bind.") ->SetType(PluginPropertyType::PASSWORD), - Property::NewInstance(LdapConfig::CONFIG_OBJECTCLASS) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_OBJECTCLASS) ->SetLabel("Object class") ->SetType(PluginPropertyType::STRING) ->SetDescription("The object class to use when searching for additional mail accounts of the logged in SnappyMail user") ->SetDefaultValue("user"), - Property::NewInstance(LdapConfig::CONFIG_BASE) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_BASE) ->SetLabel("Base DN") ->SetType(PluginPropertyType::STRING) ->SetDescription("The base DN to search in for additional mail accounts of the logged in SnappyMail user"), - Property::NewInstance(LdapConfig::CONFIG_FIELD_SEARCH) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_SEARCH) ->SetLabel("Search field") ->SetType(PluginPropertyType::STRING) ->SetDescription("The name of the ldap attribute that has to contain the here defined 'LDAP search string'.") ->SetDefaultValue("member"), - Property::NewInstance(LdapConfig::CONFIG_SEARCH_STRING) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_SEARCH_STRING) ->SetLabel("LDAP search string") ->SetType(PluginPropertyType::STRING) ->SetDescription("The search string used to find ldap objects of mail accounts the user has access to. @@ -102,7 +101,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin \n#BASE_DN# - replaced with the value inside the field 'User base DN'.") ->SetDefaultValue("uid=#USERNAME#"), - Property::NewInstance(LdapConfig::CONFIG_FIELD_USERNAME) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_USERNAME) ->SetLabel("Username field of additional account") ->SetType(PluginPropertyType::STRING) ->SetDescription("The field containing the username of the found additional mail account. @@ -110,7 +109,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin \nIf this field contains an email address, only the local-part before the @ is used.") ->SetDefaultValue("uid"), - Property::NewInstance(LdapConfig::CONFIG_FIELD_MAIL_DOMAIN) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_DOMAIN) ->SetLabel("Domain name field of additional account") ->SetType(PluginPropertyType::STRING) ->SetDescription("The field containing the domain name of the found additional mail account. @@ -118,7 +117,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin \nIf this field contains an email address, only the domain-part after the @ is used.") ->SetDefaultValue("mail"), - Property::NewInstance(LdapConfig::CONFIG_FIELD_NAME) + Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_NAME) ->SetLabel("Additional account name field") ->SetType(PluginPropertyType::STRING) ->SetDescription("The field containing the default sender name of the found additional mail account.") From f86ed70238794f3b484ccd8e3522fe354f142824 Mon Sep 17 00:00:00 2001 From: cm-schl <63400209+cm-schl@users.noreply.github.com> Date: Wed, 30 Nov 2022 09:15:25 +0100 Subject: [PATCH 9/9] add a readme for users --- plugins/ldap-mail-accounts/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 plugins/ldap-mail-accounts/README.md diff --git a/plugins/ldap-mail-accounts/README.md b/plugins/ldap-mail-accounts/README.md new file mode 100644 index 000000000..28176c2e8 --- /dev/null +++ b/plugins/ldap-mail-accounts/README.md @@ -0,0 +1,17 @@ +## LDAP Mail Accounts Plugin for SnappyMail + +### Description +This plugin can be used to add additional mail accounts to SnappyMail when a user logs in successfully. The list of additional accounts is retrieved by a ldap query that can be configured inside the plugin settings.\ +On a successful login the username of the SnappyMail user is passed to the plugin and will be searched in the ldap. If additional mail accounts are found, the username and domain-part of those will be used to add the new mail account. The plugin tries to log in the user with the same password used to login to SnappyMail - if this fails SnappyMail asks the user to insert his credentials. + +### Configuration +- Install and activate the plugin using the SnappyMail Admin Panel -> menu Extensions. +- Click on the gear symbol beside the plugin to open the configration dialog. + - Insert the connection data to access your LDAP. Leave username and password empty for an anonymous login. + - The fields `Object class`, `Base DN`, `Search field` and `LDAP search string` are used to put together a ldap search filter which will return the additional mail accounts of a user:\ + `(&(objectclass=)(=))`.\ + This filter will be executed on the `Base DN` you have defined. `LDAP search string` can contain the placeholders `#USERNAME#` (will be replaced with the username the user logged in to Snappymail) and `#BASE_DN#` (will be replaced with the value you inserted into the field `Base DN` inside the plugin settings). This will allow you to create more complex search strings like `uid=#USERNAME#`. + - `Username field of additional account`, `Domain name field of additional account` and `Additional account name field` are used to define the ldap attributes to read when the ldap search was successful. For example insert `mail` into the `Username field of additional account` and the `Domain name field of additional account` to use the [local-part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the mail address as username and the [domain-part](https://en.wikipedia.org/wiki/Email_address#Domain) as domain for the additional account.\ + `Username field of additional account` and `Domain name field of additional account` before use get checked by the plugin if they contain a mail address and if true only the local-part or domain-part is returned. If no `@` is found the content of the found ldap attribute is returned without modification. This can be usefull if your user should login with something different than the mail address (a username that is diffrent from the local-part of the mail address). + +**Important:** keep in mind, that SnappyMail normally needs a mail address as username. In some special circumstances (login with an ldap username, not a mail address) this can mean that you have to configure the plugin to put together a 'fake' mail address like `@`.