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