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