working prototype

This commit is contained in:
cm-schl 2022-11-25 10:42:14 +01:00
parent 460ea5a479
commit d8b9907e32
3 changed files with 174 additions and 182 deletions

View file

@ -11,43 +11,25 @@ class LdapConfig
public const CONFIG_BIND_USER = "bind_user"; public const CONFIG_BIND_USER = "bind_user";
public const CONFIG_BIND_PASSWORD = "bind_password"; public const CONFIG_BIND_PASSWORD = "bind_password";
public const CONFIG_USER_BASE = "user_base"; public const CONFIG_BASE = "base";
public const CONFIG_USER_OBJECTCLASS = "user_objectclass"; public const CONFIG_OBJECTCLASS = "objectclass";
public const CONFIG_USER_FIELD_NAME = "user_field_name"; public const CONFIG_FIELD_NAME = "field_name";
public const CONFIG_USER_FIELD_SEARCH = "user_field_search"; public const CONFIG_FIELD_SEARCH = "field_search";
public const CONFIG_USER_FIELD_MAIL = "user_field_mail"; public const CONFIG_FIELD_USERNAME = "field_username";
public const CONFIG_SEARCH_STRING = "search_string";
/* Not needed at the moment public const CONFIG_FIELD_MAIL_DOMAIN = "field_domain";
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 $server; public $server;
public $protocol; public $protocol;
public $bind_user; public $bind_user;
public $bind_password; public $bind_password;
public $user_base; public $base;
public $user_objectclass; public $objectclass;
public $user_field_name; public $field_name;
public $user_field_search; public $field_search;
public $user_field_mail; public $field_username;
public $search_string;
/* Not needed at the moment public $field_domain;
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 static function MakeConfig(Plugin $config): LdapConfig 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->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3));
$ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER)); $ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER));
$ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD));
$ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE)); $ldap->base = trim($config->Get("plugin", self::CONFIG_BASE));
$ldap->user_objectclass = trim($config->Get("plugin", self::CONFIG_USER_OBJECTCLASS)); $ldap->objectclass = trim($config->Get("plugin", self::CONFIG_OBJECTCLASS));
$ldap->user_field_name = trim($config->Get("plugin", self::CONFIG_USER_FIELD_NAME)); $ldap->field_name = trim($config->Get("plugin", self::CONFIG_FIELD_NAME));
$ldap->user_field_search = trim($config->Get("plugin", self::CONFIG_USER_FIELD_SEARCH)); $ldap->field_search = trim($config->Get("plugin", self::CONFIG_FIELD_SEARCH));
$ldap->user_field_mail = trim($config->Get("plugin", self::CONFIG_USER_FIELD_MAIL)); $ldap->field_username = trim($config->Get("plugin", self::CONFIG_FIELD_USERNAME));
$ldap->search_string = trim($config->Get("plugin", self::CONFIG_SEARCH_STRING));
/* Not needed at the moment $ldap->field_domain = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_DOMAIN));
$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; return $ldap;
} }

View file

@ -57,104 +57,87 @@ class LdapMailAccounts
try { try {
$this->EnsureBound(); $this->EnsureBound();
} catch (LdapException $e) { } 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 // Try to get account information. Login() returns the username of the user
// if this was configured inside the domain config. // and removes the domainname if this was configured inside the domain config.
$username = @ldap_escape($oAccount->Login(), "", LDAP_ESCAPE_FILTER); $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 { try {
$mailAddressResults = $this->FindLdapResults( $mailAddressResults = $this->FindLdapResults(
$this->config->user_field_search, $this->config->field_search,
$username, $searchString,
$this->config->user_base, $this->config->base,
$this->config->user_objectclass, $this->config->objectclass,
$this->config->user_field_name, $this->config->field_name,
$this->config->user_field_mail $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) { if (count($mailAddressResults) < 1) {
$this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY); $this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY);
return []; return false;
} else if (count($mailAddressResults) == 1) { } 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); $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(); $oActions = \RainLoop\Api::Actions();
$oMainAccount = $oActions->getMainAccountFromToken();
//Check if SnappyMail is configured to allow additional accounts
if (!$oActions->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { if (!$oActions->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) {
return $oActions->FalseResponse(__FUNCTION__); return $oActions->FalseResponse(__FUNCTION__);
} }
$aAccounts = $oActions->GetAccounts($oMainAccount); $aAccounts = $oActions->GetAccounts($oAccount);
$sPassword = $oActions->GetActionParam('Password', '');
$bNew = '1' === (string)$oActions->GetActionParam('New', '1');
foreach($mailAddressResults as $mailAddressResult) foreach($mailAddressResults as $mailAddressResult)
{ {
$sUsername = $mailAddressResult->$username; $sUsername = $mailAddressResult->username;
$sEmail = \MailSo\Base\Utils::IdnToAscii($sUsername, true); $sDomain = $mailAddressResult->domain;
if ($bNew && ($oMainAccount->Email() === $sUsername || isset($aAccounts[$sUsername]))) {
//Account already exists
return false;
}
if ($bNew || $sPassword) { $this->logger->Write("Domain: $sDomain Username: $sUsername Login: " . $oAccount->Login() . " E-Mail: " . $oAccount->Email(), \LOG_NOTICE, self::LOG_KEY);
$oNewAccount = $oActions->LoginProcess($sUsername, $sPassword, false, false);
$aAccounts[$sUsername] = $oNewAccount->asTokenArray($oMainAccount);
} else {
$aAccounts[$sUsername] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sUsername]);
}
if ($aAccounts[$sUsername]) { //only execute if the found account isn't already in the list of additional accounts
$aAccounts[$sUsername]['name'] = $mailAddressResult->$name; //and if the found account is different from the main account
$oActions->SetAccounts($oMainAccount, $aAccounts); //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 ($aAccounts)
if (!$this->config->group_get) {
return $identities; $oActions->SetAccounts($oAccount, $aAccounts);
return true;
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) { return false;
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 * @inheritDoc
* @throws \RainLoop\Exceptions\ClientException * @throws \RainLoop\Exceptions\ClientException
@ -252,26 +235,33 @@ class LdapMailAccounts
/** /**
* @param string $searchField * @param string $searchField
* @param string $searchValue * @param string $searchString
* @param string $searchBase * @param string $searchBase
* @param string $objectClass * @param string $objectClass
* @param string $nameField * @param string $nameField
* @param string $mailField * @param string $usernameField
* @param string $domainField
* @return LdapResult[] * @return LdapResult[]
* @throws LdapException * @throws LdapException
*/ */
private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField): array private function FindLdapResults(
{ string $searchField,
$this->EnsureBound(); string $searchString,
string $searchBase,
string $objectClass,
string $nameField,
string $usernameField,
string $domainField): array
{
$this->EnsureBound();
$nameField = strtolower($nameField); $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=$searchString))";
$filter = "(&(objectclass=$objectClass)($searchField=uid=$searchValue,$searchBase))"; $this->logger->Write("Used ldap filter to search for additional mail accounts: $filter", \LOG_NOTICE, self::LOG_KEY);
$this->logger->Write("Filter=$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) { if (!$ldapResult) {
$this->HandleLdapError("Fetch $objectClass"); $this->HandleLdapError("Fetch $objectClass");
return []; return [];
@ -283,6 +273,7 @@ class LdapMailAccounts
return []; return [];
} }
// Save the found ldap entries into a LdapResult object and return them
$results = []; $results = [];
for ($i = 0; $i < $entries["count"]; $i++) { for ($i = 0; $i < $entries["count"]; $i++) {
$entry = $entries[$i]; $entry = $entries[$i];
@ -290,7 +281,12 @@ class LdapMailAccounts
$result = new LdapResult(); $result = new LdapResult();
$result->dn = $entry["dn"]; $result->dn = $entry["dn"];
$result->name = $this->LdapGetAttribute($entry, $nameField, true, true); $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; $results[] = $result;
} }
@ -298,6 +294,52 @@ class LdapMailAccounts
return $results; 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 array $entry
* @param string $attribute * @param string $attribute
@ -341,4 +383,7 @@ class LdapResult
/** @var string */ /** @var string */
public $username; public $username;
/** @var string */
public $domain;
} }

View file

@ -34,6 +34,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin
$this->addHook("login.success", 'AddLdapMailAccounts'); $this->addHook("login.success", 'AddLdapMailAccounts');
} }
// Function gets called by RainLoop/Actions/User.php
public function AddLdapMailAccounts(Account $oAccount) public function AddLdapMailAccounts(Account $oAccount)
{ {
// Set up config // Set up config
@ -59,84 +60,56 @@ class LdapMailAccountsPlugin extends AbstractPlugin
Property::NewInstance(LdapConfig::CONFIG_BIND_USER) Property::NewInstance(LdapConfig::CONFIG_BIND_USER)
->SetLabel("Bind User DN") ->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), ->SetType(PluginPropertyType::STRING),
Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD)
->SetLabel("Bind User Password") ->SetLabel("Bind User Password")
->SetDescription("Leave empty for anonymous bind") ->SetDescription("Leave empty for anonymous bind.")
->SetType(PluginPropertyType::PASSWORD), ->SetType(PluginPropertyType::PASSWORD),
Property::NewInstance(LdapConfig::CONFIG_USER_OBJECTCLASS) Property::NewInstance(LdapConfig::CONFIG_OBJECTCLASS)
->SetLabel("User object class") ->SetLabel("Object class")
->SetType(PluginPropertyType::STRING) ->SetType(PluginPropertyType::STRING)
->SetDefaultValue("user"), ->SetDefaultValue("user"),
Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_SEARCH) Property::NewInstance(LdapConfig::CONFIG_BASE)
->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") ->SetLabel("User base DN")
->SetType(PluginPropertyType::STRING) ->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_FIELD_SEARCH)
->SetLabel("Search field")
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) ->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) Property::NewInstance(LdapConfig::CONFIG_SEARCH_STRING)
->SetLabel("Group mail field") ->SetLabel("LDAP search string")
->SetType(PluginPropertyType::STRING) ->SetType(PluginPropertyType::STRING)
->SetDescription("The field in the group object listing all identities (email addresses) of the group") ->SetDescription("The search string used to find ldap objects of mail accounts the user has access to.
->SetDefaultValue("mail"), \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) Property::NewInstance(LdapConfig::CONFIG_FIELD_USERNAME)
->SetLabel("Group name field") ->SetLabel("Username field of additional account")
->SetType(PluginPropertyType::STRING) ->SetType(PluginPropertyType::STRING)
->SetDescription("The field in the group object with the name") ->SetDescription("The field containing the username of the found additional mail account.
->SetDefaultValue("cn"), \nThis username gets used by SnappyMail to login to the additional mail account.")
->SetDefaultValue("uid"),
Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MEMBER) Property::NewInstance(LdapConfig::CONFIG_FIELD_MAIL_DOMAIN)
->SetLabel("Group member field") ->SetLabel("Domain name field of additional account")
->SetType(PluginPropertyType::STRING) ->SetType(PluginPropertyType::STRING)
->SetDescription("The field in the group object with all member DNs") ->SetDescription("The field containing the domain name of the found additional mail account.
->SetDefaultValue("member"), \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) Property::NewInstance(LdapConfig::CONFIG_FIELD_NAME)
->SetLabel("Group mail sender format") ->SetLabel("Additional account name field")
->SetType(PluginPropertyType::STRING) ->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") ->SetDescription("The field containing the default sender name of the found additional mail account.")
->SetDefaultValue("#USER# || #GROUP#"), ->SetDefaultValue("displayName")
Property::NewInstance(LdapConfig::CONFIG_GROUP_BASE)
->SetLabel("Group base DN")
->SetType(PluginPropertyType::STRING)
->SetDescription("The base DN to search in for groups")
*/
]; ];
} }
} }