form = new IdentSwitchForm($this); $this->switcher = new IdentSwitchSwitcher(); $this->preconfig = new IdentSwitchPreconfig($this); $this->checker = new IdentSwitchChecker(); $this->add_hook('startup', [$this, 'on_startup']); $this->add_hook('render_page', [$this, 'on_render_page']); $this->add_hook('refresh', [$this, 'on_refresh']); $this->add_hook('smtp_connect', [$this, 'on_smtp_connect']); $this->add_hook('managesieve_connect', [$this, 'on_managesieve_connect']); $this->add_hook('identity_form', [$this, 'on_identity_form']); $this->add_hook('identity_update', [$this, 'on_identity_update']); $this->add_hook('identity_create', [$this, 'on_identity_create']); $this->add_hook('identity_create_after', [$this, 'on_identity_create_after']); $this->add_hook('identity_delete', [$this, 'on_identity_delete']); $this->add_hook('template_object_composeheaders', [$this, 'on_template_object_composeheaders']); $this->add_hook('preferences_list', [$this, 'on_special_folders_form']); $this->add_hook('preferences_save', [$this, 'on_special_folders_update']); $this->register_action('plugin.ident_switch.switch', [$this, 'on_switch']); $this->load_config(); $rc = rcmail::get_instance(); foreach (rcube_storage::$folder_types as $type) { $key = $type . '_mbox_default' . self::MY_POSTFIX; if (empty($_SESSION[$key])) { $_SESSION[$key] = $rc->config->get($type . '_mbox'); } } } /** * Handle startup hook: detect impersonation, disable caches, restore folder config. * * @param array $args Hook arguments containing 'task' and other startup data. * @return array Modified hook arguments. */ public function on_startup(array $args): array { $rc = rcmail::get_instance(); if (strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0) { // We are impersonating $rc->config->set('imap_cache', null); $rc->config->set('messages_cache', false); if ($args['task'] === 'mail') { $this->add_texts('localization/'); $rc->config->set('create_default_folders', false); } } foreach (rcube_storage::$folder_types as $type) { $defaultKey = $type . '_mbox_default' . self::MY_POSTFIX; $otherKey = $type . '_mbox' . self::MY_POSTFIX; $val = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey]; $rc->config->set($type . '_mbox', $val); } return $args; } /** * Handle render_page hook: inject account switcher or settings form script. * * @param array $args Hook arguments containing page rendering data. * @return array Modified hook arguments. */ public function on_render_page(array $args): array { $rc = rcmail::get_instance(); match ($rc->task) { 'mail' => $this->render_switch($rc, $args), 'settings' => $this->include_script('ident_switch-form.js'), default => null, }; if ($rc->task === 'mail') { $this->include_stylesheet('ident_switch.css'); } return $args; } /** * Render the account switcher dropdown in the mail view. * * Queries the database for all enabled alternative accounts and generates * an HTML select element with an unread badge, injected into the page footer. * * @param rcmail $rc Roundcube instance. * @param array $args Hook arguments for page rendering. */ private function render_switch(rcmail $rc, array $args): void { // Currently selected identity $iid_s = $_SESSION['iid' . self::MY_POSTFIX] ?? null; $iid = 0; if (is_int($iid_s)) { $iid = $iid_s; } elseif ($iid_s === '-1') { $iid = -1; } elseif (is_string($iid_s) && ctype_digit($iid_s)) { $iid = intval($iid_s); } $accNames = [$_SESSION['global_alias'] ?? $rc->user->data['username']]; $accValues = [-1]; $accSelected = -1; $iidMap = [0 => -1]; // primary account: iid 0 → select value -1 // Get list of alternative accounts $sql = "SELECT " . "isw.id, isw.iid, isw.label, isw.username, ii.email" . " FROM" . " {$rc->db->table_name(self::TABLE)} isw" . " INNER JOIN {$rc->db->table_name('identities')} ii ON isw.iid=ii.identity_id" . " WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.parent_id IS NULL"; $qRec = $rc->db->query($sql, $rc->user->data['user_id'], self::DB_ENABLED); while ($r = $rc->db->fetch_assoc($qRec)) { $accValues[] = $r['id']; $iidMap[$r['iid']] = $r['id']; if ($iid === (int)$r['iid']) { $accSelected = $r['id']; } $lbl = $r['label'] ?: $r['username'] ?: $r['email']; $accNames[] = $lbl; } if (count($accValues) <= 1) { return; } $this->include_script('ident_switch-switch.js'); // Pass config to JS environment $rc->output->set_env('ident_switch_iid_map', $iidMap); $select = new html_select([ 'id' => 'plugin-ident_switch-account', 'style' => 'display: none; padding: 0;', 'onchange' => 'plugin_switchIdent_switch(this.value);', ]); $select->add($accNames, $accValues); $html = '' . $select->show([$accSelected]) . '' . ''; $rc->output->add_footer($html); if (!$rc->config->get('ident_switch.check_mail', true)) { return; } // Run initial check and pass counts via env $this->checker->check_new_mail([]); $counts = $_SESSION['ident_switch_counts'] ?? []; $initialCounts = []; foreach ($counts as $cIid => $info) { $initialCounts[$cIid] = [ 'unseen' => $info['unseen'], 'baseline' => $info['baseline'] ?? $info['unseen'], ]; } $rc->output->set_env('ident_switch_initial_counts', $initialCounts); } /** * Handle refresh hook: check new mail on secondary identities. * * @param array $args Hook arguments (empty for refresh). * @return array Unmodified hook arguments. */ public function on_refresh(array $args): array { $rc = rcmail::get_instance(); if (!$rc->config->get('ident_switch.check_mail', true)) { return $args; } return $this->checker->check_new_mail($args); } /** * Handle smtp_connect hook: configure SMTP settings for the active account. * * @param array $args Hook arguments containing SMTP connection parameters. * @return array Modified hook arguments with updated SMTP settings. */ public function on_smtp_connect(array $args): array { return $this->switcher->configure_smtp($args); } /** * Handle managesieve_connect hook: configure Sieve settings for the active account. * * @param array $args Hook arguments containing Sieve connection parameters. * @return array Modified hook arguments with updated Sieve settings. */ public function on_managesieve_connect(array $args): array { return $this->switcher->configure_managesieve($args); } /** * Handle identity_form hook: add plugin-specific fields to the identity editor. * * @param array $args Hook arguments containing 'record' with identity data. * @return array Modified hook arguments with added form sections. */ public function on_identity_form(array $args): array { return $this->form->on_identity_form($args, $this->preconfig); } /** * Handle identity_update hook: validate and save plugin fields on identity edit. * * @param array $args Hook arguments containing 'id' and 'record' with identity data. * @return array Modified hook arguments, with 'abort' set on validation failure. */ public function on_identity_update(array $args): array { return $this->form->on_identity_update($args); } /** * Handle identity_create hook: validate plugin fields before identity creation. * * @param array $args Hook arguments containing 'record' with identity data. * @return array Modified hook arguments, with 'abort' set on validation failure. */ public function on_identity_create(array $args): array { return $this->form->on_identity_create($args); } /** * Handle identity_create_after hook: persist plugin data after identity creation. * * @param array $args Hook arguments containing 'id' (new identity_id) and 'record'. * @return array Unmodified hook arguments. */ public function on_identity_create_after(array $args): array { return $this->form->on_identity_create_after($args); } /** * Handle identity_delete hook: remove plugin data when an identity is deleted. * * @param array $args Hook arguments containing 'id' of the identity being deleted. * @return array Unmodified hook arguments. */ public function on_identity_delete(array $args): array { return $this->form->on_identity_delete($args); } /** * Handle template_object_composeheaders hook: filter and fix identity selection. * * Restricts the From dropdown to identities belonging to the active account * (the account itself + its aliases). When impersonating, also pre-selects * the correct identity. * * @param array $args Hook arguments containing form element 'id'. */ public function on_template_object_composeheaders(array $args): void { if ($args['id'] !== '_from') { return; } $rc = rcmail::get_instance(); $userId = $rc->user->ID; $isImpersonating = strcasecmp($_SESSION['username'], $rc->user->data['username']) !== 0; // Pre-select the active identity when impersonating if ($isImpersonating) { if (isset($_SESSION['iid' . self::MY_POSTFIX])) { $iid = $_SESSION['iid' . self::MY_POSTFIX]; $rc->output->add_script("plugin_switchIdent_fixIdent({$iid});", 'docready'); } else { self::write_log('Special session variable with active identity ID not found.'); } } // Compute allowed identity IDs for the From dropdown $iidSession = $_SESSION['iid' . self::MY_POSTFIX] ?? null; if ($isImpersonating && is_numeric($iidSession) && (int)$iidSession > 0) { // Switched to a secondary account: allow its identity + aliases $sql = 'SELECT id FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ? AND parent_id IS NULL'; $q = $rc->db->query($sql, (int)$iidSession, $userId); $r = $rc->db->fetch_assoc($q); if ($r) { $accountId = (int)$r['id']; $sql = 'SELECT iid FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE (id = ? OR parent_id = ?) AND user_id = ? AND flags & ? > 0'; $q = $rc->db->query($sql, $accountId, $accountId, $userId, self::DB_ENABLED); $allowed = []; while ($row = $rc->db->fetch_assoc($q)) { $allowed[] = (int)$row['iid']; } $rc->output->set_env('ident_switch_allowed_identities', $allowed); } } else { // Primary account: exclude identities belonging to enabled accounts/aliases $sql = 'SELECT iid FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE user_id = ? AND flags & ? > 0'; $q = $rc->db->query($sql, $userId, self::DB_ENABLED); $excluded = []; while ($row = $rc->db->fetch_assoc($q)) { $excluded[] = (int)$row['iid']; } if (!empty($excluded)) { $sql = 'SELECT identity_id FROM ' . $rc->db->table_name('identities') . ' WHERE user_id = ? AND del = 0'; $q = $rc->db->query($sql, $userId); $allowed = []; while ($row = $rc->db->fetch_assoc($q)) { $iid = (int)$row['identity_id']; if (!in_array($iid, $excluded)) { $allowed[] = $iid; } } $rc->output->set_env('ident_switch_allowed_identities', $allowed); } } } /** * Handle preferences_list hook: customize special folders form for remote accounts. * * @param array $args Hook arguments containing 'section' and 'blocks' with form data. * @return array Modified hook arguments with updated folder selections. */ public function on_special_folders_form(array $args): array { return $this->switcher->get_special_folders_form($args); } /** * Handle preferences_save hook: persist special folder assignments for remote accounts. * * @param array $args Hook arguments containing 'section' and 'prefs' with folder data. * @return array Modified hook arguments, with 'abort' set to prevent default save. */ public function on_special_folders_update(array $args): array { return $this->switcher->save_special_folders($args); } /** * Handle the account switch action (AJAX). */ public function on_switch(): void { $this->switcher->switch_account(); } /** * Trim a string, returning null if the result is empty. * * @param string|null $str Input string. * @return string|null Trimmed string or null if empty. */ public static function ntrim(?string $str): ?string { if ($str === null) { return null; } $s = trim($str); return $s !== '' ? $s : null; } /** * Parse scheme prefix (ssl://, tls://) from a host string. * * @param string $host Host string, optionally prefixed with ssl:// or tls://. * @return array{scheme: string, host: string} Parsed scheme and bare host. */ public static function parse_host_scheme(string $host): array { $lower = strtolower($host); if (str_starts_with($lower, 'ssl://')) { return ['scheme' => 'ssl', 'host' => substr($host, 6)]; } if (str_starts_with($lower, 'tls://')) { return ['scheme' => 'tls', 'host' => substr($host, 6)]; } return ['scheme' => '', 'host' => $host]; } /** * Resolve username for an identity: use stored username or fall back to email. * * @param integer $iid Identity ID. * @param string|null $username Stored username (may be empty). * @return string Resolved username. */ public static function resolve_username(int $iid, ?string $username): string { if (!empty($username)) { return $username; } $rc = rcmail::get_instance(); $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; $q = $rc->db->query($sql, $iid); $r = $rc->db->fetch_assoc($q); return $r['email'] ?? ''; } /** * Write a message to the plugin's log file. * * @param string $txt Log message. */ public static function write_log(string $txt): void { rcmail::get_instance()->write_log('ident_switch', $txt); } /** * Write a debug message (only when ident_switch.debug is enabled). * * @param string $txt Log message. */ public static function debug_log(string $txt): void { if (rcmail::get_instance()->config->get('ident_switch.debug', false)) { rcmail::get_instance()->write_log('ident_switch', '[DEBUG] ' . $txt); } } }