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_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;
}

View file

@ -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;
}

View file

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