From 6a0b0907ece42ca7b03d6ec2acd658faeb0e3e36 Mon Sep 17 00:00:00 2001 From: BohdanYatsenkoDarkGroup Date: Fri, 2 Jan 2026 00:46:08 +0100 Subject: [PATCH 1/3] Delete config.inc.php.dist --- config.inc.php.dist | 261 -------------------------------------------- 1 file changed, 261 deletions(-) delete mode 100644 config.inc.php.dist diff --git a/config.inc.php.dist b/config.inc.php.dist deleted file mode 100644 index 2cf06fc..0000000 --- a/config.inc.php.dist +++ /dev/null @@ -1,261 +0,0 @@ -rc->user->data['username'] will be used by this plugin). -// 6. Define which directory object attributes to use for the Roundcube identity -// values and signature placeholders (fieldmap, the array keys are the names -// of the Roundcube values, the array values are the corresponding directory -// attribute names to grab the data from). -// -// This would result in the following plugin config value: -// -// $config['identity_from_directory_ldap'] = [ -// 'hosts' => [ 'ldaps://dc01.contoso.com:636' ], -// 'base_dn' => 'DC=contoso,DC=com', -// 'bind_dn' => 'CN=roundcube-svc,OU=service-accounts,DC=contoso,DC=com', -// 'bind_pass' => 'password-of-service-user-defined-in-bind_dn', -// 'scope' => 'sub', // search mode: sub|base|list -// 'filter' => '(&(sAMAccountType=805306368)(!(servicePrincipalName=*))(!(isCriticalSystemObject=TRUE))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))', -// 'search_fields' => ['mail', 'sAMAccountName'], -// 'fieldmap' => [ -// 'name' => 'displayName', -// 'email' => 'mail', -// 'surname' => 'sn', -// 'firstname' => 'givenName', -// 'jobtitle' => 'title', -// 'department' => 'department', -// 'phone' => 'telephoneNumber', -// 'fax' => 'facsimileTelephoneNumber', -// 'website' => 'wWWHomePage', -// 'company' => 'organization', -// 'username' => 'sAMAccountName', -// ], -// ]; -$config['identity_from_directory_ldap'] = [ - 'hosts' => ['ldaps://dc01.contoso.com:636'], - 'base_dn' => 'DC=contoso,DC=com', - 'bind_dn' => 'CN=roundcube-svc,OU=service-accounts,DC=contoso,DC=com', - 'bind_pass' => 'password-of-service-user-defined-in-bind_dn', - 'scope' => 'sub', // available search modes: sub|base|list - 'filter' => '(&(sAMAccountType=805306368)(!(servicePrincipalName=*))(!(isCriticalSystemObject=TRUE))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))', - 'search_fields' => ['mail', 'sAMAccountName'], - // fieldmap: - // - key: value name in Roundcube - // - values: corresponding attribute name in your LDAP - // - // You can use the mapped fields keys (e.g. 'foo") as placeholders in - // $config['identity_from_directory_signature_template_plaintext'] and - // $config['identity_from_directory_signature_template_html']). See the - // comment there for details. - 'fieldmap' => [ - // mandatory RC identity fields - 'name' => 'displayName', - 'email' => 'mail', - 'organization' => 'company', - // additional data - 'surname' => 'sn', - 'firstname' => 'givenName', - 'jobtitle' => 'title', - 'department' => 'department', - 'phone' => 'telephoneNumber', - 'fax' => 'facsimileTelephoneNumber', - 'website' => 'wWWHomePage', - 'username' => 'sAMAccountName', - ], -]; - - -// optional fallback values that will be used if a LDAP attribute defined -// in $config['identity_from_directory_ldap']['fieldmap'] provides no or empty -// data. Just remove or add keys you do (not) need to have a fallback value for. -$config['identity_from_directory_fallback_values'] = [ - 'organization' => 'ACME Inc.', - 'phone' => '+49 123 123 123', - //'fax' => '+49 123 123 999', - 'website' => 'https://example.com/', -]; - - -// Regular expression (used with preg_match()) to exclude email alias addresses -// from handling. -// -// An empty string ('') disables the feature (=all of a user's email alias -// addresses defined by the directory data will be handled). -// -// There are environments with unwanted but still needed email aliases. A good -// example is a company re-branding including a domain change, where the -// organization wants that users are not being able to use email addresses with -// the old domain as identity in Roundcube but still needs to be able to receive -// emails addressed to the old domain. -// -// Examples: -// - '/^.+@example\.com$/im' -// Excludes all email addresses ending with "@example.com" (not case -// sensitive). -// - '/^.+@example\.(com|net|org)$/im' -// Excludes all email addresses ending with "@example.com", "@example.net" or -// "@example.org" (case-insensitive) -$config['identity_from_directory_exclude_alias_regex'] = ''; - - -// Switch to control if unmanaged identities should be deleted. -// -// true: Delete all identities without a matching email address in the user's -// directory dataset (= identities not maintained by this plugin). Strongly -// recommended for automatic housekeeping in most environments once everything -// is working as expected. -// -// false: Do not delete unmanaged identities (= identities with no fitting -// email address in the directory record of the user). These identities will -// remain untouched until users delete them themselves or the identity gets -// updated because the email address was added to a user's directory data. -// -// It makes sense to start testing with false to prevent unwanted deletions on -// running systems until you are sure that all identities should be defined by -// the user's directory data. -// -// You can define exceptions from this automatic cleanup by using the -// $config['identity_from_directory_exclude_delete_unmanaged_regex'] option. -// This might be helpful for edge cases or when other plugins are in use -// that create or influence a user's identities. -$config['identity_from_directory_delete_unmanaged'] = false; - - -// Regular expression (used with preg_match()) to exclude identities with -// matching email addresses from automatic cleanup when -// $config['identity_from_directory_delete_unmanaged'] is true. -// -// An empty string ('') disables the exclusion feature (= all of a user's -// unmanaged identities are deleted). This setting has no effect if -// $config['identity_from_directory_delete_unmanaged'] is set to false (as this -// disables the entire automatic deletion cleanup mechanism). -// -// Examples: -// - '/^.+@example\.com$/im' -// Excludes all identities using email addresses ending with "@example.com" -// (case-insensitive). -// - '/^.+@example\.(com|net|org)$/im' -// Excludes all identities using email addresses ending with "@example.com", -// "@example.net" or "@example.org" (case-insensitive) -$config['identity_from_directory_exclude_delete_unmanaged_regex'] = ''; - - -// Switch for signature handling -// -// true: Overwrite existing signatures on each login (not only name, organization, -// email and other attributes). -// -// false: Do not touch or overwrite the signature of an identity (so a user can -// still maintain the signature value and format in a self-reliant way). -$config['identity_from_directory_update_signatures'] = true; - - -// Signature templates -// -// You can use each key from $config['identity_from_directory_ldap']['fieldmap'] -// as %placeholder(_html|url)%. They will be replaced with the values from the -// either the directory or $config['identity_from_directory_fallback_values'] -// (if there is no or empty data for this key in LDAP). Each value is available -// in three ways (as provided by LDAP, with encoded HTML entities and URL encoded -// for using it in URLs). -// -// Example for 'foo': -// - %foo%: raw value of 'foo' -// - %foo_html%: HTML entities encoded value of 'foo' -// - %foo_url%: URL encoded value of 'foo'. Additional optimizations are -// applied it the key is named 'email' (usage of Punycode for email domains), -// 'phone' or 'fax' (stripping of chars not compatible with tel:// URLs) -// -// $config['identity_from_directory_signature_template_plaintext'] will be used -// if $config['identity_from_directory_use_html_signature'] is false. -// -// On images in HTML signatures: -// - The easiest way is the usage of base64 data URLs with e.g. -// data:image/png;base64,[...]: -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs -// Most mail user agents (MUAs) are supporting this but check if the all over -// compatibility is good enough for you: -// https://www.caniemail.com/features/image-base64/ -// - The second option are CID (or Content-ID) images. They work by attaching -// the image to the email you send and then using standard HTML image tags -// that reference that image to embed it in the email when the user opens it. -// Most MUAs are supporting this and Roundcube also provides and uses this -// functionality for HTML signatures. But tinkering out how to use it for -// this plugin and documenting is still an open issue (contributions welcome): -// https://github.com/foundata/roundcube-plugin-identity-from-directory/issues/3 -$config['identity_from_directory_signature_template_html'] = ' -

- Kind regards
- %name_html%
- %organization_html% -

-

- mailto: %email_html%
- phone: %phone_html%
- web: %website_html% -

-'; - -$config['identity_from_directory_signature_template_plaintext'] = ' -Kind regards -%name% -%organization% - -mailto: %email% -phone: %phone% -web: %website% -'; - - -// Switch: Also search the 'proxyAddresses' field for email alias addresses? -// -// 'proxyAddresses' is a Active Directory user attribute which may contain -// a CSV string like 'smtp:foo@example.com,smtp:bar@example.net' or an -// array with such values. They represent alias email addresses (if any). -// The directory server needs to provide this field (which is the default -// for MS Active Directory but not for most other LDAP servers out there; -// Therefore this option defaults to false). -// -// true: The plugin will also maintain identities for email aliases in -// 'proxyAddresses' (if any). -// -// false: Do not care about email aliases in 'proxyAddresses'. -$config['identity_from_directory_handle_proxyaddresses'] = false; - - -// Switch for signature format -// -// true: use HTML instead of plain text signatures. -$config['identity_from_directory_use_html_signature'] = true; - - -// Switch for signature sanitation -// -// true: Use rcmail_action_settings_index::wash_html() on HTML signatures. -// You can disable this if you got problems with stripped HTML attributes -// and you are sure that you can trust the LDAP data in any case. -$config['identity_from_directory_wash_html_signature'] = true; - - -// Switch for logging additional debug data into the Roundcube log -// "identity_from_directory". -// -// true: Write LDAP search info, records and other useful debugging info into -// the log file. You might want to also set $config['ldap_debug'] = true for -// logging Roundcube's LDAP conversations, including the ones triggered by -// this plugin. -$config['identity_from_directory_debug'] = false; From d9d5a04fb93cc4d5f8c7df5dc3783505ba26728d Mon Sep 17 00:00:00 2001 From: BohdanYatsenkoDarkGroup Date: Fri, 2 Jan 2026 00:46:23 +0100 Subject: [PATCH 2/3] Delete identity_from_directory.php --- identity_from_directory.php | 403 ------------------------------------ 1 file changed, 403 deletions(-) delete mode 100644 identity_from_directory.php diff --git a/identity_from_directory.php b/identity_from_directory.php deleted file mode 100644 index 3f42579..0000000 --- a/identity_from_directory.php +++ /dev/null @@ -1,403 +0,0 @@ -rc = rcmail::get_instance(); - - // Triggered after a user successfully logged in - // https://github.com/roundcube/roundcubemail/wiki/Plugin-Hooks#login_after - // This plugin is using it to update / edit existing identities - $this->add_hook('login_after', [$this, 'login_after']); - } - - - /** - * 'login_after' hook handler, used to create or update the user identities - */ - public function login_after($args) - { - $this->load_config('config.inc.php.dist'); // load the plugin's distribution config file as default - $this->load_config(); // merge with local configuration file (which can overwrite any settings) - if ($this->ldap) { - return $args; - } - $debug_plugin = (bool) $this->rc->config->get('identity_from_directory_debug'); - - // get user's data from directory and prepare it for further processing - $user_data = [ - 'name' => '', - 'email' => '', // default / main email address; IDN domains (no Punycode/ACE) - 'email_list' => [], // list of all email addresses (main, default, aliases); IDN domains (no Punycode/ACE) - 'managed_identity_ids' => [] // list of IDs of identities managed by this plugin - ]; - if ($this->init_ldap([ - 'mail_host' => $this->rc->user->data['mail_host'], - ])) { - // '*' does NOT search all fields but triggers the usage of 'search_fields' instead, see - // https://github.com/roundcube/roundcubemail/blob/master/program/lib/Roundcube/rcube_ldap.php#L900C49-L900C62 - // - // This 'search_fields' array gets set to the plugin's $config['identity_from_directory_ldap']['search_fields'] - // as connection property by $this->init_ldap(). So searching in '*' limits the fields to the plugin's config. - $results = $this->ldap->search('*', $this->rc->user->data['username'], true); - - if (count($results->records) === 1) { - - $ad_handle_proxyaddresses = (bool) $this->rc->config->get('identity_from_directory_handle_proxyaddresses'); - $exclude_alias_regex = (string) $this->rc->config->get('identity_from_directory_exclude_alias_regex'); - - $ldap_entry = $results->records[0]; - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'Found a record for ' . $this->rc->user->data['username'] . ': ' - . print_r($ldap_entry, true)); - } - - $user_data['name'] = is_array($ldap_entry['name']) ? $ldap_entry['name'][0] : $ldap_entry['name']; - $user_data['email'] = rcube_utils::idn_to_utf8(trim(is_array($ldap_entry['email']) ? $ldap_entry['email'][0] : $ldap_entry['email'])); - if (!empty($user_data['email'])) { - $user_data['email_list'][] = $user_data['email']; - } - - foreach (array_keys($ldap_entry) as $key) { - // add email addresses (main, aliases) to the list for the user - if (preg_match('/^email($|:)/i', $key)) { - foreach ((array) $ldap_entry[$key] as $alias) { - $alias = rcube_utils::idn_to_utf8(trim($alias)); - if (empty($alias) || self::email_in_array($alias, $user_data['email_list'])) { - continue; - } - if (strpos($alias, '@') === false || (!empty($exclude_alias_regex) && (preg_match($exclude_alias_regex, $alias)))) { - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'Excluded ' . $alias . ' from handling as it is an invalid email address or matching "' - . $exclude_alias_regex . '" (identity_from_directory_exclude_alias_regex).'); - } - continue; - } - $user_data['email_list'][] = $alias; - } - - // handle Active Directory attribute "proxyAddresses" - } elseif ($ad_handle_proxyaddresses && preg_match('/^proxyaddresses($|:)/i', $key)) { - // originally a CSV string like 'smtp:foo@exmaple.com,bar@example.net'. - // The used library returns as string if there is only one address and - // an array if there are multiple. - $proxyaddresses = $ldap_entry[$key]; - if (!is_array($proxyaddresses)) { - $proxyaddresses = [ $proxyaddresses ]; - } - foreach ((array) $proxyaddresses as $alias) { - $alias = rcube_utils::idn_to_utf8(trim(preg_replace('/^smtp:(.+)/i', '\1', trim($alias), 1))); - if (empty($alias) || self::email_in_array($alias, $user_data['email_list'])) { - continue; - } - if (strpos($alias, '@') === false || (!empty($exclude_alias_regex) && (preg_match($exclude_alias_regex, $alias)))) { - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'Excluded '. $alias . ' from handling as it is an invalid email address or matching "' - . $exclude_alias_regex . '" (identity_from_directory_exclude_alias_regex).'); - } - continue; - } - $user_data['email_list'][] = $alias; - } - - // add LDAP data but exclude _ID, _raw_attrib etc. and do not overwrite already existing keys - } elseif (strpos($key, '_') !== 0 && !array_key_exists($key, $user_data)) { - $user_data[$key] = $ldap_entry[$key]; - } - } - $user_data['email_list'] = array_unique($user_data['email_list']); - - } elseif ($debug_plugin && count($results->records) > 1) { - rcube::write_log('identity_from_directory', - 'Searching for '. $this->rc->user->data['username'] - . ' returned more than one result; all were ignored as unambiguous assignment is not possible.'); - } - } - if (empty($user_data['email_list'])) { - return $args; - } - - // get config and other data needed for further processing - $ldap_config = (array) $this->rc->config->get('identity_from_directory_ldap'); - $update_signatures = (bool) $this->rc->config->get('identity_from_directory_update_signatures'); - $use_html_signature = (bool) $this->rc->config->get('identity_from_directory_use_html_signature'); - $wash_html_signature = (bool) $this->rc->config->get('identity_from_directory_wash_html_signature'); - if ($use_html_signature) { - $signature_template = (string) $this->rc->config->get('identity_from_directory_signature_template_html'); - } else { - $signature_template = (string) $this->rc->config->get('identity_from_directory_signature_template_plaintext'); - } - $signature_fallback_values = (array) $this->rc->config->get('identity_from_directory_fallback_values'); - $identities_existing = $this->rc->user->list_emails(); // list of all user emails (from identities), array with identity_id, name and email address - - // maintain an identity for each of the user's determined email addresses - foreach ((array) $user_data['email_list'] as $email) { - $hook_to_use = 'identity_create'; - $identity_id = 0; // often called 'iid' in other parts of RC sources - $is_standard = 0; // 1: use the identity as default (there can only be one) - - foreach ($identities_existing as $identity_existing) { - // case-insensitive search to update an existing identity, even if - // there are differences in capitalization. - if (self::email_in_array($identity_existing['email'], [ $email ])) { - $hook_to_use = 'identity_update'; - $identity_id = $identity_existing['identity_id']; - break; - } - } - - if ($user_data['email'] === $email) { - $is_standard = 1; - } - - // see https://github.com/roundcube/roundcubemail/blob/master/program/actions/settings/identity_save.php for available keys - $identity_record = [ - 'user_id' => $this->rc->user->ID, - 'standard' => $is_standard, - 'name' => (!empty($user_data['name']) ? $user_data['name'] : $this->rc->user->data['username']), - 'email' => $email, - 'organization' => (array_key_exists('organization', $user_data) ? $user_data['organization'] : ''), - ]; - - if ($update_signatures) { - // copy signature template - $signature = $signature_template; - - // add signature to identity record, replace placeholders in the signature template with - // the values from the directory or $config['identity_from_directory_fallback_values']: - // - %foo%: raw value of field 'foo' - // - %foo_html%: HTML entities encoded value of field 'foo' - // - %foo_url%: URL encoded value of field 'foo'. Additional optimizations are - // applied for the fields 'email' (usage of Punycode for email domains), - // 'phone' and 'fax' (stripping of chars not compatible with tel:// URLs) - foreach (array_keys($ldap_config['fieldmap']) as $placeholder) { - $replace_raw = ''; - if ($placeholder === 'email') { - // Use the correct email address (alias) of the corresponding identity for - // the %email%, %email_html% and %email_url% placeholders instead of the - // single mapped value returned by the directory (which should be stored in - // $user_data['email']). Otherwise, the same single email address value would - // be used for all of the user's identities (even the one of alias addresses). - $replace_raw = (string) $email; - } elseif (array_key_exists($placeholder, $user_data) && ((string) $user_data[$placeholder] !== '')) { - $replace_raw = (string) $user_data[$placeholder]; - } elseif (array_key_exists($placeholder, $signature_fallback_values) && ((string) $signature_fallback_values[$placeholder] !== '')) { - $replace_raw = (string) $signature_fallback_values[$placeholder]; - } elseif (array_key_exists($placeholder, $identity_record) && ((string) $identity_record[$placeholder] !== '')) { - $replace_raw = (string) $identity_record[$placeholder]; - } else { - continue; - } - - $replace_html = ''; - $replace_html = htmlspecialchars($replace_raw, \ENT_NOQUOTES, RCUBE_CHARSET); - - $replace_url = ''; - if ($placeholder === 'phone' || $placeholder === 'fax') { - // strip some chars for "tel://" URL usage - $replace_url = urlencode(preg_replace('/[^+0-9]+/', '', $replace_raw)); - } elseif ($placeholder === 'email') { - // use Punycode/ACE for "mailto://" URL usage - $replace_url = urlencode(rcube_utils::idn_to_ascii($replace_raw)); - } else { - $replace_url = urlencode($replace_raw); - } - - $signature = str_replace([ '%'. $placeholder . '%', - '%'. $placeholder . '_html%', - '%'. $placeholder . '_url%' ], - [ $replace_raw, - $replace_html, - $replace_url ], $signature); - } - - $identity_record['html_signature'] = ($use_html_signature) ? 1 : 0; - $identity_record['signature'] = ($use_html_signature && $wash_html_signature) ? rcmail_action_settings_index::wash_html($signature) : $signature; // XSS protection - } - - $plugin = $this->rc->plugins->exec_hook($hook_to_use, [ - 'id' => $identity_id, - 'record' => $identity_record, - ]); - - if (!$plugin['abort'] && !empty($plugin['record']['email'])) { - if ($identity_id === 0) { - $identity_id = $this->rc->user->insert_identity($plugin['record']); - } else { - $this->rc->user->update_identity($identity_id, $plugin['record']); - } - } - - if (empty($identity_id)) { - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'The identity for user \'' . $this->rc->user->data['username'] . '\' ' - . 'could not be saved' . (!empty($plugin['message']) ? ' ( ' . $plugin['message'] . ' ): ' : ': ') - . print_r($identity_record, true)); - } - continue; - } - - // Store the ID of the identity as managed. Any optionally needed cleanup - // action gets a lot easier if there is a list of identities known to be - // managed for the current user by this plugin. - $user_data['managed_identity_ids'][] = $identity_id; - } - - // delete identities which are not managed by this plugin - $delete_unmanaged = (bool) $this->rc->config->get('identity_from_directory_delete_unmanaged'); - $exclude_delete_unmanaged_regex = (string) $this->rc->config->get('identity_from_directory_exclude_delete_unmanaged_regex'); - if ($delete_unmanaged) { - $identity_existing_count = count($identities_existing); - foreach ($identities_existing as $identity_existing) { - - if ($identity_existing_count > 1 && !(in_array($identity_existing['identity_id'], $user_data['managed_identity_ids']))) { - - if (!empty($exclude_delete_unmanaged_regex) && preg_match($exclude_delete_unmanaged_regex, $identity_existing['email'])) { - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'Excluded identity ' . $identity_existing['identity_id'] . ' of user ' - . $this->rc->user->data['username'] . ' from automatic deletion even ' - . 'though it is not managed by this plugin; it\'s email address ' - . $identity_existing['email'] . ' is matching "' . $exclude_delete_unmanaged_regex - . '" (identity_from_directory_exclude_delete_unmanaged_regex).'); - } - continue; - } - - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'Deleting identity '. $identity_existing['identity_id'] .' with email address ' - . $identity_existing['email'] . ' because it is not managed by this plugin.'); - } - - if (!($this->rc->user->delete_identity($identity_existing['identity_id'])) && $debug_plugin) { - rcube::write_log('identity_from_directory', - 'Could note delete identity '. $identity_existing['identity_id'] - . ' with email address '.$identity_existing['email']); - } - $identity_existing_count--; - - } - - } - } - - return $args; - } - - - /** - * Search for an email address in an array of email addresses. The search - * will ignores differences in capitalization or Punycode/ACE. - * - * RFC 5321 (Simple Mail Transfer Protocol) section 2.3.11 leaves it up - * to the host if the "local-part" in "local-part@domain" is case- - * insensitively. De-facto, it gets handled case-insensitive by most - * systems out there and users are expecting that Foo@example.com == - * foo@example.com quite often. This function also acts like this. - * - * @param string $needle Value to seek. - * @param array $haystack Array to seek in. - * @return bool - */ - public static function email_in_array($needle, $haystack) - { - $haystack_new = []; - foreach($haystack as $key => $value) { - $haystack_new[$key] = mb_strtolower(rcube_utils::idn_to_utf8(trim($value)), 'UTF-8'); - } - return in_array(mb_strtolower(rcube_utils::idn_to_utf8(trim($needle)), 'UTF-8'), $haystack_new); - } - - - /** - * Initialize LDAP backend connection - */ - private function init_ldap($args) - { - // check if connection is already initialized / nothing to do - if ($this->ldap) { - return $this->ldap->ready; - } - - // Get config and set some fallback / default values - // See this plugin's config.php.dist for a detailled description of the settings - $this->load_config('config.inc.php.dist'); // load the plugin's distribution config file as default - $this->load_config(); // merge with local configuration file (which can overwrite any settings) - - $debug_plugin = (bool) $this->rc->config->get('identity_from_directory_debug'); - $debug_ldap = (bool) $this->rc->config->get('ldap_debug'); - $mail_domain = (string) $this->rc->config->mail_domain($args['mail_host']); - - $ldap_config = (array) $this->rc->config->get('identity_from_directory_ldap'); - if (!array_key_exists('searchonly', $ldap_config)) { - $ldap_config['searchonly'] = true; - } - if (!array_key_exists('search_fields', $ldap_config) || !is_array($ldap_config['search_fields'])) { - $ldap_config['search_fields'] = ['mail', 'sAMAccountName', 'username']; - } - if (empty($ldap_config) || - empty($ldap_config['search_fields']) || - empty($ldap_config['fieldmap'])) { - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'The plugin config seems to be invalid, please check $config[\'identity_from_directory_ldap\'].'); - } - return false; - } - if (!array_key_exists('name', $ldap_config['fieldmap']) || - !array_key_exists('email', $ldap_config['fieldmap']) || - !array_key_exists('organization', $ldap_config['fieldmap'])) { - if ($debug_plugin) { - rcube::write_log('identity_from_directory', - 'The plugin config seems to be invalid, please check $config[\'identity_from_directory_ldap\'][\'fieldmap\'].'); - } - return false; - } - - // add mapping for the "proxyAddresses" attribute (which stores email aliases when using Active Directory) - $ad_handle_proxyaddresses = $this->rc->config->get('identity_from_directory_handle_proxyaddresses'); - if ($ad_handle_proxyaddresses) { - $ldap_config['fieldmap']['proxyaddresses'] = 'proxyAddresses'; - } - - // connect to the directory - $this->ldap = new identity_from_directory_ldap_backend($ldap_config, $debug_ldap, $mail_domain, $ldap_config['search_fields']); - return $this->ldap->ready; - } -} - - -/** - * Utilize Roundcube's dedicated model class to access an LDAP-like address directory - * - * @link https://github.com/roundcube/roundcubemail/blob/master/program/lib/Roundcube/rcube_ldap.php - */ -class identity_from_directory_ldap_backend extends rcube_ldap -{ - public function __construct($props, $debug, $mail_domain, $search) - { - parent::__construct($props, $debug, $mail_domain); - $this->prop['search_fields'] = (array) $search; - } -} From ec08edb776c6e0168e859bee6208a16559ec1573 Mon Sep 17 00:00:00 2001 From: BohdanYatsenkoDarkGroup Date: Fri, 2 Jan 2026 00:48:21 +0100 Subject: [PATCH 3/3] Add files via upload Changed config.inc.php.dist and identify_from_director.php --- config.inc.php.dist | 274 +++++++++++++++++++++++ identity_from_directory.php | 423 ++++++++++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 config.inc.php.dist create mode 100644 identity_from_directory.php diff --git a/config.inc.php.dist b/config.inc.php.dist new file mode 100644 index 0000000..40d1c09 --- /dev/null +++ b/config.inc.php.dist @@ -0,0 +1,274 @@ +rc->user->data['username'] will be used by this plugin). +// 6. Define which directory object attributes to use for the Roundcube identity +// values and signature placeholders (fieldmap, the array keys are the names +// of the Roundcube values, the array values are the corresponding directory +// attribute names to grab the data from). +// +// This would result in the following plugin config value: +// +// $config['identity_from_directory_ldap'] = [ +// 'hosts' => [ 'ldaps://dc01.contoso.com:636' ], +// 'base_dn' => 'DC=contoso,DC=com', +// 'bind_dn' => 'CN=roundcube-svc,OU=service-accounts,DC=contoso,DC=com', +// 'bind_pass' => 'password-of-service-user-defined-in-bind_dn', +// 'scope' => 'sub', // search mode: sub|base|list +// 'filter' => '(&(sAMAccountType=805306368)(!(servicePrincipalName=*))(!(isCriticalSystemObject=TRUE))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))', +// 'search_fields' => ['mail', 'sAMAccountName'], +// 'fieldmap' => [ +// 'name' => 'displayName', +// 'email' => 'mail', +// 'surname' => 'sn', +// 'firstname' => 'givenName', +// 'jobtitle' => 'title', +// 'department' => 'department', +// 'phone' => 'telephoneNumber', +// 'fax' => 'facsimileTelephoneNumber', +// 'website' => 'wWWHomePage', +// 'company' => 'organization', +// 'username' => 'sAMAccountName', +// ], +// ]; +$config['identity_from_directory_ldap'] = [ + 'hosts' => ['ldaps://dc01.contoso.com:636'], + 'base_dn' => 'DC=contoso,DC=com', + 'bind_dn' => 'CN=roundcube-svc,OU=service-accounts,DC=contoso,DC=com', + 'bind_pass' => 'password-of-service-user-defined-in-bind_dn', + 'scope' => 'sub', // available search modes: sub|base|list + 'filter' => '(&(sAMAccountType=805306368)(!(servicePrincipalName=*))(!(isCriticalSystemObject=TRUE))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))', + 'search_fields' => ['mail', 'sAMAccountName'], + // fieldmap: + // - key: value name in Roundcube + // - values: corresponding attribute name in your LDAP + // + // You can use the mapped fields keys (e.g. 'foo") as placeholders in + // $config['identity_from_directory_signature_template_plaintext'] and + // $config['identity_from_directory_signature_template_html']). See the + // comment there for details. + 'fieldmap' => [ + // mandatory RC identity fields + 'name' => 'displayName', + 'email' => 'mail', + 'organization' => 'company', + // additional data + 'surname' => 'sn', + 'firstname' => 'givenName', + 'jobtitle' => 'title', + 'department' => 'department', + 'phone' => 'telephoneNumber', + 'fax' => 'facsimileTelephoneNumber', + 'website' => 'wWWHomePage', + 'username' => 'sAMAccountName', + ], +]; + + +// optional fallback values that will be used if a LDAP attribute defined +// in $config['identity_from_directory_ldap']['fieldmap'] provides no or empty +// data. Just remove or add keys you do (not) need to have a fallback value for. +$config['identity_from_directory_fallback_values'] = [ + 'organization' => 'ACME Inc.', + 'phone' => '+49 123 123 123', + //'fax' => '+49 123 123 999', + 'website' => 'https://example.com/', +]; + + +// Regular expression (used with preg_match()) to exclude email alias addresses +// from handling. +// +// An empty string ('') disables the feature (=all of a user's email alias +// addresses defined by the directory data will be handled). +// +// There are environments with unwanted but still needed email aliases. A good +// example is a company re-branding including a domain change, where the +// organization wants that users are not being able to use email addresses with +// the old domain as identity in Roundcube but still needs to be able to receive +// emails addressed to the old domain. +// +// Examples: +// - '/^.+@example\.com$/im' +// Excludes all email addresses ending with "@example.com" (not case +// sensitive). +// - '/^.+@example\.(com|net|org)$/im' +// Excludes all email addresses ending with "@example.com", "@example.net" or +// "@example.org" (case-insensitive) +$config['identity_from_directory_exclude_alias_regex'] = ''; + + +// Switch to control if unmanaged identities should be deleted. +// +// true: Delete all identities without a matching email address in the user's +// directory dataset (= identities not maintained by this plugin). Strongly +// recommended for automatic housekeeping in most environments once everything +// is working as expected. +// +// false: Do not delete unmanaged identities (= identities with no fitting +// email address in the directory record of the user). These identities will +// remain untouched until users delete them themselves or the identity gets +// updated because the email address was added to a user's directory data. +// +// It makes sense to start testing with false to prevent unwanted deletions on +// running systems until you are sure that all identities should be defined by +// the user's directory data. +// +// You can define exceptions from this automatic cleanup by using the +// $config['identity_from_directory_exclude_delete_unmanaged_regex'] option. +// This might be helpful for edge cases or when other plugins are in use +// that create or influence a user's identities. +$config['identity_from_directory_delete_unmanaged'] = false; + + +// Regular expression (used with preg_match()) to exclude identities with +// matching email addresses from automatic cleanup when +// $config['identity_from_directory_delete_unmanaged'] is true. +// +// An empty string ('') disables the exclusion feature (= all of a user's +// unmanaged identities are deleted). This setting has no effect if +// $config['identity_from_directory_delete_unmanaged'] is set to false (as this +// disables the entire automatic deletion cleanup mechanism). +// +// Examples: +// - '/^.+@example\.com$/im' +// Excludes all identities using email addresses ending with "@example.com" +// (case-insensitive). +// - '/^.+@example\.(com|net|org)$/im' +// Excludes all identities using email addresses ending with "@example.com", +// "@example.net" or "@example.org" (case-insensitive) +$config['identity_from_directory_exclude_delete_unmanaged_regex'] = ''; + + +// Switch for signature handling +// +// true: Overwrite existing signatures on each login (not only name, organization, +// email and other attributes). +// +// false: Do not touch or overwrite the signature of an identity (so a user can +// still maintain the signature value and format in a self-reliant way). +$config['identity_from_directory_update_signatures'] = true; + + +// Switch for signature creation on missing signatures +// +// true: Create signatures only if the corresponding identity currently has no +// signature (empty). This allows users to customize their signature after the +// initial creation without it being overwritten on subsequent logins. +// +// false: Do not create signatures if signature updates are disabled. +// +// Note: If $config['identity_from_directory_update_signatures'] is true, then +// signatures are overwritten on each login regardless of this option. +$config['identity_from_directory_create_signatures'] = false; + + +// Signature templates +// +// You can use each key from $config['identity_from_directory_ldap']['fieldmap'] +// as %placeholder(_html|url)%. They will be replaced with the values from the +// either the directory or $config['identity_from_directory_fallback_values'] +// (if there is no or empty data for this key in LDAP). Each value is available +// in three ways (as provided by LDAP, with encoded HTML entities and URL encoded +// for using it in URLs). +// +// Example for 'foo': +// - %foo%: raw value of 'foo' +// - %foo_html%: HTML entities encoded value of 'foo' +// - %foo_url%: URL encoded value of 'foo'. Additional optimizations are +// applied it the key is named 'email' (usage of Punycode for email domains), +// 'phone' or 'fax' (stripping of chars not compatible with tel:// URLs) +// +// $config['identity_from_directory_signature_template_plaintext'] will be used +// if $config['identity_from_directory_use_html_signature'] is false. +// +// On images in HTML signatures: +// - The easiest way is the usage of base64 data URLs with e.g. +// data:image/png;base64,[...]: +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs +// Most mail user agents (MUAs) are supporting this but check if the all over +// compatibility is good enough for you: +// https://www.caniemail.com/features/image-base64/ +// - The second option are CID (or Content-ID) images. They work by attaching +// the image to the email you send and then using standard HTML image tags +// that reference that image to embed it in the email when the user opens it. +// Most MUAs are supporting this and Roundcube also provides and uses this +// functionality for HTML signatures. But tinkering out how to use it for +// this plugin and documenting is still an open issue (contributions welcome): +// https://github.com/foundata/roundcube-plugin-identity-from-directory/issues/3 +$config['identity_from_directory_signature_template_html'] = ' +

+ Kind regards
+ %name_html%
+ %organization_html% +

+

+ mailto: %email_html%
+ phone: %phone_html%
+ web: %website_html% +

+'; + +$config['identity_from_directory_signature_template_plaintext'] = ' +Kind regards +%name% +%organization% + +mailto: %email% +phone: %phone% +web: %website% +'; + + +// Switch: Also search the 'proxyAddresses' field for email alias addresses? +// +// 'proxyAddresses' is a Active Directory user attribute which may contain +// a CSV string like 'smtp:foo@example.com,smtp:bar@example.net' or an +// array with such values. They represent alias email addresses (if any). +// The directory server needs to provide this field (which is the default +// for MS Active Directory but not for most other LDAP servers out there; +// Therefore this option defaults to false). +// +// true: The plugin will also maintain identities for email aliases in +// 'proxyAddresses' (if any). +// +// false: Do not care about email aliases in 'proxyAddresses'. +$config['identity_from_directory_handle_proxyaddresses'] = false; + + +// Switch for signature format +// +// true: use HTML instead of plain text signatures. +$config['identity_from_directory_use_html_signature'] = true; + + +// Switch for signature sanitation +// +// true: Use rcmail_action_settings_index::wash_html() on HTML signatures. +// You can disable this if you got problems with stripped HTML attributes +// and you are sure that you can trust the LDAP data in any case. +$config['identity_from_directory_wash_html_signature'] = true; + + +// Switch for logging additional debug data into the Roundcube log +// "identity_from_directory". +// +// true: Write LDAP search info, records and other useful debugging info into +// the log file. You might want to also set $config['ldap_debug'] = true for +// logging Roundcube's LDAP conversations, including the ones triggered by +// this plugin. +$config['identity_from_directory_debug'] = false; diff --git a/identity_from_directory.php b/identity_from_directory.php new file mode 100644 index 0000000..935a473 --- /dev/null +++ b/identity_from_directory.php @@ -0,0 +1,423 @@ +rc = rcmail::get_instance(); + + // Triggered after a user successfully logged in + // https://github.com/roundcube/roundcubemail/wiki/Plugin-Hooks#login_after + // This plugin is using it to update / edit existing identities + $this->add_hook('login_after', [$this, 'login_after']); + } + + + /** + * 'login_after' hook handler, used to create or update the user identities + */ + public function login_after($args) + { + $this->load_config('config.inc.php.dist'); // load the plugin's distribution config file as default + $this->load_config(); // merge with local configuration file (which can overwrite any settings) + if ($this->ldap) { + return $args; + } + $debug_plugin = (bool) $this->rc->config->get('identity_from_directory_debug'); + + // get user's data from directory and prepare it for further processing + $user_data = [ + 'name' => '', + 'email' => '', // default / main email address; IDN domains (no Punycode/ACE) + 'email_list' => [], // list of all email addresses (main, default, aliases); IDN domains (no Punycode/ACE) + 'managed_identity_ids' => [] // list of IDs of identities managed by this plugin + ]; + if ($this->init_ldap([ + 'mail_host' => $this->rc->user->data['mail_host'], + ])) { + // '*' does NOT search all fields but triggers the usage of 'search_fields' instead, see + // https://github.com/roundcube/roundcubemail/blob/master/program/lib/Roundcube/rcube_ldap.php#L900C49-L900C62 + // + // This 'search_fields' array gets set to the plugin's $config['identity_from_directory_ldap']['search_fields'] + // as connection property by $this->init_ldap(). So searching in '*' limits the fields to the plugin's config. + $results = $this->ldap->search('*', $this->rc->user->data['username'], true); + + if (count($results->records) === 1) { + + $ad_handle_proxyaddresses = (bool) $this->rc->config->get('identity_from_directory_handle_proxyaddresses'); + $exclude_alias_regex = (string) $this->rc->config->get('identity_from_directory_exclude_alias_regex'); + + $ldap_entry = $results->records[0]; + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'Found a record for ' . $this->rc->user->data['username'] . ': ' + . print_r($ldap_entry, true)); + } + + $user_data['name'] = is_array($ldap_entry['name']) ? $ldap_entry['name'][0] : $ldap_entry['name']; + $user_data['email'] = rcube_utils::idn_to_utf8(trim(is_array($ldap_entry['email']) ? $ldap_entry['email'][0] : $ldap_entry['email'])); + if (!empty($user_data['email'])) { + $user_data['email_list'][] = $user_data['email']; + } + + foreach (array_keys($ldap_entry) as $key) { + // add email addresses (main, aliases) to the list for the user + if (preg_match('/^email($|:)/i', $key)) { + foreach ((array) $ldap_entry[$key] as $alias) { + $alias = rcube_utils::idn_to_utf8(trim($alias)); + if (empty($alias) || self::email_in_array($alias, $user_data['email_list'])) { + continue; + } + if (strpos($alias, '@') === false || (!empty($exclude_alias_regex) && (preg_match($exclude_alias_regex, $alias)))) { + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'Excluded ' . $alias . ' from handling as it is an invalid email address or matching "' + . $exclude_alias_regex . '" (identity_from_directory_exclude_alias_regex).'); + } + continue; + } + $user_data['email_list'][] = $alias; + } + + // handle Active Directory attribute "proxyAddresses" + } elseif ($ad_handle_proxyaddresses && preg_match('/^proxyaddresses($|:)/i', $key)) { + // originally a CSV string like 'smtp:foo@exmaple.com,bar@example.net'. + // The used library returns as string if there is only one address and + // an array if there are multiple. + $proxyaddresses = $ldap_entry[$key]; + if (!is_array($proxyaddresses)) { + $proxyaddresses = [ $proxyaddresses ]; + } + foreach ((array) $proxyaddresses as $alias) { + $alias = rcube_utils::idn_to_utf8(trim(preg_replace('/^smtp:(.+)/i', '\1', trim($alias), 1))); + if (empty($alias) || self::email_in_array($alias, $user_data['email_list'])) { + continue; + } + if (strpos($alias, '@') === false || (!empty($exclude_alias_regex) && (preg_match($exclude_alias_regex, $alias)))) { + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'Excluded '. $alias . ' from handling as it is an invalid email address or matching "' + . $exclude_alias_regex . '" (identity_from_directory_exclude_alias_regex).'); + } + continue; + } + $user_data['email_list'][] = $alias; + } + + // add LDAP data but exclude _ID, _raw_attrib etc. and do not overwrite already existing keys + } elseif (strpos($key, '_') !== 0 && !array_key_exists($key, $user_data)) { + $user_data[$key] = $ldap_entry[$key]; + } + } + $user_data['email_list'] = array_unique($user_data['email_list']); + + } elseif ($debug_plugin && count($results->records) > 1) { + rcube::write_log('identity_from_directory', + 'Searching for '. $this->rc->user->data['username'] + . ' returned more than one result; all were ignored as unambiguous assignment is not possible.'); + } + } + if (empty($user_data['email_list'])) { + return $args; + } + + // get config and other data needed for further processing + $ldap_config = (array) $this->rc->config->get('identity_from_directory_ldap'); + $update_signatures = (bool) $this->rc->config->get('identity_from_directory_update_signatures'); + $create_signatures = (bool) $this->rc->config->get('identity_from_directory_create_signatures'); + $use_html_signature = (bool) $this->rc->config->get('identity_from_directory_use_html_signature'); + $wash_html_signature = (bool) $this->rc->config->get('identity_from_directory_wash_html_signature'); + if ($use_html_signature) { + $signature_template = (string) $this->rc->config->get('identity_from_directory_signature_template_html'); + } else { + $signature_template = (string) $this->rc->config->get('identity_from_directory_signature_template_plaintext'); + } + $signature_fallback_values = (array) $this->rc->config->get('identity_from_directory_fallback_values'); + $identities_existing = $this->rc->user->list_emails(); // list of all user emails (from identities), array with identity_id, name and email address + + // maintain an identity for each of the user's determined email addresses + foreach ((array) $user_data['email_list'] as $email) { + $hook_to_use = 'identity_create'; + $identity_id = 0; // often called 'iid' in other parts of RC sources + $is_standard = 0; // 1: use the identity as default (there can only be one) + + foreach ($identities_existing as $identity_existing) { + // case-insensitive search to update an existing identity, even if + // there are differences in capitalization. + if (self::email_in_array($identity_existing['email'], [ $email ])) { + $hook_to_use = 'identity_update'; + $identity_id = $identity_existing['identity_id']; + break; + } + } + + if ($user_data['email'] === $email) { + $is_standard = 1; + } + + // see https://github.com/roundcube/roundcubemail/blob/master/program/actions/settings/identity_save.php for available keys + $identity_record = [ + 'user_id' => $this->rc->user->ID, + 'standard' => $is_standard, + 'name' => (!empty($user_data['name']) ? $user_data['name'] : $this->rc->user->data['username']), + 'email' => $email, + 'organization' => (array_key_exists('organization', $user_data) ? $user_data['organization'] : ''), + ]; + + // Decide whether to set/overwrite the signature + // - update_signatures: always overwrite + // - create_signatures: set only if the current identity has an empty signature + $should_set_signature = false; + if ($update_signatures) { + $should_set_signature = true; + } elseif ($create_signatures) { + if ($identity_id === 0) { + // New identity => signature is missing + $should_set_signature = true; + } elseif (method_exists($this->rc->user, 'get_identity')) { + $existing_identity = $this->rc->user->get_identity($identity_id); + if (is_array($existing_identity) && array_key_exists('signature', $existing_identity)) { + $existing_signature = (string) $existing_identity['signature']; + $should_set_signature = (trim($existing_signature) === ''); + } + } + } + + if ($should_set_signature) { + // copy signature template + $signature = $signature_template; + + // add signature to identity record, replace placeholders in the signature template with + // the values from the directory or $config['identity_from_directory_fallback_values']: + // - %foo%: raw value of field 'foo' + // - %foo_html%: HTML entities encoded value of field 'foo' + // - %foo_url%: URL encoded value of field 'foo'. Additional optimizations are + // applied for the fields 'email' (usage of Punycode for email domains), + // 'phone' and 'fax' (stripping of chars not compatible with tel:// URLs) + foreach (array_keys($ldap_config['fieldmap']) as $placeholder) { + $replace_raw = ''; + if ($placeholder === 'email') { + // Use the correct email address (alias) of the corresponding identity for + // the %email%, %email_html% and %email_url% placeholders instead of the + // single mapped value returned by the directory (which should be stored in + // $user_data['email']). Otherwise, the same single email address value would + // be used for all of the user's identities (even the one of alias addresses). + $replace_raw = (string) $email; + } elseif (array_key_exists($placeholder, $user_data) && ((string) $user_data[$placeholder] !== '')) { + $replace_raw = (string) $user_data[$placeholder]; + } elseif (array_key_exists($placeholder, $signature_fallback_values) && ((string) $signature_fallback_values[$placeholder] !== '')) { + $replace_raw = (string) $signature_fallback_values[$placeholder]; + } elseif (array_key_exists($placeholder, $identity_record) && ((string) $identity_record[$placeholder] !== '')) { + $replace_raw = (string) $identity_record[$placeholder]; + } else { + continue; + } + + $replace_html = ''; + $replace_html = htmlspecialchars($replace_raw, \ENT_NOQUOTES, RCUBE_CHARSET); + + $replace_url = ''; + if ($placeholder === 'phone' || $placeholder === 'fax') { + // strip some chars for "tel://" URL usage + $replace_url = urlencode(preg_replace('/[^+0-9]+/', '', $replace_raw)); + } elseif ($placeholder === 'email') { + // use Punycode/ACE for "mailto://" URL usage + $replace_url = urlencode(rcube_utils::idn_to_ascii($replace_raw)); + } else { + $replace_url = urlencode($replace_raw); + } + + $signature = str_replace([ '%'. $placeholder . '%', + '%'. $placeholder . '_html%', + '%'. $placeholder . '_url%' ], + [ $replace_raw, + $replace_html, + $replace_url ], $signature); + } + + $identity_record['html_signature'] = ($use_html_signature) ? 1 : 0; + $identity_record['signature'] = ($use_html_signature && $wash_html_signature) ? rcmail_action_settings_index::wash_html($signature) : $signature; // XSS protection + } + + $plugin = $this->rc->plugins->exec_hook($hook_to_use, [ + 'id' => $identity_id, + 'record' => $identity_record, + ]); + + if (!$plugin['abort'] && !empty($plugin['record']['email'])) { + if ($identity_id === 0) { + $identity_id = $this->rc->user->insert_identity($plugin['record']); + } else { + $this->rc->user->update_identity($identity_id, $plugin['record']); + } + } + + if (empty($identity_id)) { + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'The identity for user \'' . $this->rc->user->data['username'] . '\' ' + . 'could not be saved' . (!empty($plugin['message']) ? ' ( ' . $plugin['message'] . ' ): ' : ': ') + . print_r($identity_record, true)); + } + continue; + } + + // Store the ID of the identity as managed. Any optionally needed cleanup + // action gets a lot easier if there is a list of identities known to be + // managed for the current user by this plugin. + $user_data['managed_identity_ids'][] = $identity_id; + } + + // delete identities which are not managed by this plugin + $delete_unmanaged = (bool) $this->rc->config->get('identity_from_directory_delete_unmanaged'); + $exclude_delete_unmanaged_regex = (string) $this->rc->config->get('identity_from_directory_exclude_delete_unmanaged_regex'); + if ($delete_unmanaged) { + $identity_existing_count = count($identities_existing); + foreach ($identities_existing as $identity_existing) { + + if ($identity_existing_count > 1 && !(in_array($identity_existing['identity_id'], $user_data['managed_identity_ids']))) { + + if (!empty($exclude_delete_unmanaged_regex) && preg_match($exclude_delete_unmanaged_regex, $identity_existing['email'])) { + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'Excluded identity ' . $identity_existing['identity_id'] . ' of user ' + . $this->rc->user->data['username'] . ' from automatic deletion even ' + . 'though it is not managed by this plugin; it\'s email address ' + . $identity_existing['email'] . ' is matching "' . $exclude_delete_unmanaged_regex + . '" (identity_from_directory_exclude_delete_unmanaged_regex).'); + } + continue; + } + + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'Deleting identity '. $identity_existing['identity_id'] .' with email address ' + . $identity_existing['email'] . ' because it is not managed by this plugin.'); + } + + if (!($this->rc->user->delete_identity($identity_existing['identity_id'])) && $debug_plugin) { + rcube::write_log('identity_from_directory', + 'Could note delete identity '. $identity_existing['identity_id'] + . ' with email address '.$identity_existing['email']); + } + $identity_existing_count--; + + } + + } + } + + return $args; + } + + + /** + * Search for an email address in an array of email addresses. The search + * will ignores differences in capitalization or Punycode/ACE. + * + * RFC 5321 (Simple Mail Transfer Protocol) section 2.3.11 leaves it up + * to the host if the "local-part" in "local-part@domain" is case- + * insensitively. De-facto, it gets handled case-insensitive by most + * systems out there and users are expecting that Foo@example.com == + * foo@example.com quite often. This function also acts like this. + * + * @param string $needle Value to seek. + * @param array $haystack Array to seek in. + * @return bool + */ + public static function email_in_array($needle, $haystack) + { + $haystack_new = []; + foreach($haystack as $key => $value) { + $haystack_new[$key] = mb_strtolower(rcube_utils::idn_to_utf8(trim($value)), 'UTF-8'); + } + return in_array(mb_strtolower(rcube_utils::idn_to_utf8(trim($needle)), 'UTF-8'), $haystack_new); + } + + + /** + * Initialize LDAP backend connection + */ + private function init_ldap($args) + { + // check if connection is already initialized / nothing to do + if ($this->ldap) { + return $this->ldap->ready; + } + + // Get config and set some fallback / default values + // See this plugin's config.php.dist for a detailled description of the settings + $this->load_config('config.inc.php.dist'); // load the plugin's distribution config file as default + $this->load_config(); // merge with local configuration file (which can overwrite any settings) + + $debug_plugin = (bool) $this->rc->config->get('identity_from_directory_debug'); + $debug_ldap = (bool) $this->rc->config->get('ldap_debug'); + $mail_domain = (string) $this->rc->config->mail_domain($args['mail_host']); + + $ldap_config = (array) $this->rc->config->get('identity_from_directory_ldap'); + if (!array_key_exists('searchonly', $ldap_config)) { + $ldap_config['searchonly'] = true; + } + if (!array_key_exists('search_fields', $ldap_config) || !is_array($ldap_config['search_fields'])) { + $ldap_config['search_fields'] = ['mail', 'sAMAccountName', 'username']; + } + if (empty($ldap_config) || + empty($ldap_config['search_fields']) || + empty($ldap_config['fieldmap'])) { + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'The plugin config seems to be invalid, please check $config[\'identity_from_directory_ldap\'].'); + } + return false; + } + if (!array_key_exists('name', $ldap_config['fieldmap']) || + !array_key_exists('email', $ldap_config['fieldmap']) || + !array_key_exists('organization', $ldap_config['fieldmap'])) { + if ($debug_plugin) { + rcube::write_log('identity_from_directory', + 'The plugin config seems to be invalid, please check $config[\'identity_from_directory_ldap\'][\'fieldmap\'].'); + } + return false; + } + + // add mapping for the "proxyAddresses" attribute (which stores email aliases when using Active Directory) + $ad_handle_proxyaddresses = $this->rc->config->get('identity_from_directory_handle_proxyaddresses'); + if ($ad_handle_proxyaddresses) { + $ldap_config['fieldmap']['proxyaddresses'] = 'proxyAddresses'; + } + + // connect to the directory + $this->ldap = new identity_from_directory_ldap_backend($ldap_config, $debug_ldap, $mail_domain, $ldap_config['search_fields']); + return $this->ldap->ready; + } +} + + +/** + * Utilize Roundcube's dedicated model class to access an LDAP-like address directory + * + * @link https://github.com/roundcube/roundcubemail/blob/master/program/lib/Roundcube/rcube_ldap.php + */ +class identity_from_directory_ldap_backend extends rcube_ldap +{ + public function __construct($props, $debug, $mail_domain, $search) + { + parent::__construct($props, $debug, $mail_domain); + $this->prop['search_fields'] = (array) $search; + } +}