2022-11-16 01:29:01 +08:00
< ? php
use RainLoop\Enumerations\Capa ;
use MailSo\Log\Logger ;
2022-11-29 23:28:59 +08:00
use RainLoop\Actions ;
2022-11-30 18:54:55 +08:00
use RainLoop\Model\MainAccount ;
2022-11-16 01:29:01 +08:00
class LdapMailAccounts
{
/** @var resource */
private $ldap ;
/** @var bool */
private $ldapAvailable = true ;
/** @var bool */
private $ldapConnected = false ;
/** @var bool */
private $ldapBound = false ;
2022-11-29 23:51:54 +08:00
/** @var LdapMailAccountsConfig */
2022-11-16 01:29:01 +08:00
private $config ;
/** @var Logger */
private $logger ;
2022-11-28 18:49:28 +08:00
private const LOG_KEY = " LDAP MAIL ACCOUNTS PLUGIN " ;
2022-11-16 01:29:01 +08:00
/**
* LdapMailAccount constructor .
*
2022-11-29 23:51:54 +08:00
* @ param LdapMailAccountsConfig $config LdapMailAccountsConfig object containing the admin configuration for this plugin
2022-11-29 23:28:59 +08:00
* @ param Logger $logger Used to write to the logfile
2022-11-16 01:29:01 +08:00
*/
2022-11-29 23:51:54 +08:00
public function __construct ( LdapMailAccountsConfig $config , Logger $logger )
2022-11-16 01:29:01 +08:00
{
$this -> 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 ();
}
2023-03-03 19:27:56 +08:00
/**
* @ inheritDoc
*
* AOverwrite the MainAccount mail address by looking up the new one in the ldap directory
*
* The ldap search string has to be configured in the plugin configuration of the extension ( in the SnappyMail Admin Panel )
*
* @ param string & $sEmail
* @ param string & $sLogin
*/
public function overwriteEmail ( & $sEmail , & $sLogin )
{
try {
$this -> EnsureBound ();
} catch ( LdapMailAccountsException $e ) {
2023-03-07 22:31:09 +08:00
return false ; // exceptions are only thrown from the handle error function that does logging already
2023-03-03 19:27:56 +08:00
}
// Try to get account information. IncLogin() returns the username of the user
// and removes the domainname if this was configured inside the domain config.
$username = $sEmail ;
$oActions = \RainLoop\Api :: Actions ();
$oDomain = $oActions -> DomainProvider () -> Load ( \MailSo\Base\Utils :: GetDomainFromEmail ( $sEmail ), true );
if ( $oDomain -> IncShortLogin ()){
$username = @ ldap_escape ( $this -> RemoveEventualDomainPart ( $sEmail ), " " , 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 -> field_search ,
$searchString ,
$this -> config -> base ,
$this -> config -> objectclass ,
$this -> config -> field_name ,
$this -> config -> field_username ,
$this -> config -> field_domain ,
2023-03-07 22:05:10 +08:00
true ,
2023-03-03 19:27:56 +08:00
$this -> config -> field_mail_address_main_account ,
);
}
catch ( LdapMailAccountsException $e ) {
2023-03-07 22:31:09 +08:00
return false ; // exceptions are only thrown from the handle error function that does logging already
2023-03-03 19:27:56 +08:00
}
if ( count ( $mailAddressResults ) < 1 ) {
2023-03-07 22:05:10 +08:00
$this -> logger -> Write ( " Could not find user $username in LDAP! Overwriting of main mail address not possible. " , \LOG_NOTICE , self :: LOG_KEY );
2023-03-03 19:27:56 +08:00
return false ;
}
foreach ( $mailAddressResults as $mailAddressResult )
{
if ( $mailAddressResult -> username === $username ) {
//$sLogin is already set to be the same as $sEmail by function "resolveLoginCredentials" in /RainLoop/Actions/UserAuth.php
//that called this hook, so we just have to overwrite the mail address
$sEmail = $mailAddressResult -> mailMainAccount ;
}
}
}
2022-11-16 01:29:01 +08:00
/**
* @ inheritDoc
2022-11-30 18:54:55 +08:00
*
2022-11-25 19:37:00 +08:00
* Add additional mail accounts to the given primary account by looking up the ldap directory
2022-11-30 18:54:55 +08:00
*
2023-03-03 19:27:56 +08:00
* The ldap search string has to be configured in the plugin configuration of the extension ( in the SnappyMail Admin Panel )
2022-11-30 18:54:55 +08:00
*
* @ param MainAccount $oAccount
2023-03-03 19:27:56 +08:00
* @ return bool true if additional accounts have been added or no additional accounts where found in ldap . false if an error occured
2022-11-16 01:29:01 +08:00
*/
2022-11-30 18:54:55 +08:00
public function AddLdapMailAccounts ( MainAccount $oAccount ) : bool
2022-11-16 01:29:01 +08:00
{
try {
$this -> EnsureBound ();
2022-11-29 23:51:54 +08:00
} catch ( LdapMailAccountsException $e ) {
2023-03-07 22:31:09 +08:00
return false ; // exceptions are only thrown from the handle error function that does logging already
2022-11-16 01:29:01 +08:00
}
2023-03-03 19:27:56 +08:00
// Try to get account information. IncLogin() returns the username of the user
2022-11-25 17:42:14 +08:00
// and removes the domainname if this was configured inside the domain config.
2022-12-08 17:35:20 +08:00
$username = @ ldap_escape ( $oAccount -> IncLogin (), " " , LDAP_ESCAPE_FILTER );
2022-11-16 01:29:01 +08:00
2022-11-25 17:42:14 +08:00
$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 );
2022-11-30 18:54:55 +08:00
$this -> logger -> Write ( " ldap search string after replacement of placeholders: $searchString " , \LOG_NOTICE , self :: LOG_KEY );
2022-11-25 17:42:14 +08:00
2022-11-16 01:29:01 +08:00
try {
$mailAddressResults = $this -> FindLdapResults (
2022-11-25 17:42:14 +08:00
$this -> config -> field_search ,
$searchString ,
$this -> config -> base ,
$this -> config -> objectclass ,
$this -> config -> field_name ,
$this -> config -> field_username ,
2023-02-10 19:04:50 +08:00
$this -> config -> field_domain ,
2023-03-07 22:05:10 +08:00
false ,
2023-02-10 19:04:50 +08:00
$this -> config -> field_mail_address_additional_account
2022-11-16 01:29:01 +08:00
);
2022-11-30 18:54:55 +08:00
}
2022-11-29 23:51:54 +08:00
catch ( LdapMailAccountsException $e ) {
2023-03-07 22:31:09 +08:00
return false ; // exceptions are only thrown from the handle error function that does logging already
2022-11-16 01:29:01 +08:00
}
if ( count ( $mailAddressResults ) < 1 ) {
$this -> logger -> Write ( " Could not find user $username " , \LOG_NOTICE , self :: LOG_KEY );
2022-11-25 17:42:14 +08:00
return false ;
2022-11-16 01:29:01 +08:00
} 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 );
2022-11-25 17:42:14 +08:00
return true ;
2022-11-16 01:29:01 +08:00
}
2022-11-25 17:42:14 +08:00
//Basing on https://github.com/the-djmaze/snappymail/issues/616
2022-11-28 18:49:28 +08:00
2022-11-16 01:29:01 +08:00
$oActions = \RainLoop\Api :: Actions ();
2022-11-30 18:54:55 +08:00
2022-11-25 17:42:14 +08:00
//Check if SnappyMail is configured to allow additional accounts
2022-11-16 01:29:01 +08:00
if ( ! $oActions -> GetCapa ( Capa :: ADDITIONAL_ACCOUNTS )) {
return $oActions -> FalseResponse ( __FUNCTION__ );
}
2022-11-25 17:42:14 +08:00
$aAccounts = $oActions -> GetAccounts ( $oAccount );
2022-11-16 01:29:01 +08:00
2023-02-16 00:57:59 +08:00
//Search for accounts with suffix " (LDAP)" at the end of the name that were created by this plugin and initially remove them from the
2022-11-29 23:28:59 +08:00
//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 ]);
}
}
2022-11-16 01:29:01 +08:00
foreach ( $mailAddressResults as $mailAddressResult )
{
2022-11-25 17:42:14 +08:00
$sUsername = $mailAddressResult -> username ;
$sDomain = $mailAddressResult -> domain ;
2022-11-29 23:28:59 +08:00
$sName = $mailAddressResult -> name ;
2023-02-17 19:01:39 +08:00
$sEmail = $mailAddressResult -> mailAdditionalAccount ;
2022-11-25 17:42:14 +08:00
2022-11-28 18:49:28 +08:00
//Check if the domain of the found mail address is in the list of configured domains
if ( $oActions -> DomainProvider () -> Load ( $sDomain , true ))
2022-11-25 17:42:14 +08:00
{
2022-11-28 18:49:28 +08:00
//only execute if the found account isn't already in the list of additional accounts
2023-02-17 19:33:32 +08:00
//and if the found account is different from the main account.
//The check if the address is different from the one of the main account when using the Nextcloud integration needs
//to be done twice: directly on the mail address (when Nextcloud is configured to log the user in by mail address)
//or on "$sUsername@$sDomain" for the case Nextcloud logs the user in to SnappyMail by his username and a default domain.
if ( ! isset ( $aAccounts [ $sEmail ]) && $oAccount -> Email () !== $sEmail && $oAccount -> Email () !== " $sUsername @ $sDomain " )
2022-11-28 18:49:28 +08:00
{
//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
2022-12-08 17:35:20 +08:00
$sPass = $oAccount -> IncPassword ();
2023-02-17 19:01:39 +08:00
//After creating the accounts here $sUsername is used as username to login to the IMAP server (see Account.php)
2023-02-16 00:57:59 +08:00
$oNewAccount = RainLoop\Model\AdditionalAccount :: NewInstanceFromCredentials ( $oActions , $sEmail , $sUsername , $sPass );
2022-11-30 18:54:55 +08:00
2023-02-16 00:57:59 +08:00
$aAccounts [ $sEmail ] = $oNewAccount -> asTokenArray ( $oAccount );
2022-11-29 23:28:59 +08:00
}
//Always inject/update the found mailbox names into the array (also if the mailbox already existed)
2023-02-16 00:57:59 +08:00
if ( isset ( $aAccounts [ $sEmail ]))
2022-11-29 23:28:59 +08:00
{
2023-02-16 00:57:59 +08:00
$aAccounts [ $sEmail ][ 'name' ] = $sName . " (LDAP) " ;
2022-11-28 18:49:28 +08:00
}
}
else {
2023-02-16 00:57:59 +08:00
$this -> logger -> Write ( " Domain $sDomain is not part of configured domains in SnappyMail Admin Panel - mail address $sEmail will not be added. " , \LOG_NOTICE , self :: LOG_KEY );
2022-11-30 18:54:55 +08:00
}
2022-11-16 01:29:01 +08:00
}
2022-11-25 17:42:14 +08:00
if ( $aAccounts )
{
$oActions -> SetAccounts ( $oAccount , $aAccounts );
return true ;
2022-11-16 01:29:01 +08:00
}
2022-11-25 17:42:14 +08:00
return false ;
2022-11-16 01:29:01 +08:00
}
/**
2022-11-29 23:28:59 +08:00
* Checks if a connection to the LDAP was possible
2022-11-30 18:54:55 +08:00
*
* @ throws LdapMailAccountsException
*
2022-11-29 23:28:59 +08:00
* */
2022-11-16 01:29:01 +08:00
private function EnsureConnected () : void
{
if ( $this -> ldapConnected ) return ;
$res = $this -> Connect ();
if ( ! $res )
$this -> HandleLdapError ( " Connect " );
}
2022-11-29 23:28:59 +08:00
/**
* Connect to the LDAP using the server address and protocol version defined inside the configuration of the plugin
*/
2022-11-16 01:29:01 +08:00
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 ;
}
2022-11-30 18:54:55 +08:00
/**
2022-11-29 23:28:59 +08:00
* Ensures the plugin has been authenticated at the LDAP
2022-11-30 18:54:55 +08:00
*
* @ throws LdapMailAccountsException
*
2022-11-29 23:28:59 +08:00
* */
2022-11-16 01:29:01 +08:00
private function EnsureBound () : void
{
if ( $this -> ldapBound ) return ;
$this -> EnsureConnected ();
$res = $this -> Bind ();
if ( ! $res )
$this -> HandleLdapError ( " Bind " );
}
2022-11-29 23:28:59 +08:00
/**
* Authenticates the plugin at the LDAP using the username and password defined inside the configuration of the plugin
2022-11-30 18:54:55 +08:00
*
2022-11-29 23:28:59 +08:00
* @ return bool true if authentication was successful
*/
2022-11-16 01:29:01 +08:00
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 ;
}
/**
2022-11-29 23:28:59 +08:00
* Handles and logs an eventual LDAP error
2022-11-30 18:54:55 +08:00
*
2022-11-16 01:29:01 +08:00
* @ param string $op
2022-11-29 23:51:54 +08:00
* @ throws LdapMailAccountsException
2022-11-16 01:29:01 +08:00
*/
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 );
2022-11-29 23:51:54 +08:00
throw new LdapMailAccountsException ( $message , $errorNo );
2022-11-16 01:29:01 +08:00
}
/**
2022-11-29 23:28:59 +08:00
* Looks up the LDAP for additional mail accounts
2022-11-30 18:54:55 +08:00
*
2022-11-29 23:28:59 +08:00
* 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 )
2022-11-30 18:54:55 +08:00
*
2022-11-16 01:29:01 +08:00
* @ param string $searchField
2022-11-25 17:42:14 +08:00
* @ param string $searchString
2022-11-16 01:29:01 +08:00
* @ param string $searchBase
* @ param string $objectClass
* @ param string $nameField
2022-11-25 17:42:14 +08:00
* @ param string $usernameField
* @ param string $domainField
2023-03-07 22:05:10 +08:00
* @ param bool $overwriteMailMainAccount true if the mail address of the main account should be looked up for overwriting . false if additional mail accounts should be searched
* @ param string $mailAddressField The field containing the mail address ( of main account or additional mail account )
2022-11-29 23:51:54 +08:00
* @ return LdapMailAccountResult []
* @ throws LdapMailAccountsException
2022-11-16 01:29:01 +08:00
*/
2022-11-25 17:42:14 +08:00
private function FindLdapResults (
2022-11-30 18:54:55 +08:00
string $searchField ,
string $searchString ,
string $searchBase ,
string $objectClass ,
string $nameField ,
string $usernameField ,
2023-02-10 19:04:50 +08:00
string $domainField ,
bool $overwriteMailMainAccount ,
2023-03-07 22:05:10 +08:00
string $mailAddressField ) : array
2022-11-30 18:54:55 +08:00
{
$this -> EnsureBound ();
2022-11-16 01:29:01 +08:00
$nameField = strtolower ( $nameField );
2022-11-25 17:42:14 +08:00
$usernameField = strtolower ( $usernameField );
$domainField = strtolower ( $domainField );
2022-11-16 01:29:01 +08:00
2022-11-25 17:42:14 +08:00
$filter = " (&(objectclass= $objectClass )( $searchField = $searchString )) " ;
2023-03-07 22:05:10 +08:00
$this -> logger -> Write ( " Used ldap filter to search for mail account(s): $filter " , \LOG_NOTICE , self :: LOG_KEY );
2022-11-16 01:29:01 +08:00
2023-02-10 19:04:50 +08:00
//Set together the attributes to search inside the LDAP
2023-03-07 22:05:10 +08:00
$ldapAttributes = [ 'dn' , $usernameField , $nameField , $domainField , $mailAddressField ];
2023-02-10 19:04:50 +08:00
$ldapResult = @ ldap_search ( $this -> ldap , $searchBase , $filter , $ldapAttributes );
2022-11-16 01:29:01 +08:00
if ( ! $ldapResult ) {
$this -> HandleLdapError ( " Fetch $objectClass " );
return [];
}
$entries = @ ldap_get_entries ( $this -> ldap , $ldapResult );
if ( ! $entries ) {
$this -> HandleLdapError ( " Fetch $objectClass " );
return [];
}
2022-11-29 23:51:54 +08:00
// Save the found ldap entries into a LdapMailAccountResult object and return them
2022-11-16 01:29:01 +08:00
$results = [];
for ( $i = 0 ; $i < $entries [ " count " ]; $i ++ ) {
$entry = $entries [ $i ];
2022-11-29 23:51:54 +08:00
$result = new LdapMailAccountResult ();
2022-11-16 01:29:01 +08:00
$result -> dn = $entry [ " dn " ];
$result -> name = $this -> LdapGetAttribute ( $entry , $nameField , true , true );
2022-11-30 18:54:55 +08:00
2022-11-25 17:42:14 +08:00
$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 );
2022-11-16 01:29:01 +08:00
2023-03-07 22:05:10 +08:00
if ( $overwriteMailMainAccount ) {
$result -> mailMainAccount = $this -> LdapGetAttribute ( $entry , $mailAddressField , true , true );
}
else {
$result -> mailAdditionalAccount = $this -> LdapGetAttribute ( $entry , $mailAddressField , true , true );
}
2023-02-10 19:04:50 +08:00
2022-11-16 01:29:01 +08:00
$results [] = $result ;
}
return $results ;
}
2022-11-25 17:42:14 +08:00
/**
* Removes an eventually found domain - part of an email address
2022-11-30 18:54:55 +08:00
*
2022-11-25 17:42:14 +08:00
* 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 .
2022-11-30 18:54:55 +08:00
*
2022-11-25 17:42:14 +08:00
* @ 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 ;
2022-11-30 18:54:55 +08:00
}
2022-11-25 17:42:14 +08:00
/**
* Removes an eventually found local - part of an email address
2022-11-30 18:54:55 +08:00
*
2022-11-25 17:42:14 +08:00
* 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 .
2022-11-30 18:54:55 +08:00
*
2022-11-25 17:42:14 +08:00
* @ 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 ;
2022-11-30 18:54:55 +08:00
}
2022-11-25 17:42:14 +08:00
2022-11-16 01:29:01 +08:00
/**
2022-11-29 23:28:59 +08:00
* Gets LDAP attributes out of the input array
2022-11-30 18:54:55 +08:00
*
2022-11-29 23:28:59 +08:00
* @ 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 .
2022-11-16 01:29:01 +08:00
* @ return string | string []
*/
private function LdapGetAttribute ( array $entry , string $attribute , bool $single = true , bool $required = false )
{
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 );
}
}
2022-11-29 23:51:54 +08:00
class LdapMailAccountResult
2022-11-16 01:29:01 +08:00
{
/** @var string */
public $dn ;
/** @var string */
public $name ;
/** @var string */
public $username ;
2022-11-25 17:42:14 +08:00
/** @var string */
public $domain ;
2023-02-10 19:04:50 +08:00
/** @var string */
public $mailMainAccount ;
/** @var string */
public $mailAdditionalAccount ;
2022-11-16 01:29:01 +08:00
}