From 651bb19d053acf60a8c4a867b30dedb9b63eaf26 Mon Sep 17 00:00:00 2001 From: Laurent Dinclaux Date: Tue, 10 Feb 2026 10:21:09 +1100 Subject: [PATCH] Split ident_switch.php into focused classes Extract business logic into dedicated classes under lib/: - IdentSwitchForm: form building, validation, and persistence - IdentSwitchSwitcher: account switching, SMTP config, special folders - IdentSwitchPreconfig: domain-based preconfiguration The main plugin class remains the orchestrator, registering hooks and delegating to the appropriate handler. Constants changed from private to public to allow cross-class access. --- ident_switch-form.js | 4 +- ident_switch.php | 734 ++--------------------------------- lib/IdentSwitchForm.php | 504 ++++++++++++++++++++++++ lib/IdentSwitchPreconfig.php | 98 +++++ lib/IdentSwitchSwitcher.php | 304 +++++++++++++++ 5 files changed, 937 insertions(+), 707 deletions(-) create mode 100644 lib/IdentSwitchForm.php create mode 100644 lib/IdentSwitchPreconfig.php create mode 100644 lib/IdentSwitchSwitcher.php diff --git a/ident_switch-form.js b/ident_switch-form.js index 9d0c9b8..9a98574 100644 --- a/ident_switch-form.js +++ b/ident_switch-form.js @@ -21,16 +21,14 @@ function plugin_switchIdent_processPreconfig() { var disVal = disFld.val(); if (disVal > 0) { $("INPUT[name='_ident_switch.form.imap.host']").prop("disabled", true); - $("SELECT[name='_ident_switch.form.imap.tls']").prop("disabled", true); + $("INPUT[name='_ident_switch.form.imap.tls']").prop("disabled", true); $("INPUT[name='_ident_switch.form.imap.port']").prop("disabled", true); $("INPUT[name='_ident_switch.form.smtp.host']").prop("disabled", true); - $("SELECT[name='_ident_switch.form.smtp.tls']").prop("disabled", true); $("INPUT[name='_ident_switch.form.smtp.port']").prop("disabled", true); } if (2 == disVal) { $("INPUT[name='_ident_switch.form.imap.username']").prop("disabled", true); - $("INPUT[name='_ident_switch.form.smtp.username']").prop("disabled", true); } } diff --git a/ident_switch.php b/ident_switch.php index 2151d7e..4475161 100644 --- a/ident_switch.php +++ b/ident_switch.php @@ -13,34 +13,47 @@ * * @url https://github.com/Gecka-apps/ident_switch */ + +require_once __DIR__ . '/lib/IdentSwitchPreconfig.php'; +require_once __DIR__ . '/lib/IdentSwitchForm.php'; +require_once __DIR__ . '/lib/IdentSwitchSwitcher.php'; + class ident_switch extends rcube_plugin { /** @var string Task regex: active on all tasks except login/logout. */ public $task = '?(?!login|logout).*'; /** @var string Database table name for this plugin. */ - private const TABLE = 'ident_switch'; + public const TABLE = 'ident_switch'; /** @var string Session variable suffix used to store/restore state. */ - private const MY_POSTFIX = '_iswitch'; + public const MY_POSTFIX = '_iswitch'; /** @var int Flag: account switching is enabled. */ - private const DB_ENABLED = 1; + public const DB_ENABLED = 1; /** @var int Flag: use TLS for IMAP connection. */ - private const DB_SECURE_IMAP_TLS = 4; + public const DB_SECURE_IMAP_TLS = 4; /** @var int SMTP authentication: use same credentials as IMAP. */ - private const SMTP_AUTH_IMAP = 1; + public const SMTP_AUTH_IMAP = 1; /** @var int SMTP authentication: no authentication required. */ - private const SMTP_AUTH_NONE = 2; + public const SMTP_AUTH_NONE = 2; + + private IdentSwitchForm $form; + private IdentSwitchSwitcher $switcher; + private IdentSwitchPreconfig $preconfig; /** * Initialize plugin: register hooks, actions, and save default folder config. */ public function init(): void { + $this->form = new IdentSwitchForm($this); + $this->switcher = new IdentSwitchSwitcher(); + $this->preconfig = new IdentSwitchPreconfig($this); + $this->add_hook('startup', [$this, 'on_startup']); $this->add_hook('render_page', [$this, 'on_render_page']); $this->add_hook('smtp_connect', [$this, 'on_smtp_connect']); @@ -184,199 +197,23 @@ class ident_switch extends rcube_plugin /** * Handle smtp_connect hook: configure SMTP settings for the active account. * - * Loads SMTP host, port, credentials, and TLS settings from the database - * for the currently selected identity. - * * @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 { - $iid = $_SESSION['iid' . self::MY_POSTFIX] ?? null; - if (!is_numeric($iid) || $iid == -1) { - self::write_log('no identity switch is selected... trying to find related smtp server from the from header'); - $requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST); - if (empty($requestFrom)) { - self::write_log('no _from post parameter found... falling back to original default config'); - return $args; - } - - $iid = intval($requestFrom); - if ($iid === 0) { - self::write_log('falling back to original default config as _from post field is no integer: ' . $_POST['_from']); - return $args; - } - } - - $rc = rcmail::get_instance(); - - $sql = 'SELECT smtp_host, flags, smtp_port, username, smtp_auth, password FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ?'; - $q = $rc->db->query($sql, $iid, $rc->user->ID); - $r = $rc->db->fetch_assoc($q); - if (is_array($r)) { - if (!$r['username']) { - // Load email from identity - $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; - $q = $rc->db->query($sql, $iid); - $rIid = $rc->db->fetch_assoc($q); - $r['username'] = $rIid['email']; - } - - $args['smtp_user'] = $r['username']; - $args['smtp_pass'] = $r['smtp_auth'] == self::SMTP_AUTH_IMAP ? $rc->decrypt($r['password']) : ''; - - // In RC 1.6+ smtp_server was renamed to smtp_host and includes port - $smtpHost = $r['smtp_host'] ?: 'localhost'; - - if ($r['flags'] & self::DB_SECURE_IMAP_TLS) { - if (str_contains($smtpHost, ':')) { - self::write_log('SMTP server already contains protocol, ignoring TLS flag.'); - } else { - $smtpHost = 'tls://' . $smtpHost; - } - } - - $smtpPort = $r['smtp_port'] ?: 587; - $args['smtp_host'] = $smtpHost . ':' . $smtpPort; - } - - return $args; - } - - /** - * Build the common form fields for identity settings. - * - * @param array $record Identity record data used for placeholders. - * @return array Form field definitions for enabled, label, and readonly. - */ - private static function get_common_form(array &$record): array - { - $prefix = 'ident_switch.form.common.'; - return [ - $prefix . 'enabled' => ['type' => 'checkbox', 'onchange' => 'plugin_switchIdent_enabled_onChange();'], - $prefix . 'label' => ['type' => 'text', 'size' => 32, 'placeholder' => $record['email']], - $prefix . 'readonly' => ['type' => 'hidden'], - ]; - } - - /** - * Build the IMAP form fields for identity settings. - * - * @param array $record Identity record data used for placeholders. - * @return array Form field definitions for IMAP host, port, TLS, username, password, delimiter. - */ - private static function get_imap_form(array &$record): array - { - $prefix = 'ident_switch.form.imap.'; - return [ - $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'], - $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 143], - $prefix . 'tls' => ['type' => 'checkbox'], - $prefix . 'username' => ['type' => 'text', 'size' => 64, 'placeholder' => $record['email']], - $prefix . 'password' => ['type' => 'password', 'size' => 64], - $prefix . 'delimiter' => ['type' => 'text', 'size' => 1, 'placeholder' => '.'], - ]; - } - - /** - * Build the SMTP form fields for identity settings. - * - * @param array $record Identity record data used for default auth type selection. - * @return array Form field definitions for SMTP host, port, and auth type. - */ - private function get_smtp_form(array &$record): array - { - $prefix = 'ident_switch.form.smtp.'; - - $authType = new html_select(['name' => "_{$prefix}auth"]); - $authType->add($this->gettext('form.smtp.auth.imap'), self::SMTP_AUTH_IMAP); - $authType->add($this->gettext('form.smtp.auth.none'), self::SMTP_AUTH_NONE); - - return [ - $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'], - $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 587], - $prefix . 'auth' => ['value' => $authType->show([$record['ident_switch.form.smtp.auth']])], - ]; + return $this->switcher->configure_smtp($args); } /** * Handle identity_form hook: add plugin-specific fields to the identity editor. * - * Loads existing account data from the database (if editing) or applies - * preconfigured settings, then adds Common/IMAP/SMTP sections to the form. - * * @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 { - $rc = rcmail::get_instance(); - - // Do not show options for default identity - if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { - return $args; - } - - $this->add_texts('localization'); - - $row = null; - if (isset($args['record']['identity_id'])) { - $sql = 'SELECT * FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ?'; - $q = $rc->db->query($sql, $args['record']['identity_id'], $rc->user->ID); - $row = $rc->db->fetch_assoc($q); - } - - $record = &$args['record']; - - // Load data if exists - if ($row) { - $dbToForm = [ - 'label' => 'common.label', - 'imap_host' => 'imap.host', - 'imap_port' => 'imap.port', - 'imap_delimiter' => 'imap.delimiter', - 'username' => 'imap.username', - 'password' => 'imap.password', - 'smtp_host' => 'smtp.host', - 'smtp_port' => 'smtp.port', - 'smtp_auth' => 'smtp.auth', - ]; - foreach ($row as $k => $v) { - if (isset($dbToForm[$k])) { - $record['ident_switch.form.' . $dbToForm[$k]] = $v; - } - } - - // Parse flags - $record['ident_switch.form.common.enabled'] = (bool)($row['flags'] & self::DB_ENABLED); - $record['ident_switch.form.imap.tls'] = (bool)($row['flags'] & self::DB_SECURE_IMAP_TLS); - - // Set readonly if needed - $cfg = $this->get_preconfig($record['email']); - if (is_array($cfg) && ($cfg['readonly'] ?? false)) { - $record['ident_switch.form.common.readonly'] = 1; - if (in_array(strtoupper($cfg['user'] ?? ''), ['EMAIL', 'MBOX'])) { - $record['ident_switch.form.common.readonly'] = 2; - } - } - } else { - $this->apply_preconfig($record); - } - - $args['form']['ident_switch.common'] = [ - 'name' => $this->gettext('form.common.caption'), - 'content' => self::get_common_form($record), - ]; - $args['form']['ident_switch.imap'] = [ - 'name' => $this->gettext('form.imap.caption'), - 'content' => self::get_imap_form($record), - ]; - $args['form']['ident_switch.smtp'] = [ - 'name' => $this->gettext('form.smtp.caption'), - 'content' => $this->get_smtp_form($record), - ]; - - return $args; + return $this->form->on_identity_form($args, $this->preconfig); } /** @@ -387,94 +224,29 @@ class ident_switch extends rcube_plugin */ public function on_identity_update(array $args): array { - $rc = rcmail::get_instance(); - - // Do not do anything for default identity - if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { - return $args; - } - - if (!self::get_field_value('common', 'enabled', false)) { - self::sw_imap_off($args['id']); - return $args; - } - - $data = self::check_field_values(); - if ($data['err']) { - $this->add_texts('localization'); - $args['abort'] = true; - $args['message'] = 'ident_switch.err.' . $data['err']; - return $args; - } - - $data['id'] = $args['id']; - self::save_field_values($rc, $data); - - return $args; + return $this->form->on_identity_update($args); } /** * Handle identity_create hook: validate plugin fields before identity creation. * - * Stores validated data in session since identity_id is not yet available. - * * @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 { - $rc = rcmail::get_instance(); - - // Do not do anything for default identity - if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { - return $args; - } - - if (!self::get_field_value('common', 'enabled', false)) { - return $args; - } - - $data = self::check_field_values(); - if ($data['err']) { - $this->add_texts('localization'); - $args['abort'] = true; - $args['message'] = 'ident_switch.err.' . $data['err']; - } - - // Save data for _after (cannot pass with $args) - $_SESSION['createData' . self::MY_POSTFIX] = $data; - - return $args; + return $this->form->on_identity_create($args); } /** * Handle identity_create_after hook: persist plugin data after identity creation. * - * Retrieves validated data from session and saves it with the new identity_id. - * * @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 { - $rc = rcmail::get_instance(); - - // Do not do anything for default identity - if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { - return $args; - } - - $data = $_SESSION['createData' . self::MY_POSTFIX] ?? null; - - unset($_SESSION['createData' . self::MY_POSTFIX]); - if (!$data || count($data) === 0) { - self::write_log("Object with ident_switch values not found in session for ID = {$args['id']}."); - } else { - $data['id'] = $args['id']; - self::save_field_values($rc, $data); - } - - return $args; + return $this->form->on_identity_create_after($args); } /** @@ -485,16 +257,7 @@ class ident_switch extends rcube_plugin */ public function on_identity_delete(array $args): array { - $rc = rcmail::get_instance(); - - $sql = 'DELETE FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ?'; - $q = $rc->db->query($sql, $args['id'], $rc->user->ID); - - if ($rc->db->affected_rows($q)) { - self::write_log("Deleted associated information for identity with ID = {$args['id']}."); - } - - return $args; + return $this->form->on_identity_delete($args); } /** @@ -522,468 +285,31 @@ class ident_switch extends rcube_plugin /** * Handle preferences_list hook: customize special folders form for remote accounts. * - * When viewing folder preferences while impersonating, shows the remote account's - * special folder assignments instead of the default ones. - * * @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 { - $rc = rcmail::get_instance(); - - if ($args['section'] === 'folders' - && strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0 - ) { - $no_override = array_flip((array)$rc->config->get('dont_override')); - $onchange = "if ($(this).val() == 'INBOX') $(this).val('')"; - $select = $rc->folder_selector([ - 'noselection' => '---', - 'realnames' => true, - 'maxlength' => 30, - 'folder_filter' => 'mail', - 'folder_rights' => 'w', - ]); - - $sql = 'SELECT label FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ?'; - $q = $rc->db->query($sql, $_SESSION['iid' . self::MY_POSTFIX], $rc->user->ID); - $r = $rc->db->fetch_assoc($q); - $args['blocks']['main']['name'] .= ' (' . ($r['label'] ? rcube::Q($rc->gettext('server')) . ': ' . $r['label'] : 'remote') . ')'; - - foreach (rcube_storage::$folder_types as $type) { - if (isset($no_override[$type . '_mbox'])) { - continue; - } - - $defaultKey = $type . '_mbox_default' . self::MY_POSTFIX; - $otherKey = $type . '_mbox' . self::MY_POSTFIX; - $selected = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey] ?? ''; - $attr = ['id' => '_' . $type . '_mbox', 'name' => '_' . $type . '_mbox', 'onchange' => $onchange]; - $args['blocks']['main']['options'][$type . '_mbox']['content'] = $select->show($selected, $attr); - } - } - - return $args; + return $this->switcher->get_special_folders_form($args); } /** * Handle preferences_save hook: persist special folder assignments for remote accounts. * - * Saves folder preferences to the plugin's database table instead of the default - * Roundcube preferences when impersonating a remote account. - * * @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 { - $rc = rcmail::get_instance(); - - if ($args['section'] === 'folders' - && strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0 - ) { - $sql = 'SELECT id FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ?'; - $q = $rc->db->query($sql, $_SESSION['iid' . self::MY_POSTFIX], $rc->user->ID); - $r = $rc->db->fetch_assoc($q); - if ($r) { - $sql = 'UPDATE ' . - $rc->db->table_name(self::TABLE) . - ' SET drafts_mbox = ?, sent_mbox = ?, junk_mbox = ?, trash_mbox = ?' . - ' WHERE id = ?'; - - $rc->db->query( - $sql, - $args['prefs']['drafts_mbox'], - $args['prefs']['sent_mbox'], - $args['prefs']['junk_mbox'], - $args['prefs']['trash_mbox'], - $r['id'] - ); - - // Abort to prevent RC from saving prefs to default storage - $args['abort'] = true; - $args['result'] = true; - - foreach (rcube_storage::$folder_types as $type) { - if (!empty($args['prefs'][$type . '_mbox'])) { - $otherKey = $type . '_mbox' . self::MY_POSTFIX; - $_SESSION[$otherKey] = $args['prefs'][$type . '_mbox']; - } - } - return $args; - } - - $args['abort'] = true; - $args['result'] = false; - return $args; - } - - foreach (rcube_storage::$folder_types as $type) { - if (!empty($args['prefs'][$type . '_mbox'])) { - $key = $type . '_mbox_default' . self::MY_POSTFIX; - $_SESSION[$key] = $args['prefs'][$type . '_mbox']; - } - } - return $args; - } - - /** - * Validate all plugin form field values from POST data. - * - * Checks length constraints, port ranges, and parses flags. - * Also retrieves and includes the password. - * - * @return array Validated data with field values and optional 'err' key on failure. - */ - private static function check_field_values(): array - { - $retVal = []; - - $retVal['label'] = self::get_field_value('common', 'label'); - if (strlen($retVal['label'] ?? '') > 32) { - $retVal['err'] = 'label.long'; - return $retVal; - } - - $retVal['imap.host'] = self::get_field_value('imap', 'host'); - if (strlen($retVal['imap.host'] ?? '') > 64) { - $retVal['err'] = 'host.long'; - return $retVal; - } - - $retVal['imap.port'] = self::get_field_value('imap', 'port'); - if ($retVal['imap.port'] && !ctype_digit($retVal['imap.port'])) { - $retVal['err'] = 'port.num'; - return $retVal; - } - if ($retVal['imap.port'] && ($retVal['imap.port'] <= 0 || $retVal['imap.port'] > 65535)) { - $retVal['err'] = 'port.range'; - return $retVal; - } - - $retVal['imap.delimiter'] = self::get_field_value('imap', 'delimiter'); - if (strlen($retVal['imap.delimiter'] ?? '') > 1) { - $retVal['err'] = 'delim.long'; - return $retVal; - } - - $retVal['imap.user'] = self::get_field_value('imap', 'username'); - if (strlen($retVal['imap.user'] ?? '') > 64) { - $retVal['err'] = 'user.long'; - return $retVal; - } - - $retVal['smtp.host'] = self::get_field_value('smtp', 'host'); - if (strlen($retVal['smtp.host'] ?? '') > 64) { - $retVal['err'] = 'host.long'; - return $retVal; - } - - $retVal['smtp.port'] = self::get_field_value('smtp', 'port'); - if ($retVal['smtp.port'] && !ctype_digit($retVal['smtp.port'])) { - $retVal['err'] = 'port.num'; - return $retVal; - } - if ($retVal['smtp.port'] && ($retVal['smtp.port'] <= 0 || $retVal['smtp.port'] > 65535)) { - $retVal['err'] = 'port.range'; - return $retVal; - } - - $retVal['smtp.auth'] = self::get_field_value('smtp', 'auth'); - if (!ctype_digit($retVal['smtp.auth'] ?? '')) { - $retVal['err'] = 'auth.num'; - return $retVal; - } - - // Get also password - $retVal['imap.pass'] = self::get_field_value('imap', 'password', false, true); - - // Parse secure settings - $retVal['flags'] = self::DB_ENABLED; - - $tls = self::get_field_value('imap', 'tls', false); - if ($tls) { - $retVal['flags'] |= self::DB_SECURE_IMAP_TLS; - } - - return $retVal; - } - - /** - * Retrieve a form field value from POST input. - * - * @param string $section Form section name (common, imap, smtp). - * @param string $field Field name within the section. - * @param bool $trim Whether to trim and nullify empty values. - * @param bool $html Whether to allow HTML in the value. - * @return string|null The field value, or null if empty and trimmed. - */ - private static function get_field_value(string $section, string $field, bool $trim = true, bool $html = false): ?string - { - $retVal = rcube_utils::get_input_value( - "_ident_switch_form_{$section}_{$field}", - rcube_utils::INPUT_POST, - $html - ); - if (!$trim) { - return $retVal; - } - - return self::ntrim($retVal); - } - - /** - * Persist plugin field values to the database (insert or update). - * - * Handles password encryption and determines whether to create a new - * record or update an existing one. - * - * @param rcmail $rc Roundcube instance for DB access and encryption. - * @param array $data Validated field data including 'id' (identity_id). - * @return bool True if a query was executed, false otherwise. - */ - private static function save_field_values(rcmail $rc, array $data): bool - { - $sql = 'SELECT id, password FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE iid = ? AND user_id = ?'; - $q = $rc->db->query($sql, $data['id'], $rc->user->ID); - $r = $rc->db->fetch_assoc($q); - if ($r) { - // Record already exists, will update it - $sql = 'UPDATE ' . - $rc->db->table_name(self::TABLE) . - ' SET flags = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?, smtp_host = ?, smtp_port = ?, smtp_auth = ?, user_id = ?, iid = ?' . - ' WHERE id = ?'; - } elseif ($data['flags'] & self::DB_ENABLED) { - // No record exists, create new one - $sql = 'INSERT INTO ' . - $rc->db->table_name(self::TABLE) . - '(flags, label, imap_host, imap_port, imap_delimiter, username, password, smtp_host, smtp_port, smtp_auth, user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - } else { - return false; - } - - // Do we need to update pwd? - if ($data['imap.pass'] !== ($r['password'] ?? null)) { - $data['imap.pass'] = $rc->encrypt($data['imap.pass']); - } - - $rc->db->query( - $sql, - $data['flags'], - $data['label'], - $data['imap.host'], - $data['imap.port'], - $data['imap.delimiter'], - $data['imap.user'], - $data['imap.pass'], - $data['smtp.host'], - $data['smtp.port'], - $data['smtp.auth'], - $rc->user->ID, - $data['id'], - $r['id'] ?? null - ); - - return true; + return $this->switcher->save_special_folders($args); } /** * Handle the account switch action (AJAX). - * - * Saves current account state to session, loads the target account's - * IMAP/SMTP configuration, and redirects to INBOX. - * When switching back to default (id=-1), restores the original session state. */ public function on_switch(): void { - $rc = rcmail::get_instance(); - - $my_postfix_len = strlen(self::MY_POSTFIX); - $identId = rcube_utils::get_input_value('_ident-id', rcube_utils::INPUT_POST); - - $rc->session->remove('folders'); - $rc->session->remove('unseen_count'); - - if ($identId == -1) { - // Switch to main account - self::write_log('Switching mailbox back to default.'); - - // Restore everything with STORAGE*my_postfix - foreach ($_SESSION as $k => $v) { - if (str_starts_with(strtolower($k), 'storage') && str_ends_with($k, self::MY_POSTFIX)) { - $realKey = substr($k, 0, -$my_postfix_len); - $_SESSION[$realKey] = $_SESSION[$k]; - $rc->session->remove($k); - } - } - - $_SESSION['imap_delimiter'] = $_SESSION['imap_delimiter' . self::MY_POSTFIX] ?? '.'; - $_SESSION['username'] = $rc->user->data['username']; - $_SESSION['password'] = $_SESSION['password' . self::MY_POSTFIX]; - $_SESSION['iid' . self::MY_POSTFIX] = -1; - - foreach (rcube_storage::$folder_types as $type) { - $otherKey = $type . '_mbox' . self::MY_POSTFIX; - if (isset($_SESSION[$otherKey])) { - $rc->session->remove($otherKey); - } - } - } else { - $sql = 'SELECT imap_host, flags, imap_port, imap_delimiter, drafts_mbox, sent_mbox, junk_mbox, trash_mbox, username, password, iid FROM ' . $rc->db->table_name(self::TABLE) . ' WHERE id = ? AND user_id = ?'; - $q = $rc->db->query($sql, $identId, $rc->user->ID); - $r = $rc->db->fetch_assoc($q); - if (is_array($r)) { - if (!$r['username']) { - // Load email from identity - $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; - $q = $rc->db->query($sql, $r['iid']); - $rIid = $rc->db->fetch_assoc($q); - $r['username'] = $rIid['email']; - } - - self::write_log("Switching mailbox to one for identity with ID = {$r['iid']} (username = '{$r['username']}')."); - - if ($_SESSION['username'] === $rc->user->data['username']) { - // If we are in default account now - save values - foreach ($_SESSION as $k => $v) { - if (str_starts_with(strtolower($k), 'storage') && !str_ends_with($k, self::MY_POSTFIX)) { - if (!isset($_SESSION[$k . self::MY_POSTFIX])) { - $_SESSION[$k . self::MY_POSTFIX] = $_SESSION[$k]; - } - $rc->session->remove($k); - } - } - - foreach (['password', 'imap_delimiter'] as $k) { - if (!isset($_SESSION[$k . self::MY_POSTFIX])) { - $_SESSION[$k . self::MY_POSTFIX] = $_SESSION[$k]; - } - $rc->session->remove($k); - } - } - - $def_port = 143; - $ssl = null; - if ($r['flags'] & self::DB_SECURE_IMAP_TLS) { - $ssl = 'tls'; - } - $port = $r['imap_port'] ?: $def_port; - - $host = $r['imap_host'] ?: 'localhost'; - if ($ssl && !str_starts_with(strtolower($host), "{$ssl}://")) { - $host = "{$ssl}://" . $host; - } - - $delimiter = $r['imap_delimiter'] ?: '.'; - - $_SESSION['storage_host'] = $host; - $_SESSION['storage_ssl'] = $ssl; - $_SESSION['storage_port'] = $port; - $_SESSION['imap_delimiter'] = $delimiter; - $_SESSION['username'] = $r['username']; - $_SESSION['password'] = $r['password']; - $_SESSION['iid' . self::MY_POSTFIX] = $r['iid']; - - foreach (rcube_storage::$folder_types as $type) { - if (!empty($r[$type . '_mbox'])) { - $otherKey = $type . '_mbox' . self::MY_POSTFIX; - $_SESSION[$otherKey] = $r[$type . '_mbox']; - } - } - } else { - self::write_log("Requested remote mailbox with ID = {$identId} not found."); - return; - } - } - - $rc->output->redirect([ - '_task' => 'mail', - '_mbox' => 'INBOX', - ]); - } - - /** - * Disable the ident_switch flag for a given identity. - * - * @param int $iid Identity ID to disable. - */ - private static function sw_imap_off(int $iid): void - { - $rc = rcmail::get_instance(); - - $sql = 'UPDATE ' . $rc->db->table_name(self::TABLE) . ' SET flags = flags & ? WHERE iid = ? AND user_id = ?'; - $rc->db->query($sql, ~self::DB_ENABLED, $iid, $rc->user->ID); - } - - /** - * Load preconfigured settings for a domain from config. - * - * @param string $email Email address to extract domain from. - * @return array|false Preconfig array for the domain, or false if not found. - */ - private function get_preconfig(string $email): array|false - { - $dom = substr(strstr($email, '@'), 1); - if (!$dom) { - return false; - } - - $this->load_config(); - - $cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []); - $cfg = $cfg[$dom] ?? null; - - if ($cfg) { - if (empty($cfg['host'])) { - return false; - } - } - return $cfg ?: false; - } - - /** - * Apply preconfigured settings to an identity form record. - * - * Parses the host URL to extract scheme, host, and port, then sets - * the username based on the config's 'user' setting (email or mbox). - * - * @param array $record Identity record to modify (passed by reference). - * @return bool True if the preconfig is readonly, false otherwise. - */ - private function apply_preconfig(array &$record): bool - { - $email = $record['email']; - $cfg = $this->get_preconfig($email); - if (is_array($cfg)) { - self::write_log("Applying predefined configuration for '{$email}'."); - - if (!empty($cfg['host'])) { - // Parse and set host and related - $urlArr = parse_url($cfg['host']); - - $record['ident_switch.form.imap.host'] = $record['ident_switch.form.smtp.host'] = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : ''; - $record['ident_switch.form.imap.port'] = $record['ident_switch.form.smtp.port'] = !empty($urlArr['port']) ? intval($urlArr['port']) : ''; - - $record['ident_switch.form.imap.tls'] = strcasecmp($urlArr['scheme'] ?? '', 'tls') === 0; - } - - $loginSet = false; - if (!empty($cfg['user'])) { - match (strtoupper($cfg['user'])) { - 'EMAIL' => ($record['ident_switch.form.imap.username'] = $email) && ($loginSet = true), - 'MBOX' => ($record['ident_switch.form.imap.username'] = strstr($email, '@', true)) && ($loginSet = true), - default => null, - }; - } - - if (!empty($cfg['readonly'])) { - $record['ident_switch.form.common.readonly'] = $loginSet ? 2 : 1; - } - - return (bool)($cfg['readonly'] ?? false); - } - - return false; + $this->switcher->switch_account(); } /** @@ -992,7 +318,7 @@ class ident_switch extends rcube_plugin * @param string|null $str Input string. * @return string|null Trimmed string or null if empty. */ - private static function ntrim(?string $str): ?string + public static function ntrim(?string $str): ?string { if ($str === null) { return null; @@ -1007,7 +333,7 @@ class ident_switch extends rcube_plugin * * @param string $txt Log message. */ - private static function write_log(string $txt): void + public static function write_log(string $txt): void { rcmail::get_instance()->write_log('ident_switch', $txt); } diff --git a/lib/IdentSwitchForm.php b/lib/IdentSwitchForm.php new file mode 100644 index 0000000..840fdaa --- /dev/null +++ b/lib/IdentSwitchForm.php @@ -0,0 +1,504 @@ +plugin = $plugin; + } + + /** + * Build the common form fields for identity settings. + * + * @param array $record Identity record data used for placeholders. + * @return array Form field definitions for enabled, label, and readonly. + */ + public static function get_common_fields(array &$record): array + { + $prefix = 'ident_switch.form.common.'; + return [ + $prefix . 'enabled' => ['type' => 'checkbox', 'onchange' => 'plugin_switchIdent_enabled_onChange();'], + $prefix . 'label' => ['type' => 'text', 'size' => 32, 'placeholder' => $record['email'] ?? ''], + $prefix . 'readonly' => ['type' => 'hidden'], + ]; + } + + /** + * Build the IMAP form fields for identity settings. + * + * @param array $record Identity record data used for placeholders. + * @return array Form field definitions for IMAP host, port, TLS, username, password, delimiter. + */ + public static function get_imap_fields(array &$record): array + { + $prefix = 'ident_switch.form.imap.'; + return [ + $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'], + $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 143], + $prefix . 'tls' => ['type' => 'checkbox'], + $prefix . 'username' => ['type' => 'text', 'size' => 64, 'placeholder' => $record['email'] ?? ''], + $prefix . 'password' => ['type' => 'password', 'size' => 64], + $prefix . 'delimiter' => ['type' => 'text', 'size' => 1, 'placeholder' => '.'], + ]; + } + + /** + * Build the SMTP form fields for identity settings. + * + * @param array $record Identity record data used for default auth type selection. + * @return array Form field definitions for SMTP host, port, and auth type. + */ + public function get_smtp_fields(array &$record): array + { + $prefix = 'ident_switch.form.smtp.'; + + $authType = new html_select(['name' => "_{$prefix}auth"]); + $authType->add($this->plugin->gettext('form.smtp.auth.imap'), ident_switch::SMTP_AUTH_IMAP); + $authType->add($this->plugin->gettext('form.smtp.auth.none'), ident_switch::SMTP_AUTH_NONE); + + return [ + $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'], + $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 587], + $prefix . 'auth' => ['value' => $authType->show([$record['ident_switch.form.smtp.auth'] ?? null])], + ]; + } + + /** + * Handle identity_form hook: add plugin-specific fields to the identity editor. + * + * Loads existing account data from the database (if editing) or applies + * preconfigured settings, then adds Common/IMAP/SMTP sections to the form. + * + * @param array $args Hook arguments containing 'record' with identity data. + * @param IdentSwitchPreconfig $preconfig Preconfiguration handler. + * @return array Modified hook arguments with added form sections. + */ + public function on_identity_form(array $args, IdentSwitchPreconfig $preconfig): array + { + $rc = rcmail::get_instance(); + + // When creating a new identity, record may be null + if ($args['record'] === null) { + $args['record'] = []; + } + + // Do not show options for default identity + if (!empty($args['record']['email']) && strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { + return $args; + } + + $this->plugin->add_texts('localization'); + + $row = null; + if (isset($args['record']['identity_id'])) { + $sql = 'SELECT * FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $args['record']['identity_id'], $rc->user->ID); + $row = $rc->db->fetch_assoc($q); + } + + $record = &$args['record']; + + // Load data if exists + if ($row) { + $dbToForm = [ + 'label' => 'common.label', + 'imap_host' => 'imap.host', + 'imap_port' => 'imap.port', + 'imap_delimiter' => 'imap.delimiter', + 'username' => 'imap.username', + 'password' => 'imap.password', + 'smtp_host' => 'smtp.host', + 'smtp_port' => 'smtp.port', + 'smtp_auth' => 'smtp.auth', + ]; + foreach ($row as $k => $v) { + if (isset($dbToForm[$k])) { + $record['ident_switch.form.' . $dbToForm[$k]] = $v; + } + } + + // Parse flags + $record['ident_switch.form.common.enabled'] = (bool)($row['flags'] & ident_switch::DB_ENABLED); + $record['ident_switch.form.imap.tls'] = (bool)($row['flags'] & ident_switch::DB_SECURE_IMAP_TLS); + + // Set readonly if needed + $cfg = $preconfig->get($record['email']); + if (is_array($cfg) && ($cfg['readonly'] ?? false)) { + $record['ident_switch.form.common.readonly'] = 1; + if (in_array(strtoupper($cfg['user'] ?? ''), ['EMAIL', 'MBOX'])) { + $record['ident_switch.form.common.readonly'] = 2; + } + } + } else { + $preconfig->apply($record); + } + + $args['form']['ident_switch.common'] = [ + 'name' => $this->plugin->gettext('form.common.caption'), + 'content' => self::get_common_fields($record), + ]; + $args['form']['ident_switch.imap'] = [ + 'name' => $this->plugin->gettext('form.imap.caption'), + 'content' => self::get_imap_fields($record), + ]; + $args['form']['ident_switch.smtp'] = [ + 'name' => $this->plugin->gettext('form.smtp.caption'), + 'content' => $this->get_smtp_fields($record), + ]; + + return $args; + } + + /** + * 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 + { + $rc = rcmail::get_instance(); + + // Do not do anything for default identity + if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { + return $args; + } + + if (!self::get_field_value('common', 'enabled', false)) { + $this->disable($args['id']); + return $args; + } + + $data = $this->validate(); + if (!empty($data['err'])) { + $this->plugin->add_texts('localization'); + $args['abort'] = true; + $args['message'] = 'ident_switch.err.' . $data['err']; + return $args; + } + + $this->apply_readonly_preconfig($data, $args['record']['email']); + $data['id'] = $args['id']; + $this->save($rc, $data); + + return $args; + } + + /** + * Handle identity_create hook: validate plugin fields before identity creation. + * + * Stores validated data in session since identity_id is not yet available. + * + * @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 + { + $rc = rcmail::get_instance(); + + // Do not do anything for default identity + if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { + return $args; + } + + if (!self::get_field_value('common', 'enabled', false)) { + return $args; + } + + $data = $this->validate(); + if (!empty($data['err'])) { + $this->plugin->add_texts('localization'); + $args['abort'] = true; + $args['message'] = 'ident_switch.err.' . $data['err']; + } + + $this->apply_readonly_preconfig($data, $args['record']['email']); + + // Save data for _after (cannot pass with $args) + $_SESSION['createData' . ident_switch::MY_POSTFIX] = $data; + + return $args; + } + + /** + * Handle identity_create_after hook: persist plugin data after identity creation. + * + * Retrieves validated data from session and saves it with the new identity_id. + * + * @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 + { + $rc = rcmail::get_instance(); + + // Do not do anything for default identity + if (strcasecmp($args['record']['email'], $rc->user->data['username']) === 0) { + return $args; + } + + $data = $_SESSION['createData' . ident_switch::MY_POSTFIX] ?? null; + + unset($_SESSION['createData' . ident_switch::MY_POSTFIX]); + if (!$data || count($data) === 0) { + ident_switch::write_log("Object with ident_switch values not found in session for ID = {$args['id']}."); + } else { + $data['id'] = $args['id']; + $this->save($rc, $data); + } + + return $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 + { + $rc = rcmail::get_instance(); + + $sql = 'DELETE FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $args['id'], $rc->user->ID); + + if ($rc->db->affected_rows($q)) { + ident_switch::write_log("Deleted associated information for identity with ID = {$args['id']}."); + } + + return $args; + } + + /** + * Fill in missing validated data from preconfig when identity is readonly. + * + * When a preconfig has readonly=true, form fields are disabled in the + * browser and not submitted via POST. This fills the missing values + * from the preconfig on the server side. + * + * @param array $data Validated data from validate(), modified in place. + * @param string $email Email address of the identity. + */ + private function apply_readonly_preconfig(array &$data, string $email): void + { + $preconfig = new IdentSwitchPreconfig($this->plugin); + $cfg = $preconfig->get($email); + if (!is_array($cfg) || empty($cfg['readonly'])) { + return; + } + + // Build temporary record to get preconfig values + $record = ['email' => $email]; + $preconfig->apply($record); + + // Map preconfig record keys to validated data keys + $map = [ + 'ident_switch.form.imap.host' => 'imap.host', + 'ident_switch.form.imap.port' => 'imap.port', + 'ident_switch.form.imap.username' => 'imap.user', + 'ident_switch.form.smtp.host' => 'smtp.host', + 'ident_switch.form.smtp.port' => 'smtp.port', + ]; + + foreach ($map as $recordKey => $dataKey) { + if (empty($data[$dataKey]) && isset($record[$recordKey])) { + $data[$dataKey] = $record[$recordKey]; + } + } + + // Handle TLS flag from preconfig + if (!empty($record['ident_switch.form.imap.tls'])) { + $data['flags'] |= ident_switch::DB_SECURE_IMAP_TLS; + } + } + + /** + * Validate all plugin form field values from POST data. + * + * Checks length constraints, port ranges, and parses flags. + * Also retrieves and includes the password. + * + * @return array Validated data with field values and optional 'err' key on failure. + */ + public function validate(): array + { + $retVal = []; + + $retVal['label'] = self::get_field_value('common', 'label'); + if (strlen($retVal['label'] ?? '') > 32) { + $retVal['err'] = 'label.long'; + return $retVal; + } + + $retVal['imap.host'] = self::get_field_value('imap', 'host'); + if (strlen($retVal['imap.host'] ?? '') > 64) { + $retVal['err'] = 'host.long'; + return $retVal; + } + + $retVal['imap.port'] = self::get_field_value('imap', 'port'); + if ($retVal['imap.port'] && !ctype_digit($retVal['imap.port'])) { + $retVal['err'] = 'port.num'; + return $retVal; + } + if ($retVal['imap.port'] && ($retVal['imap.port'] <= 0 || $retVal['imap.port'] > 65535)) { + $retVal['err'] = 'port.range'; + return $retVal; + } + + $retVal['imap.delimiter'] = self::get_field_value('imap', 'delimiter'); + if (strlen($retVal['imap.delimiter'] ?? '') > 1) { + $retVal['err'] = 'delim.long'; + return $retVal; + } + + $retVal['imap.user'] = self::get_field_value('imap', 'username'); + if (strlen($retVal['imap.user'] ?? '') > 64) { + $retVal['err'] = 'user.long'; + return $retVal; + } + + $retVal['smtp.host'] = self::get_field_value('smtp', 'host'); + if (strlen($retVal['smtp.host'] ?? '') > 64) { + $retVal['err'] = 'host.long'; + return $retVal; + } + + $retVal['smtp.port'] = self::get_field_value('smtp', 'port'); + if ($retVal['smtp.port'] && !ctype_digit($retVal['smtp.port'])) { + $retVal['err'] = 'port.num'; + return $retVal; + } + if ($retVal['smtp.port'] && ($retVal['smtp.port'] <= 0 || $retVal['smtp.port'] > 65535)) { + $retVal['err'] = 'port.range'; + return $retVal; + } + + $retVal['smtp.auth'] = self::get_field_value('smtp', 'auth'); + if (!ctype_digit($retVal['smtp.auth'] ?? '')) { + $retVal['err'] = 'auth.num'; + return $retVal; + } + + // Get also password + $retVal['imap.pass'] = self::get_field_value('imap', 'password', false, true); + + // Parse secure settings + $retVal['flags'] = ident_switch::DB_ENABLED; + + $tls = self::get_field_value('imap', 'tls', false); + if ($tls) { + $retVal['flags'] |= ident_switch::DB_SECURE_IMAP_TLS; + } + + return $retVal; + } + + /** + * Retrieve a form field value from POST input. + * + * @param string $section Form section name (common, imap, smtp). + * @param string $field Field name within the section. + * @param bool $trim Whether to trim and nullify empty values. + * @param bool $html Whether to allow HTML in the value. + * @return string|null The field value, or null if empty and trimmed. + */ + public static function get_field_value(string $section, string $field, bool $trim = true, bool $html = false): ?string + { + $retVal = rcube_utils::get_input_value( + "_ident_switch_form_{$section}_{$field}", + rcube_utils::INPUT_POST, + $html + ); + if (!$trim) { + return $retVal; + } + + return ident_switch::ntrim($retVal); + } + + /** + * Persist plugin field values to the database (insert or update). + * + * Handles password encryption and determines whether to create a new + * record or update an existing one. + * + * @param rcmail $rc Roundcube instance for DB access and encryption. + * @param array $data Validated field data including 'id' (identity_id). + * @return bool True if a query was executed, false otherwise. + */ + public function save(rcmail $rc, array $data): bool + { + $sql = 'SELECT id, password FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $data['id'], $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + if ($r) { + // Record already exists, will update it + $sql = 'UPDATE ' . + $rc->db->table_name(ident_switch::TABLE) . + ' SET flags = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?, smtp_host = ?, smtp_port = ?, smtp_auth = ?, user_id = ?, iid = ?' . + ' WHERE id = ?'; + } elseif ($data['flags'] & ident_switch::DB_ENABLED) { + // No record exists, create new one + $sql = 'INSERT INTO ' . + $rc->db->table_name(ident_switch::TABLE) . + '(flags, label, imap_host, imap_port, imap_delimiter, username, password, smtp_host, smtp_port, smtp_auth, user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + } else { + return false; + } + + // Do we need to update pwd? + if ($data['imap.pass'] !== ($r['password'] ?? null)) { + $data['imap.pass'] = $rc->encrypt($data['imap.pass']); + } + + $rc->db->query( + $sql, + $data['flags'], + $data['label'], + $data['imap.host'], + $data['imap.port'], + $data['imap.delimiter'], + $data['imap.user'], + $data['imap.pass'], + $data['smtp.host'], + $data['smtp.port'], + $data['smtp.auth'], + $rc->user->ID, + $data['id'], + $r['id'] ?? null + ); + + return true; + } + + /** + * Disable the ident_switch flag for a given identity. + * + * @param int $iid Identity ID to disable. + */ + public function disable(int $iid): void + { + $rc = rcmail::get_instance(); + + $sql = 'UPDATE ' . $rc->db->table_name(ident_switch::TABLE) . ' SET flags = flags & ? WHERE iid = ? AND user_id = ?'; + $rc->db->query($sql, ~ident_switch::DB_ENABLED, $iid, $rc->user->ID); + } +} diff --git a/lib/IdentSwitchPreconfig.php b/lib/IdentSwitchPreconfig.php new file mode 100644 index 0000000..1811da8 --- /dev/null +++ b/lib/IdentSwitchPreconfig.php @@ -0,0 +1,98 @@ +plugin = $plugin; + } + + /** + * Load preconfigured settings for a domain from config. + * + * @param string $email Email address to extract domain from. + * @return array|false Preconfig array for the domain, or false if not found. + */ + public function get(string $email): array|false + { + $dom = substr(strstr($email, '@'), 1); + if (!$dom) { + return false; + } + + $this->plugin->load_config(); + + $cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []); + $cfg = $cfg[$dom] ?? null; + + if ($cfg) { + if (empty($cfg['host'])) { + return false; + } + } + return $cfg ?: false; + } + + /** + * Apply preconfigured settings to an identity form record. + * + * Parses the host URL to extract scheme, host, and port, then sets + * the username based on the config's 'user' setting (email or mbox). + * + * @param array $record Identity record to modify (passed by reference). + * @return bool True if the preconfig is readonly, false otherwise. + */ + public function apply(array &$record): bool + { + $email = $record['email'] ?? ''; + if (empty($email)) { + return false; + } + + $cfg = $this->get($email); + if (is_array($cfg)) { + ident_switch::write_log("Applying predefined configuration for '{$email}'."); + + if (!empty($cfg['host'])) { + $urlArr = parse_url($cfg['host']); + + $record['ident_switch.form.imap.host'] = $record['ident_switch.form.smtp.host'] = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : ''; + $record['ident_switch.form.imap.port'] = $record['ident_switch.form.smtp.port'] = !empty($urlArr['port']) ? intval($urlArr['port']) : ''; + + $record['ident_switch.form.imap.tls'] = strcasecmp($urlArr['scheme'] ?? '', 'tls') === 0; + } + + $loginSet = false; + if (!empty($cfg['user'])) { + match (strtoupper($cfg['user'])) { + 'EMAIL' => ($record['ident_switch.form.imap.username'] = $email) && ($loginSet = true), + 'MBOX' => ($record['ident_switch.form.imap.username'] = strstr($email, '@', true)) && ($loginSet = true), + default => null, + }; + } + + if (!empty($cfg['readonly'])) { + $record['ident_switch.form.common.readonly'] = $loginSet ? 2 : 1; + } + + return (bool)($cfg['readonly'] ?? false); + } + + return false; + } +} diff --git a/lib/IdentSwitchSwitcher.php b/lib/IdentSwitchSwitcher.php new file mode 100644 index 0000000..c60dda6 --- /dev/null +++ b/lib/IdentSwitchSwitcher.php @@ -0,0 +1,304 @@ +session->remove('folders'); + $rc->session->remove('unseen_count'); + + if ($identId == -1) { + // Switch to main account + ident_switch::write_log('Switching mailbox back to default.'); + + // Restore everything with STORAGE*my_postfix + foreach ($_SESSION as $k => $v) { + if (str_starts_with(strtolower($k), 'storage') && str_ends_with($k, ident_switch::MY_POSTFIX)) { + $realKey = substr($k, 0, -$my_postfix_len); + $_SESSION[$realKey] = $_SESSION[$k]; + $rc->session->remove($k); + } + } + + $_SESSION['imap_delimiter'] = $_SESSION['imap_delimiter' . ident_switch::MY_POSTFIX] ?? '.'; + $_SESSION['username'] = $rc->user->data['username']; + $_SESSION['password'] = $_SESSION['password' . ident_switch::MY_POSTFIX]; + $_SESSION['iid' . ident_switch::MY_POSTFIX] = -1; + + foreach (rcube_storage::$folder_types as $type) { + $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; + if (isset($_SESSION[$otherKey])) { + $rc->session->remove($otherKey); + } + } + } else { + $sql = 'SELECT imap_host, flags, imap_port, imap_delimiter, drafts_mbox, sent_mbox, junk_mbox, trash_mbox, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?'; + $q = $rc->db->query($sql, $identId, $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + if (is_array($r)) { + if (!$r['username']) { + // Load email from identity + $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; + $q = $rc->db->query($sql, $r['iid']); + $rIid = $rc->db->fetch_assoc($q); + $r['username'] = $rIid['email']; + } + + ident_switch::write_log("Switching mailbox to one for identity with ID = {$r['iid']} (username = '{$r['username']}')."); + + if ($_SESSION['username'] === $rc->user->data['username']) { + // If we are in default account now - save values + foreach ($_SESSION as $k => $v) { + if (str_starts_with(strtolower($k), 'storage') && !str_ends_with($k, ident_switch::MY_POSTFIX)) { + if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) { + $_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k]; + } + $rc->session->remove($k); + } + } + + foreach (['password', 'imap_delimiter'] as $k) { + if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) { + $_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k]; + } + $rc->session->remove($k); + } + } + + $def_port = 143; + $ssl = null; + if ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS) { + $ssl = 'tls'; + } + $port = $r['imap_port'] ?: $def_port; + + $host = $r['imap_host'] ?: 'localhost'; + if ($ssl && !str_starts_with(strtolower($host), "{$ssl}://")) { + $host = "{$ssl}://" . $host; + } + + $delimiter = $r['imap_delimiter'] ?: '.'; + + $_SESSION['storage_host'] = $host; + $_SESSION['storage_ssl'] = $ssl; + $_SESSION['storage_port'] = $port; + $_SESSION['imap_delimiter'] = $delimiter; + $_SESSION['username'] = $r['username']; + $_SESSION['password'] = $r['password']; + $_SESSION['iid' . ident_switch::MY_POSTFIX] = $r['iid']; + + foreach (rcube_storage::$folder_types as $type) { + if (!empty($r[$type . '_mbox'])) { + $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; + $_SESSION[$otherKey] = $r[$type . '_mbox']; + } + } + } else { + ident_switch::write_log("Requested remote mailbox with ID = {$identId} not found."); + return; + } + } + + $rc->output->redirect([ + '_task' => 'mail', + '_mbox' => 'INBOX', + ]); + } + + /** + * Handle smtp_connect hook: configure SMTP settings for the active account. + * + * Loads SMTP host, port, credentials, and TLS settings from the database + * for the currently selected identity. + * + * @param array $args Hook arguments containing SMTP connection parameters. + * @return array Modified hook arguments with updated SMTP settings. + */ + public function configure_smtp(array $args): array + { + $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null; + if (!is_numeric($iid) || $iid == -1) { + ident_switch::write_log('no identity switch is selected... trying to find related smtp server from the from header'); + $requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST); + if (empty($requestFrom)) { + ident_switch::write_log('no _from post parameter found... falling back to original default config'); + return $args; + } + + $iid = intval($requestFrom); + if ($iid === 0) { + ident_switch::write_log('falling back to original default config as _from post field is no integer: ' . $_POST['_from']); + return $args; + } + } + + $rc = rcmail::get_instance(); + + $sql = 'SELECT smtp_host, flags, smtp_port, username, smtp_auth, password FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $iid, $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + if (is_array($r)) { + if (!$r['username']) { + // Load email from identity + $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; + $q = $rc->db->query($sql, $iid); + $rIid = $rc->db->fetch_assoc($q); + $r['username'] = $rIid['email']; + } + + $args['smtp_user'] = $r['username']; + $args['smtp_pass'] = $r['smtp_auth'] == ident_switch::SMTP_AUTH_IMAP ? $rc->decrypt($r['password']) : ''; + + // In RC 1.6+ smtp_server was renamed to smtp_host and includes port + $smtpHost = $r['smtp_host'] ?: 'localhost'; + + if ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS) { + if (str_contains($smtpHost, ':')) { + ident_switch::write_log('SMTP server already contains protocol, ignoring TLS flag.'); + } else { + $smtpHost = 'tls://' . $smtpHost; + } + } + + $smtpPort = $r['smtp_port'] ?: 587; + $args['smtp_host'] = $smtpHost . ':' . $smtpPort; + } + + return $args; + } + + /** + * Handle preferences_list hook: customize special folders form for remote accounts. + * + * When viewing folder preferences while impersonating, shows the remote account's + * special folder assignments instead of the default ones. + * + * @param array $args Hook arguments containing 'section' and 'blocks' with form data. + * @return array Modified hook arguments with updated folder selections. + */ + public function get_special_folders_form(array $args): array + { + $rc = rcmail::get_instance(); + + if ($args['section'] === 'folders' + && strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0 + ) { + $no_override = array_flip((array)$rc->config->get('dont_override')); + $onchange = "if ($(this).val() == 'INBOX') $(this).val('')"; + $select = $rc->folder_selector([ + 'noselection' => '---', + 'realnames' => true, + 'maxlength' => 30, + 'folder_filter' => 'mail', + 'folder_rights' => 'w', + ]); + + $sql = 'SELECT label FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $_SESSION['iid' . ident_switch::MY_POSTFIX], $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + $args['blocks']['main']['name'] .= ' (' . ($r['label'] ? rcube::Q($rc->gettext('server')) . ': ' . $r['label'] : 'remote') . ')'; + + foreach (rcube_storage::$folder_types as $type) { + if (isset($no_override[$type . '_mbox'])) { + continue; + } + + $defaultKey = $type . '_mbox_default' . ident_switch::MY_POSTFIX; + $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; + $selected = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey] ?? ''; + $attr = ['id' => '_' . $type . '_mbox', 'name' => '_' . $type . '_mbox', 'onchange' => $onchange]; + $args['blocks']['main']['options'][$type . '_mbox']['content'] = $select->show($selected, $attr); + } + } + + return $args; + } + + /** + * Handle preferences_save hook: persist special folder assignments for remote accounts. + * + * Saves folder preferences to the plugin's database table instead of the default + * Roundcube preferences when impersonating a remote account. + * + * @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 save_special_folders(array $args): array + { + $rc = rcmail::get_instance(); + + if ($args['section'] === 'folders' + && strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0 + ) { + $sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $_SESSION['iid' . ident_switch::MY_POSTFIX], $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + if ($r) { + $sql = 'UPDATE ' . + $rc->db->table_name(ident_switch::TABLE) . + ' SET drafts_mbox = ?, sent_mbox = ?, junk_mbox = ?, trash_mbox = ?' . + ' WHERE id = ?'; + + $rc->db->query( + $sql, + $args['prefs']['drafts_mbox'], + $args['prefs']['sent_mbox'], + $args['prefs']['junk_mbox'], + $args['prefs']['trash_mbox'], + $r['id'] + ); + + // Abort to prevent RC from saving prefs to default storage + $args['abort'] = true; + $args['result'] = true; + + foreach (rcube_storage::$folder_types as $type) { + if (!empty($args['prefs'][$type . '_mbox'])) { + $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; + $_SESSION[$otherKey] = $args['prefs'][$type . '_mbox']; + } + } + return $args; + } + + $args['abort'] = true; + $args['result'] = false; + return $args; + } + + foreach (rcube_storage::$folder_types as $type) { + if (!empty($args['prefs'][$type . '_mbox'])) { + $key = $type . '_mbox_default' . ident_switch::MY_POSTFIX; + $_SESSION[$key] = $args['prefs'][$type . '_mbox']; + } + } + return $args; + } +}