diff --git a/.phpcs.xml b/.phpcs.xml
new file mode 100644
index 0000000..7147758
--- /dev/null
+++ b/.phpcs.xml
@@ -0,0 +1,47 @@
+
+
+ PSR-12 coding standard for NextBridge RoundCube plugin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ident_switch.php
+ lib/
+
+
+ */vendor/*
+
diff --git a/composer.json b/composer.json
index 981abf5..de448b9 100644
--- a/composer.json
+++ b/composer.json
@@ -38,6 +38,9 @@
"roundcube/plugin-installer": ">=0.3.0",
"ext-ctype": "*"
},
+ "require-dev": {
+ "squizlabs/php_codesniffer": "^3.0"
+ },
"conflict": {
"boressoft/ident_switch": "*",
"elm/identity_smtp": "*",
@@ -51,5 +54,14 @@
"min-version": "1.6",
"sql-dir": "SQL"
}
+ },
+ "scripts": {
+ "phpcs": "phpcs",
+ "phpcbf": "phpcbf"
+ },
+ "config": {
+ "allow-plugins": {
+ "roundcube/plugin-installer": true
+ }
}
}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..9a26441
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,158 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "46b2d3558cb304fde0539d1b85971382",
+ "packages": [
+ {
+ "name": "roundcube/plugin-installer",
+ "version": "0.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/roundcube/plugin-installer.git",
+ "reference": "c4335e20b86cfe3a184ccf24d675c6a0338a372a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/roundcube/plugin-installer/zipball/c4335e20b86cfe3a184ccf24d675c6a0338a372a",
+ "reference": "c4335e20b86cfe3a184ccf24d675c6a0338a372a",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "*"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": [
+ "Roundcube\\Composer\\RoundcubeInstaller"
+ ]
+ },
+ "autoload": {
+ "psr-0": {
+ "Roundcube\\Composer": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0+"
+ ],
+ "authors": [
+ {
+ "name": "Thomas Bruederli",
+ "email": "thomas@roundcube.net"
+ },
+ {
+ "name": "Till Klampaeckel",
+ "email": "till@php.net"
+ },
+ {
+ "name": "Philip Weir",
+ "email": "roundcube@tehinterweb.co.uk"
+ }
+ ],
+ "description": "A composer-installer for Roundcube plugins and skins.",
+ "support": {
+ "issues": "https://github.com/roundcube/plugin-installer/issues",
+ "source": "https://github.com/roundcube/plugin-installer/tree/0.3.2"
+ },
+ "time": "2022-06-24T09:08:18+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.13.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-11-04T16:30:35+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.2",
+ "ext-ctype": "*"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
+}
diff --git a/ident_switch.php b/ident_switch.php
index 882e91f..dce1c37 100644
--- a/ident_switch.php
+++ b/ident_switch.php
@@ -1,4 +1,5 @@
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[] = rcube::Q($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 int $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);
- }
- }
+ /** @var string Task regex: active on all tasks except login/logout. */
+ public $task = '?(?!login|logout).*';
+
+ /** @var string Database table name for this plugin. */
+ public const TABLE = 'ident_switch';
+
+ /** @var string Session variable suffix used to store/restore state. */
+ public const MY_POSTFIX = '_iswitch';
+
+ /** @var int Flag: account switching is enabled. */
+ public const DB_ENABLED = 1;
+
+ /** @var int Legacy flag: use TLS for IMAP. Read-only for backward compat; new records store scheme in host. */
+ public const DB_SECURE_IMAP_TLS = 4;
+
+ /** @var int SMTP authentication: use same credentials as IMAP. */
+ public const SMTP_AUTH_IMAP = 1;
+
+ /** @var int SMTP authentication: no authentication required. */
+ public const SMTP_AUTH_NONE = 2;
+
+ /** @var int SMTP authentication: use custom credentials. */
+ public const SMTP_AUTH_CUSTOM = 3;
+
+ /** @var int Sieve authentication: use same credentials as IMAP. */
+ public const SIEVE_AUTH_IMAP = 1;
+
+ /** @var int Sieve authentication: no authentication required. */
+ public const SIEVE_AUTH_NONE = 2;
+
+ /** @var int Sieve authentication: use custom credentials. */
+ public const SIEVE_AUTH_CUSTOM = 3;
+
+ /** @var int Notification checking: enabled. */
+ public const NOTIFY_CHECK_ENABLED = 1;
+
+ /** @var int Notification checking: disabled. */
+ public const NOTIFY_CHECK_DISABLED = 0;
+
+ private IdentSwitchForm $form;
+ private IdentSwitchSwitcher $switcher;
+ private IdentSwitchPreconfig $preconfig;
+ private IdentSwitchChecker $checker;
+
+ /**
+ * 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->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[] = rcube::Q($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);
+ }
+ }
}
diff --git a/lib/IdentSwitchChecker.php b/lib/IdentSwitchChecker.php
index 0358294..fd3cbba 100644
--- a/lib/IdentSwitchChecker.php
+++ b/lib/IdentSwitchChecker.php
@@ -1,4 +1,5 @@
get_checkable_identities($rc);
+ $identities = $this->get_checkable_identities($rc);
- // Exclude the currently active secondary identity (RC already checks it)
- $activeIid = (int)($_SESSION['iid' . ident_switch::MY_POSTFIX] ?? -1);
- $identities = array_values(array_filter($identities, function ($id) use ($activeIid) {
- return (int)$id['iid'] !== $activeIid;
- }));
+ // Exclude the currently active secondary identity (RC already checks it)
+ $activeIid = (int)($_SESSION['iid' . ident_switch::MY_POSTFIX] ?? -1);
+ $identities = array_values(array_filter($identities, function ($id) use ($activeIid) {
+ return (int)$id['iid'] !== $activeIid;
+ }));
- // When impersonating, also check the primary account
- $isImpersonating = strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0;
- if ($isImpersonating) {
- $primary = $this->get_primary_identity($rc);
- if ($primary) {
- $identities[] = $primary;
- }
- }
+ // When impersonating, also check the primary account
+ $isImpersonating = strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0;
+ if ($isImpersonating) {
+ $primary = $this->get_primary_identity($rc);
+ if ($primary) {
+ $identities[] = $primary;
+ }
+ }
- if (empty($identities)) {
- $this->send_counts($rc);
- return $args;
- }
+ if (empty($identities)) {
+ $this->send_counts($rc);
+ return $args;
+ }
- if ($rc->config->get('ident_switch.round_robin', false)) {
- // Round-robin: check one identity per refresh cycle
- $index = ($_SESSION['ident_switch_check_index'] ?? -1) + 1;
- if ($index >= count($identities)) {
- $index = 0;
- }
- $_SESSION['ident_switch_check_index'] = $index;
- $this->check_identity($rc, $identities[$index]);
- } else {
- foreach ($identities as $identity) {
- $this->check_identity($rc, $identity);
- }
- }
+ if ($rc->config->get('ident_switch.round_robin', false)) {
+ // Round-robin: check one identity per refresh cycle
+ $index = ($_SESSION['ident_switch_check_index'] ?? -1) + 1;
+ if ($index >= count($identities)) {
+ $index = 0;
+ }
+ $_SESSION['ident_switch_check_index'] = $index;
+ $this->check_identity($rc, $identities[$index]);
+ } else {
+ foreach ($identities as $identity) {
+ $this->check_identity($rc, $identity);
+ }
+ }
- $this->send_counts($rc);
+ $this->send_counts($rc);
- return $args;
- }
+ return $args;
+ }
- /**
- * Check a single identity for unseen messages and notify if new mail.
- *
- * @param rcmail $rc Roundcube instance.
- * @param array $identity Identity record from the database.
- */
- private function check_identity(rcmail $rc, array $identity): void
- {
- $counts = $_SESSION['ident_switch_counts'] ?? [];
- $previousCount = $counts[$identity['iid']]['unseen'] ?? 0;
+ /**
+ * Check a single identity for unseen messages and notify if new mail.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @param array $identity Identity record from the database.
+ */
+ private function check_identity(rcmail $rc, array $identity): void
+ {
+ $counts = $_SESSION['ident_switch_counts'] ?? [];
+ $previousCount = $counts[$identity['iid']]['unseen'] ?? 0;
- $count = $this->check_unseen($rc, $identity, $previousCount);
+ $count = $this->check_unseen($rc, $identity, $previousCount);
- ident_switch::write_log("Check identity {$identity['iid']} ({$identity['email']}): unseen={$count}, previous={$previousCount}");
+ ident_switch::write_log("Check identity {$identity['iid']} ({$identity['email']}): unseen={$count}, previous={$previousCount}");
- // Set baseline on first check; preserve it across subsequent checks
- $baseline = $counts[$identity['iid']]['baseline'] ?? $count;
+ // Set baseline on first check; preserve it across subsequent checks
+ $baseline = $counts[$identity['iid']]['baseline'] ?? $count;
- $counts[$identity['iid']] = [
- 'unseen' => $count,
- 'baseline' => $baseline,
- 'checked_at' => time(),
- ];
- $_SESSION['ident_switch_counts'] = $counts;
+ $counts[$identity['iid']] = [
+ 'unseen' => $count,
+ 'baseline' => $baseline,
+ 'checked_at' => time(),
+ ];
+ $_SESSION['ident_switch_counts'] = $counts;
- if ($count > $previousCount) {
- $this->send_notification($rc, $identity, $count);
- }
- }
+ if ($count > $previousCount) {
+ $this->send_notification($rc, $identity, $count);
+ }
+ }
- /**
- * Connect to an identity's IMAP server and return INBOX unseen count.
- *
- * @param rcmail $rc Roundcube instance.
- * @param array $identity Identity DB record.
- * @param int $previousCount Previous unseen count (returned on error).
- * @return int Unseen message count.
- */
- private function check_unseen(rcmail $rc, array $identity, int $previousCount): int
- {
- $imap = new rcube_imap_generic();
+ /**
+ * Connect to an identity's IMAP server and return INBOX unseen count.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @param array $identity Identity DB record.
+ * @param integer $previousCount Previous unseen count (returned on error).
+ * @return integer Unseen message count.
+ */
+ private function check_unseen(rcmail $rc, array $identity, int $previousCount): int
+ {
+ $imap = new rcube_imap_generic();
- $parsed = ident_switch::parse_host_scheme($identity['imap_host'] ?: 'localhost');
- $host = $parsed['host'];
- $ssl = $parsed['scheme'] ?: null;
+ $parsed = ident_switch::parse_host_scheme($identity['imap_host'] ?: 'localhost');
+ $host = $parsed['host'];
+ $ssl = $parsed['scheme'] ?: null;
- if (!$ssl && !empty($identity['flags']) && ($identity['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
- $ssl = 'tls'; // Backward compat: old records without scheme in host
- }
+ if (!$ssl && !empty($identity['flags']) && ($identity['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
+ $ssl = 'tls'; // Backward compat: old records without scheme in host
+ }
- $def_port = ($ssl === 'ssl') ? 993 : 143;
- $port = $identity['imap_port'] ?: $def_port;
+ $def_port = ($ssl === 'ssl') ? 993 : 143;
+ $port = $identity['imap_port'] ?: $def_port;
- $username = $identity['username'] ?: $identity['email'];
- $password = $rc->decrypt($identity['password']);
- if ($password === false) {
- ident_switch::write_log("Failed to decrypt password for identity {$identity['iid']}");
- return $previousCount;
- }
+ $username = $identity['username'] ?: $identity['email'];
+ $password = $rc->decrypt($identity['password']);
+ if ($password === false) {
+ ident_switch::write_log("Failed to decrypt password for identity {$identity['iid']}");
+ return $previousCount;
+ }
- $result = $imap->connect($host, $username, $password, [
- 'port' => $port,
- 'ssl_mode' => $ssl,
- 'timeout' => 5,
- ]);
+ $result = $imap->connect($host, $username, $password, [
+ 'port' => $port,
+ 'ssl_mode' => $ssl,
+ 'timeout' => 5,
+ ]);
- if (!$result) {
- ident_switch::write_log("Failed to check mail for identity {$identity['iid']}: " . $imap->error);
- return $previousCount;
- }
+ if (!$result) {
+ ident_switch::write_log("Failed to check mail for identity {$identity['iid']}: " . $imap->error);
+ return $previousCount;
+ }
- $status = $imap->status('INBOX', ['UNSEEN']);
- $unseen = $status['UNSEEN'] ?? 0;
+ $status = $imap->status('INBOX', ['UNSEEN']);
+ $unseen = $status['UNSEEN'] ?? 0;
- $imap->closeConnection();
+ $imap->closeConnection();
- return $unseen;
- }
+ return $unseen;
+ }
- /**
- * Build a virtual identity record for the primary account.
- *
- * When the user has switched to a secondary account, the primary account's
- * connection details are saved in session with the MY_POSTFIX suffix.
- *
- * @param rcmail $rc Roundcube instance.
- * @return array|null Identity-like array, or null if session data is missing.
- */
- private function get_primary_identity(rcmail $rc): ?array
- {
- $postfix = ident_switch::MY_POSTFIX;
+ /**
+ * Build a virtual identity record for the primary account.
+ *
+ * When the user has switched to a secondary account, the primary account's
+ * connection details are saved in session with the MY_POSTFIX suffix.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @return array|null Identity-like array, or null if session data is missing.
+ */
+ private function get_primary_identity(rcmail $rc): ?array
+ {
+ $postfix = ident_switch::MY_POSTFIX;
- if (!isset($_SESSION['password' . $postfix])) {
- return null;
- }
+ if (!isset($_SESSION['password' . $postfix])) {
+ return null;
+ }
- $host = $_SESSION['storage_host' . $postfix] ?? 'localhost';
- $port = $_SESSION['storage_port' . $postfix] ?? 143;
- $ssl = $_SESSION['storage_ssl' . $postfix] ?? null;
+ $host = $_SESSION['storage_host' . $postfix] ?? 'localhost';
+ $port = $_SESSION['storage_port' . $postfix] ?? 143;
+ $ssl = $_SESSION['storage_ssl' . $postfix] ?? null;
- // Prepend protocol prefix so check_unseen() can parse it
- if ($ssl === 'ssl' && !str_starts_with(strtolower($host), 'ssl://')) {
- $host = 'ssl://' . $host;
- } elseif ($ssl === 'tls' && !str_starts_with(strtolower($host), 'tls://')) {
- $host = 'tls://' . $host;
- }
+ // Prepend protocol prefix so check_unseen() can parse it
+ if ($ssl === 'ssl' && !str_starts_with(strtolower($host), 'ssl://')) {
+ $host = 'ssl://' . $host;
+ } elseif ($ssl === 'tls' && !str_starts_with(strtolower($host), 'tls://')) {
+ $host = 'tls://' . $host;
+ }
- return [
- 'iid' => 0,
- 'imap_host' => $host,
- 'imap_port' => $port,
- 'flags' => 0,
- 'username' => $rc->user->data['username'],
- 'password' => $_SESSION['password' . $postfix],
- 'email' => $rc->user->data['username'],
- 'label' => $_SESSION['global_alias'] ?? $rc->user->data['username'],
- 'notify_basic' => null,
- 'notify_sound' => null,
- 'notify_desktop' => null,
- ];
- }
+ return [
+ 'iid' => 0,
+ 'imap_host' => $host,
+ 'imap_port' => $port,
+ 'flags' => 0,
+ 'username' => $rc->user->data['username'],
+ 'password' => $_SESSION['password' . $postfix],
+ 'email' => $rc->user->data['username'],
+ 'label' => $_SESSION['global_alias'] ?? $rc->user->data['username'],
+ 'notify_basic' => null,
+ 'notify_sound' => null,
+ 'notify_desktop' => null,
+ ];
+ }
- /**
- * Get all enabled identities that have mail checking enabled.
- *
- * @param rcmail $rc Roundcube instance.
- * @return array List of identity records.
- */
- private function get_checkable_identities(rcmail $rc): array
- {
- $sql = 'SELECT isw.iid, isw.imap_host, isw.imap_port, isw.flags, '
- . 'isw.username, isw.password, isw.label, '
- . 'isw.notify_basic, isw.notify_sound, isw.notify_desktop, '
- . 'ii.email '
- . 'FROM ' . $rc->db->table_name(ident_switch::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.notify_check = ? AND isw.parent_id IS NULL';
+ /**
+ * Get all enabled identities that have mail checking enabled.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @return array List of identity records.
+ */
+ private function get_checkable_identities(rcmail $rc): array
+ {
+ $sql = 'SELECT isw.iid, isw.imap_host, isw.imap_port, isw.flags, '
+ . 'isw.username, isw.password, isw.label, '
+ . 'isw.notify_basic, isw.notify_sound, isw.notify_desktop, '
+ . 'ii.email '
+ . 'FROM ' . $rc->db->table_name(ident_switch::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.notify_check = ? AND isw.parent_id IS NULL';
- $q = $rc->db->query($sql, $rc->user->ID, ident_switch::DB_ENABLED, ident_switch::NOTIFY_CHECK_ENABLED);
+ $q = $rc->db->query($sql, $rc->user->ID, ident_switch::DB_ENABLED, ident_switch::NOTIFY_CHECK_ENABLED);
- $identities = [];
- while ($r = $rc->db->fetch_assoc($q)) {
- $identities[] = $r;
- }
+ $identities = [];
+ while ($r = $rc->db->fetch_assoc($q)) {
+ $identities[] = $r;
+ }
- return $identities;
- }
+ return $identities;
+ }
- /**
- * Send all cached unseen counts to client JS.
- *
- * @param rcmail $rc Roundcube instance.
- */
- private function send_counts(rcmail $rc): void
- {
- $counts = $_SESSION['ident_switch_counts'] ?? [];
- $data = [];
- foreach ($counts as $iid => $info) {
- $data[$iid] = [
- 'unseen' => $info['unseen'],
- 'baseline' => $info['baseline'] ?? $info['unseen'],
- ];
- }
+ /**
+ * Send all cached unseen counts to client JS.
+ *
+ * @param rcmail $rc Roundcube instance.
+ */
+ private function send_counts(rcmail $rc): void
+ {
+ $counts = $_SESSION['ident_switch_counts'] ?? [];
+ $data = [];
+ foreach ($counts as $iid => $info) {
+ $data[$iid] = [
+ 'unseen' => $info['unseen'],
+ 'baseline' => $info['baseline'] ?? $info['unseen'],
+ ];
+ }
- $rc->output->command('plugin.ident_switch.update_counts', $data);
- }
+ $rc->output->command('plugin.ident_switch.update_counts', $data);
+ }
- /**
- * Send notification command to client for a specific identity.
- *
- * @param rcmail $rc Roundcube instance.
- * @param array $identity Identity record.
- * @param int $count New unseen count.
- */
- private function send_notification(rcmail $rc, array $identity, int $count): void
- {
- $basic = $identity['notify_basic'] ?? $rc->config->get('newmail_notifier_basic', false);
- $sound = $identity['notify_sound'] ?? $rc->config->get('newmail_notifier_sound', false);
- $desktop = $identity['notify_desktop'] ?? $rc->config->get('newmail_notifier_desktop', false);
+ /**
+ * Send notification command to client for a specific identity.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @param array $identity Identity record.
+ * @param integer $count New unseen count.
+ */
+ private function send_notification(rcmail $rc, array $identity, int $count): void
+ {
+ $basic = $identity['notify_basic'] ?? $rc->config->get('newmail_notifier_basic', false);
+ $sound = $identity['notify_sound'] ?? $rc->config->get('newmail_notifier_sound', false);
+ $desktop = $identity['notify_desktop'] ?? $rc->config->get('newmail_notifier_desktop', false);
- $label = $identity['label'] ?: $identity['email'];
+ $label = $identity['label'] ?: $identity['email'];
- $rc->output->command('plugin.ident_switch.notify', [
- 'iid' => $identity['iid'],
- 'label' => $label,
- 'count' => $count,
- 'basic' => (bool)$basic,
- 'sound' => (bool)$sound,
- 'desktop' => (bool)$desktop,
- ]);
- }
+ $rc->output->command('plugin.ident_switch.notify', [
+ 'iid' => $identity['iid'],
+ 'label' => $label,
+ 'count' => $count,
+ 'basic' => (bool)$basic,
+ 'sound' => (bool)$sound,
+ 'desktop' => (bool)$desktop,
+ ]);
+ }
}
diff --git a/lib/IdentSwitchForm.php b/lib/IdentSwitchForm.php
index 80a8060..427d25d 100644
--- a/lib/IdentSwitchForm.php
+++ b/lib/IdentSwitchForm.php
@@ -1,4 +1,5 @@
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 mode select, label, and readonly.
- */
- public function get_common_fields(array &$record, bool $domainAllowed = true, string $warningHtml = ''): array
- {
- $prefix = 'ident_switch.form.common.';
- $rc = rcmail::get_instance();
-
- // Build mode select: Primary / Alias of X / Separate account
- $modeSelect = new html_select([
- 'name' => "_{$prefix}mode",
- 'onchange' => 'plugin_switchIdent_mode_onChange(this.value);',
- ]);
- $modeSelect->add($this->plugin->gettext('form.common.mode.primary'), 'primary');
-
- // Add available accounts as alias targets
- $accounts = $this->get_available_accounts($rc, $record['identity_id'] ?? null);
- foreach ($accounts as $acc) {
- $label = $acc['label'] ?: $acc['email'];
- $modeSelect->add($label, 'alias:' . $acc['id']);
- }
-
- // Only offer "Separate account" when domain is allowed
- if ($domainAllowed) {
- $modeSelect->add($this->plugin->gettext('form.common.mode.separate'), 'separate');
- }
-
- $currentMode = $record["{$prefix}mode"] ?? 'primary';
- $modeHtml = $modeSelect->show($currentMode)
- . html::span(
- ['class' => 'form-text'],
- rcube::Q($this->plugin->gettext('form.common.mode.hint'))
- )
- . $warningHtml;
-
- return [
- $prefix . 'mode' => ['value' => $modeHtml],
- $prefix . 'readonly' => ['type' => 'hidden'],
- ];
- }
-
- /**
- * Get all separate accounts available as alias targets.
- *
- * @param rcmail $rc Roundcube instance.
- * @param int|null $excludeIid Identity ID to exclude (the identity being edited).
- * @return array List of account records with id, label, username, email.
- */
- private function get_available_accounts(rcmail $rc, ?int $excludeIid): array
- {
- $sql = 'SELECT isw.id, isw.label, isw.username, ii.email'
- . ' FROM ' . $rc->db->table_name(ident_switch::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';
-
- $params = [$rc->user->ID, ident_switch::DB_ENABLED];
-
- if ($excludeIid !== null) {
- $sql .= ' AND isw.iid != ?';
- $params[] = $excludeIid;
- }
-
- $q = $rc->db->query($sql, ...$params);
- $accounts = [];
- while ($r = $rc->db->fetch_assoc($q)) {
- $accounts[] = $r;
- }
- return $accounts;
- }
-
- /**
- * Build the separate account header fields (label).
- *
- * @param array $record Identity record data.
- * @return array Form field definitions for label.
- */
- public function get_separate_fields(array &$record): array
- {
- $prefix = 'ident_switch.form.common.';
-
- $labelInput = new html_inputfield([
- 'name' => "_{$prefix}label",
- 'type' => 'text',
- 'size' => 32,
- 'placeholder' => $record['email'] ?? '',
- ]);
- $labelHtml = $labelInput->show($record["{$prefix}label"] ?? '')
- . html::span(
- ['class' => 'form-text'],
- rcube::Q($this->plugin->gettext('form.common.label.hint'))
- );
-
- return [
- $prefix . 'label' => ['value' => $labelHtml],
- ];
- }
-
- /**
- * 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, security, username, password, delimiter.
- */
- public function get_imap_fields(array &$record): array
- {
- $prefix = 'ident_switch.form.imap.';
- return [
- $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'],
- $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'ssl')],
- $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 993],
- $prefix . 'username' => ['type' => 'text', 'size' => 64, 'placeholder' => $record['email'] ?? ''],
- $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'],
- $prefix . 'delimiter' => ['value' => $this->build_delimiter_field($prefix, $record)],
- ];
- }
-
- /**
- * 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);
- $authType->add($this->plugin->gettext('form.smtp.auth.custom'), ident_switch::SMTP_AUTH_CUSTOM);
-
- // Cast to int: html_select::show() uses strict comparison (===),
- // option values are integers (constants), but POST/DB may return strings.
- $authVal = isset($record[$prefix . 'auth']) ? (int)$record[$prefix . 'auth'] : null;
-
- return [
- $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'],
- $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'tls')],
- $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 587],
- $prefix . 'auth' => ['value' => $authType->show($authVal !== null ? [$authVal] : [])],
- $prefix . 'username' => ['type' => 'text', 'size' => 64, 'autocomplete' => 'off'],
- $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'],
- ];
- }
-
- /**
- * Build the Sieve (managesieve) form fields for identity settings.
- *
- * @param array $record Identity record data used for default auth type selection.
- * @return array Form field definitions for Sieve host, port, and auth type.
- */
- public function get_sieve_fields(array &$record): array
- {
- $prefix = 'ident_switch.form.sieve.';
-
- $authType = new html_select(['name' => "_{$prefix}auth"]);
- $authType->add($this->plugin->gettext('form.sieve.auth.imap'), ident_switch::SIEVE_AUTH_IMAP);
- $authType->add($this->plugin->gettext('form.sieve.auth.none'), ident_switch::SIEVE_AUTH_NONE);
- $authType->add($this->plugin->gettext('form.sieve.auth.custom'), ident_switch::SIEVE_AUTH_CUSTOM);
-
- // Cast to int: html_select::show() uses strict comparison (===)
- $authVal = isset($record[$prefix . 'auth']) ? (int)$record[$prefix . 'auth'] : null;
-
- return [
- $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'],
- $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'tls')],
- $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 4190],
- $prefix . 'auth' => ['value' => $authType->show($authVal !== null ? [$authVal] : [])],
- $prefix . 'username' => ['type' => 'text', 'size' => 64, 'autocomplete' => 'off'],
- $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'],
- ];
- }
-
- /**
- * Build the notification form fields for identity settings.
- *
- * Shows the actual newmail_notifier default value in each tri-state select.
- * Selecting the default option stores NULL so changes to global defaults propagate.
- *
- * @param array $record Identity record data used for default values.
- * @return array Form field definitions for notification preferences.
- */
- public function get_notification_fields(array &$record): array
- {
- $rc = rcmail::get_instance();
- $prefix = 'ident_switch.form.notify.';
-
- $defaultKeys = [
- 'basic' => 'newmail_notifier_basic',
- 'sound' => 'newmail_notifier_sound',
- 'desktop' => 'newmail_notifier_desktop',
- ];
-
- $triState = function (string $name) use ($prefix, &$record, $rc, $defaultKeys): string {
- $defaultVal = $rc->config->get($defaultKeys[$name] ?? '', false);
- $defaultLabel = $defaultVal
- ? $this->plugin->gettext('form.notify.on')
- : $this->plugin->gettext('form.notify.off');
- $defaultLabel .= ' (' . strtolower($this->plugin->gettext('form.notify.default')) . ')';
-
- $select = new html_select(['name' => "_{$prefix}{$name}"]);
- $select->add($defaultLabel, '');
- $select->add($this->plugin->gettext('form.notify.on'), '1');
- $select->add($this->plugin->gettext('form.notify.off'), '0');
- return $select->show($record[$prefix . $name] ?? '');
- };
-
- return [
- $prefix . 'check' => ['type' => 'checkbox'],
- $prefix . 'basic' => ['value' => $triState('basic')],
- $prefix . 'sound' => ['value' => $triState('sound')],
- $prefix . 'desktop' => ['value' => $triState('desktop')],
- ];
- }
-
- /**
- * Build a security dropdown select for a protocol section.
- *
- * @param string $prefix Form field prefix (e.g. 'ident_switch.form.imap.').
- * @param array $record Identity record data.
- * @param string $default Default security value ('', 'tls', 'ssl').
- * @return string Rendered HTML select element.
- */
- private function build_security_select(string $prefix, array &$record, string $default): string
- {
- $select = new html_select(['name' => "_{$prefix}security"]);
- $select->add($this->plugin->gettext('form.security.none'), '');
- $select->add($this->plugin->gettext('form.security.starttls'), 'tls');
- $select->add($this->plugin->gettext('form.security.ssl'), 'ssl');
-
- $current = $record[$prefix . 'security'] ?? $default;
- $proto = str_replace('ident_switch.form.', '', rtrim($prefix, '.'));
- $hidden = $current !== '' ? ' style="display:none"' : '';
- $warning = '
'
- . rcube::Q($this->plugin->gettext('form.security.none_warning'))
- . '
';
-
- return $select->show($current) . $warning;
- }
-
- /**
- * Build the delimiter field with Auto/Manual mode select.
- *
- * @param string $prefix Form field prefix (e.g. 'ident_switch.form.imap.').
- * @param array $record Identity record data.
- * @return string Rendered HTML select + text input.
- */
- private function build_delimiter_field(string $prefix, array &$record): string
- {
- $delimValue = $record[$prefix . 'delimiter'] ?? '';
- $mode = ($delimValue !== '' && $delimValue !== null) ? 'manual' : 'auto';
-
- $select = new html_select(['name' => "_{$prefix}delimiter_mode"]);
- $select->add($this->plugin->gettext('form.imap.delimiter.auto'), 'auto');
- $select->add($this->plugin->gettext('form.imap.delimiter.manual'), 'manual');
-
- $hidden = $mode === 'auto' ? ' style="display:none"' : '';
- $input = new html_inputfield(['name' => "_{$prefix}delimiter", 'size' => 1]);
-
- return $select->show($mode)
- . ' '
- . $input->show($delimValue)
- . '';
- }
-
- /**
- * Check if a domain is allowed for ident_switch configuration.
- *
- * When 'ident_switch.preconfig_only' is enabled, only domains with
- * a matching preconfig entry are allowed.
- *
- * @param string $email Email address to check.
- * @return bool True if allowed, false if blocked by preconfig_only.
- */
- private function is_domain_allowed(string $email): bool
- {
- $rc = rcmail::get_instance();
- if (!$rc->config->get('ident_switch.preconfig_only', false)) {
- return true;
- }
- $preconfig = new IdentSwitchPreconfig($this->plugin);
- return $preconfig->get($email) !== false;
- }
-
- /**
- * Pass preconfig data and settings to JS environment.
- *
- * Parses each domain's protocol URLs into host/security/port components
- * so the client can dynamically populate form fields on email change.
- *
- * @param rcmail $rc Roundcube instance.
- */
- private function pass_preconfig_to_js(rcmail $rc): void
- {
- $this->plugin->load_config();
- $allPreconfig = $rc->config->get('ident_switch.preconfig', []);
- $preconfigOnly = $rc->config->get('ident_switch.preconfig_only', false);
-
- $jsPreconfig = [];
- foreach ($allPreconfig as $domain => $cfg) {
- $entry = [];
-
- $protocols = [
- 'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '',
- 'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '',
- 'sieve' => $cfg['sieve_host'] ?? '',
- ];
-
- foreach ($protocols as $proto => $url) {
- if (empty($url)) {
- continue;
- }
- $urlArr = parse_url($url);
- if (!is_array($urlArr)) {
- continue;
- }
- $scheme = strtolower($urlArr['scheme'] ?? '');
- $entry[$proto] = [
- 'host' => $urlArr['host'] ?? '',
- 'security' => in_array($scheme, ['ssl', 'tls']) ? $scheme : '',
- 'port' => !empty($urlArr['port']) ? intval($urlArr['port']) : '',
- ];
- }
-
- $entry['user'] = $cfg['user'] ?? '';
- $entry['readonly'] = !empty($cfg['readonly']);
- $jsPreconfig[$domain] = $entry;
- }
-
- $rc->output->set_env('ident_switch_preconfig', $jsPreconfig);
- $rc->output->set_env('ident_switch_preconfig_only', $preconfigOnly);
- $rc->output->set_env('ident_switch_warning_tpl', $this->plugin->gettext('form.preconfig_only_warning'));
- }
-
- /**
- * Delegate to ident_switch::parse_host_scheme().
- */
- private static function parse_host_scheme(string $host): array
- {
- return ident_switch::parse_host_scheme($host);
- }
-
- /**
- * Compose a host string with scheme prefix.
- *
- * @param string|null $host Bare host name.
- * @param string|null $security Security type ('', 'tls', 'ssl').
- * @return string|null Host with scheme prefix, or null if host is empty.
- */
- private static function compose_host_scheme(?string $host, ?string $security): ?string
- {
- if ($host === null || $host === '') {
- return $host;
- }
- if ($security === 'ssl' || $security === 'tls') {
- return $security . '://' . $host;
- }
- return $host;
- }
-
- /**
- * Restore plugin form field values from POST data after a save error.
- *
- * When the identity save is aborted (validation or connection error),
- * Roundcube re-renders the form. Core fields are preserved from POST by RC,
- * but plugin fields would revert to DB values without this restoration.
- *
- * @param array &$record Identity record to overlay POST values onto.
- */
- private function restore_post_values(array &$record): void
- {
- // Text and select fields (trimmed)
- $fields = [
- ['common', 'label'],
- ['imap', 'host'],
- ['imap', 'port'],
- ['imap', 'username'],
- ['imap', 'delimiter'],
- ['smtp', 'host'],
- ['smtp', 'port'],
- ['smtp', 'auth'],
- ['smtp', 'username'],
- ['sieve', 'host'],
- ['sieve', 'port'],
- ['sieve', 'auth'],
- ['sieve', 'username'],
- ['notify', 'basic'],
- ['notify', 'sound'],
- ['notify', 'desktop'],
- ];
-
- foreach ($fields as [$section, $field]) {
- $rawVal = self::get_field_value($section, $field, false);
- if ($rawVal !== null) {
- $record["ident_switch.form.{$section}.{$field}"] = self::get_field_value($section, $field);
- }
- }
-
- // Security selects: empty string means "None", must not be trimmed to null
- foreach (['imap', 'smtp', 'sieve'] as $proto) {
- $rawVal = self::get_field_value($proto, 'security', false);
- if ($rawVal !== null) {
- $record["ident_switch.form.{$proto}.security"] = $rawVal;
- }
- }
-
- // Password fields (raw, no trim)
- foreach (['imap', 'smtp', 'sieve'] as $proto) {
- $rawVal = self::get_field_value($proto, 'password', false, true);
- if ($rawVal !== null) {
- $record["ident_switch.form.{$proto}.password"] = $rawVal;
- }
- }
-
- // Mode select
- $modeVal = self::get_field_value('common', 'mode', false);
- if ($modeVal !== null) {
- $record['ident_switch.form.common.mode'] = $modeVal;
- }
-
- // Checkbox: absent from POST means unchecked
- $record['ident_switch.form.notify.check'] = !empty(self::get_field_value('notify', 'check', false));
- }
-
- /**
- * Test IMAP, SMTP, and Sieve connections using validated form data.
- *
- * @param array $data Validated data from validate().
- * @param string $email Email address (fallback username).
- * @param string $imapPass Raw IMAP password (not encrypted).
- * @return string|null Error key on failure, null on success.
- */
- private function test_connections(array $data, string $email, string $imapPass): ?string
- {
- $rc = rcmail::get_instance();
-
- // --- IMAP test ---
- $imapHostFull = $data['imap.host'] ?: 'localhost';
- $parsed = self::parse_host_scheme($imapHostFull);
- $imapHost = $parsed['host'];
- $imapSsl = $parsed['scheme'] ?: null;
- $imapDefPort = ($imapSsl === 'ssl') ? 993 : 143;
- $imapPort = $data['imap.port'] ?: $imapDefPort;
- $imapUser = $data['imap.user'] ?: $email;
-
- $imap = new rcube_imap_generic();
- $result = $imap->connect($imapHost, $imapUser, $imapPass, [
- 'port' => (int)$imapPort,
- 'ssl_mode' => $imapSsl,
- 'timeout' => 10,
- ]);
- if (!$result) {
- ident_switch::write_log("IMAP connection test failed: {$imap->error}");
- return 'imap.connect';
- }
- $imap->closeConnection();
-
- // --- SMTP test ---
- $smtpAuth = (int)($data['smtp.auth'] ?? ident_switch::SMTP_AUTH_IMAP);
- if ($smtpAuth !== ident_switch::SMTP_AUTH_NONE) {
- $smtpHostFull = $data['smtp.host'] ?: 'localhost';
- $smtpParsed = self::parse_host_scheme($smtpHostFull);
- $smtpDefPort = ($smtpParsed['scheme'] === 'ssl') ? 465 : 587;
- $smtpPort = $data['smtp.port'] ?: $smtpDefPort;
- // Compose host with scheme for rcube_smtp (expects ssl://host:port or tls://host:port)
- $smtpConnHost = self::compose_host_scheme($smtpParsed['host'], $smtpParsed['scheme'] ?: null);
- $smtpConnHost .= ':' . $smtpPort;
-
- if ($smtpAuth === ident_switch::SMTP_AUTH_CUSTOM) {
- $smtpUser = $data['smtp.user'] ?: '';
- $smtpPass = $data['smtp.pass'] ?: '';
- } else {
- $smtpUser = $imapUser;
- $smtpPass = $imapPass;
- }
-
- $smtp = new rcube_smtp();
- $result = $smtp->connect($smtpConnHost, null, $smtpUser, $smtpPass);
- if (!$result) {
- ident_switch::write_log("SMTP connection test failed");
- return 'smtp.connect';
- }
- $smtp->disconnect();
- }
-
- // --- Sieve test (only if host is configured and auth is not None) ---
- $sieveAuth = (int)($data['sieve.auth'] ?? ident_switch::SIEVE_AUTH_IMAP);
- $sieveHostFull = $data['sieve.host'] ?? '';
- if (!empty($sieveHostFull) && $sieveAuth !== ident_switch::SIEVE_AUTH_NONE) {
- $sieveParsed = self::parse_host_scheme($sieveHostFull);
- $sievePort = $data['sieve.port'] ?: 4190;
- $useTls = ($sieveParsed['scheme'] === 'tls');
- $sieveHost = $sieveParsed['host'];
- if ($sieveParsed['scheme'] === 'ssl') {
- $sieveHost = 'ssl://' . $sieveHost;
- }
-
- if ($sieveAuth === ident_switch::SIEVE_AUTH_CUSTOM) {
- $sieveUser = $data['sieve.user'] ?: '';
- $sievePass = $data['sieve.pass'] ?: '';
- } else {
- $sieveUser = $imapUser;
- $sievePass = $imapPass;
- }
-
- if (class_exists('rcube_sieve')) {
- $sieve = new rcube_sieve($sieveUser, $sievePass, $sieveHost, (int)$sievePort, null, $useTls);
- if ($sieve->error()) {
- ident_switch::write_log("Sieve connection test failed (error code: {$sieve->error()})");
- return 'sieve.connect';
- }
- }
- }
-
- return 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/Sieve 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');
-
- $preconfigOnly = $rc->config->get('ident_switch.preconfig_only', false);
- $domainAllowed = empty($args['record']['email']) || $this->is_domain_allowed($args['record']['email']);
-
- // Build domain warning HTML (included in mode field)
- $warningHtml = '';
- if ($preconfigOnly) {
- $email = $args['record']['email'] ?? '';
- $domain = '';
- if (!empty($email) && str_contains($email, '@')) {
- $domain = substr($email, strpos($email, '@') + 1);
- }
- $warningHtml = html::div(
- ['class' => 'boxwarning', 'id' => 'ident-switch-domain-warning',
- 'style' => $domainAllowed ? 'display:none' : ''],
- rcube::Q(sprintf($this->plugin->gettext('form.preconfig_only_warning'), $domain))
- );
- }
-
- // Pass preconfig data to JS for dynamic form updates
- $this->pass_preconfig_to_js($rc);
-
- $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'];
-
- // Tell JS whether this identity has an existing ident_switch record
- $rc->output->set_env('ident_switch_has_record', !empty($row));
-
- // 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',
- 'smtp_username' => 'smtp.username',
- 'smtp_password' => 'smtp.password',
- 'sieve_host' => 'sieve.host',
- 'sieve_port' => 'sieve.port',
- 'sieve_auth' => 'sieve.auth',
- 'sieve_username' => 'sieve.username',
- 'sieve_password' => 'sieve.password',
- 'notify_check' => 'notify.check',
- 'notify_basic' => 'notify.basic',
- 'notify_sound' => 'notify.sound',
- 'notify_desktop' => 'notify.desktop',
- ];
- foreach ($row as $k => $v) {
- if (isset($dbToForm[$k])) {
- $record['ident_switch.form.' . $dbToForm[$k]] = $v;
- }
- }
-
- // Replace encrypted passwords with sentinel (shows dots in form, detectable on save)
- foreach (['imap.password', 'smtp.password', 'sieve.password'] as $passKey) {
- $fullKey = 'ident_switch.form.' . $passKey;
- if (!empty($record[$fullKey])) {
- $record[$fullKey] = self::PASSWORD_SENTINEL;
- }
- }
-
- // Determine mode: alias (has parent_id) or separate account
- if ($row['parent_id'] !== null) {
- $record['ident_switch.form.common.mode'] = 'alias:' . $row['parent_id'];
- } elseif ($row['flags'] & ident_switch::DB_ENABLED) {
- $record['ident_switch.form.common.mode'] = 'separate';
- } else {
- $record['ident_switch.form.common.mode'] = 'primary';
- }
-
- // Parse host schemes into separate security fields
- foreach (['imap', 'smtp', 'sieve'] as $proto) {
- $key = "ident_switch.form.{$proto}.host";
- $hostVal = $record[$key] ?? '';
- if ($hostVal !== '') {
- $parsed = self::parse_host_scheme($hostVal);
- $record[$key] = $parsed['host'];
- $record["ident_switch.form.{$proto}.security"] = $parsed['scheme'];
- }
- }
-
- // Backward compat: if IMAP host had no scheme but TLS flag was set
- if (empty($record['ident_switch.form.imap.security']) && ($row['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
- $record['ident_switch.form.imap.security'] = '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;
- }
- // Override delimiter from preconfig (auto-detect if not specified)
- $record['ident_switch.form.imap.delimiter'] = $cfg['delimiter'] ?? null;
- }
- } else {
- $preconfig->apply($record);
- }
-
- // Restore POST values when form is re-displayed after a save error
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $this->restore_post_values($record);
- }
-
- $args['form']['ident_switch.common'] = [
- 'name' => $this->plugin->gettext('form.common.general'),
- 'content' => $this->get_common_fields($record, $domainAllowed, $warningHtml),
- ];
- $args['form']['ident_switch.separate'] = [
- 'name' => $this->plugin->gettext('form.caption'),
- 'content' => $this->get_separate_fields($record),
- ];
- $args['form']['ident_switch.imap'] = [
- 'name' => $this->plugin->gettext('form.imap.caption'),
- 'content' => $this->get_imap_fields($record),
- ];
- $args['form']['ident_switch.smtp'] = [
- 'name' => $this->plugin->gettext('form.smtp.caption'),
- 'content' => $this->get_smtp_fields($record),
- ];
- $args['form']['ident_switch.sieve'] = [
- 'name' => $this->plugin->gettext('form.sieve.caption'),
- 'content' => $this->get_sieve_fields($record),
- ];
- if (!$rc->config->get('ident_switch.check_mail', true)) {
- // Admin disabled background mail checking
- } elseif ($rc->plugins->get_plugin('newmail_notifier')) {
- $args['form']['ident_switch.notify'] = [
- 'name' => $this->plugin->gettext('form.notify.caption'),
- 'content' => $this->get_notification_fields($record),
- ];
- } elseif (!$rc->config->get('ident_switch.hide_notifier_warning', false)) {
- $args['form']['ident_switch.notify'] = [
- 'name' => $this->plugin->gettext('form.notify.caption'),
- 'content' => html::div(
- ['class' => 'boxinformation'],
- rcube::Q($this->plugin->gettext('form.notify.requires_newmail_notifier'))
- ),
- ];
- }
-
- 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;
- }
-
- $mode = self::get_field_value('common', 'mode', false) ?? 'primary';
-
- // Block separate mode for non-preconfigured domains (alias and primary are always allowed)
- if (!$this->is_domain_allowed($args['record']['email']) && $mode === 'separate') {
- $this->disable($args['id']);
- return $args;
- }
-
- if ($mode === 'primary') {
- $this->disable($args['id']);
- return $args;
- }
-
- if (str_starts_with($mode, 'alias:')) {
- $parentId = (int)substr($mode, 6);
- $this->save_alias($args['id'], $parentId);
- return $args;
- }
-
- // mode === 'separate': full validation + save
- $data = $this->validate();
- if (!empty($data['err'])) {
- $this->plugin->add_texts('localization');
- $args['abort'] = true;
- $args['result'] = false;
- $args['message'] = 'ident_switch.err.' . $data['err'];
- return $args;
- }
-
- $this->apply_readonly_preconfig($data, $args['record']['email']);
-
- // Resolve sentinel passwords to actual passwords for connection testing
- $testPass = $this->resolve_password_for_test($rc, $args['id'], $data['imap.pass']);
-
- // Test connections before saving
- $connErr = $this->test_connections($data, $args['record']['email'], $testPass);
- if ($connErr) {
- $this->plugin->add_texts('localization');
- $args['abort'] = true;
- $args['result'] = false;
- $args['message'] = 'ident_switch.err.' . $connErr;
- return $args;
- }
-
- $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;
- }
-
- $mode = self::get_field_value('common', 'mode', false) ?? 'primary';
-
- // Block separate mode for non-preconfigured domains (alias and primary are always allowed)
- if (!$this->is_domain_allowed($args['record']['email']) && $mode === 'separate') {
- return $args;
- }
-
- if ($mode === 'primary') {
- return $args;
- }
-
- if (str_starts_with($mode, 'alias:')) {
- $parentId = (int)substr($mode, 6);
- $_SESSION['createData' . ident_switch::MY_POSTFIX] = [
- 'mode' => 'alias',
- 'parent_id' => $parentId,
- 'label' => self::get_field_value('common', 'label'),
- ];
- return $args;
- }
-
- // mode === 'separate': full validation
- $data = $this->validate();
- if (!empty($data['err'])) {
- $this->plugin->add_texts('localization');
- $args['abort'] = true;
- $args['result'] = false;
- $args['message'] = 'ident_switch.err.' . $data['err'];
- return $args;
- }
-
- $this->apply_readonly_preconfig($data, $args['record']['email']);
-
- // For new identities, sentinel should not appear, but handle defensively
- $testPass = ($data['imap.pass'] === self::PASSWORD_SENTINEL) ? '' : $data['imap.pass'];
-
- // Test connections before saving
- $connErr = $this->test_connections($data, $args['record']['email'], $testPass);
- if ($connErr) {
- $this->plugin->add_texts('localization');
- $args['abort'] = true;
- $args['result'] = false;
- $args['message'] = 'ident_switch.err.' . $connErr;
- return $args;
- }
-
- // 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']}.");
- } elseif (($data['mode'] ?? '') === 'alias') {
- $this->save_alias($args['id'], $data['parent_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 simple preconfig record keys to validated data keys
- $map = [
- 'ident_switch.form.imap.port' => 'imap.port',
- 'ident_switch.form.imap.delimiter' => 'imap.delimiter',
- 'ident_switch.form.imap.username' => 'imap.user',
- 'ident_switch.form.smtp.port' => 'smtp.port',
- 'ident_switch.form.sieve.port' => 'sieve.port',
- ];
-
- foreach ($map as $recordKey => $dataKey) {
- if (empty($data[$dataKey]) && isset($record[$recordKey])) {
- $data[$dataKey] = $record[$recordKey];
- }
- }
-
- // Compose hosts with security scheme (preconfig stores them separately)
- foreach (['imap', 'smtp', 'sieve'] as $proto) {
- $hostKey = "ident_switch.form.{$proto}.host";
- $secKey = "ident_switch.form.{$proto}.security";
- if (empty($data["{$proto}.host"]) && !empty($record[$hostKey])) {
- $security = $record[$secKey] ?? '';
- $data["{$proto}.host"] = self::compose_host_scheme($record[$hostKey], $security);
- }
- }
- }
-
- /**
- * Resolve IMAP password for connection testing.
- *
- * If the password is the sentinel (unchanged), decrypt from DB.
- * Otherwise return the raw password as-is.
- *
- * @param rcmail $rc Roundcube instance.
- * @param int $iid Identity ID.
- * @param string|null $pass Raw password from POST.
- * @return string Decrypted or raw password for testing.
- */
- private function resolve_password_for_test(rcmail $rc, int $iid, ?string $pass): string
- {
- if ($pass !== self::PASSWORD_SENTINEL) {
- return $pass ?? '';
- }
-
- $sql = 'SELECT 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 (!$r || empty($r['password'])) {
- return '';
- }
-
- $decrypted = $rc->decrypt($r['password']);
- return ($decrypted !== false) ? $decrypted : '';
- }
-
- /**
- * 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;
- }
-
- // Validate and compose IMAP host with security scheme
- $imapBareHost = self::get_field_value('imap', 'host');
- $retVal['imap.host'] = $imapBareHost;
- $imapSecurity = self::get_field_value('imap', 'security') ?? '';
- $retVal['imap.host'] = self::compose_host_scheme($retVal['imap.host'], $imapSecurity);
- 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;
- }
-
- $delimMode = self::get_field_value('imap', 'delimiter_mode');
- if ($delimMode === 'manual') {
- $retVal['imap.delimiter'] = self::get_field_value('imap', 'delimiter');
- if (strlen($retVal['imap.delimiter'] ?? '') > 1) {
- $retVal['err'] = 'delim.long';
- return $retVal;
- }
- } else {
- $retVal['imap.delimiter'] = null;
- }
-
- $retVal['imap.user'] = self::get_field_value('imap', 'username');
- if (strlen($retVal['imap.user'] ?? '') > 64) {
- $retVal['err'] = 'user.long';
- return $retVal;
- }
-
- // Validate and compose SMTP host with security scheme (fallback to IMAP host)
- $retVal['smtp.host'] = self::get_field_value('smtp', 'host') ?: $imapBareHost;
- $smtpSecurity = self::get_field_value('smtp', 'security') ?? '';
- $retVal['smtp.host'] = self::compose_host_scheme($retVal['smtp.host'], $smtpSecurity);
- 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') ?? (string)ident_switch::SMTP_AUTH_IMAP;
- if (!ctype_digit($retVal['smtp.auth'])) {
- $retVal['err'] = 'auth.num';
- return $retVal;
- }
-
- // Custom SMTP credentials
- if ((int)$retVal['smtp.auth'] === ident_switch::SMTP_AUTH_CUSTOM) {
- $retVal['smtp.user'] = self::get_field_value('smtp', 'username');
- if (strlen($retVal['smtp.user'] ?? '') > 64) {
- $retVal['err'] = 'user.long';
- return $retVal;
- }
- $retVal['smtp.pass'] = self::get_field_value('smtp', 'password', false, true);
- } else {
- $retVal['smtp.user'] = null;
- $retVal['smtp.pass'] = null;
- }
-
- // Validate and compose Sieve host with security scheme (fallback to IMAP host)
- $retVal['sieve.host'] = self::get_field_value('sieve', 'host') ?: $imapBareHost;
- $sieveSecurity = self::get_field_value('sieve', 'security') ?? '';
- $retVal['sieve.host'] = self::compose_host_scheme($retVal['sieve.host'], $sieveSecurity);
- if (strlen($retVal['sieve.host'] ?? '') > 64) {
- $retVal['err'] = 'host.long';
- return $retVal;
- }
-
- $retVal['sieve.port'] = self::get_field_value('sieve', 'port');
- if ($retVal['sieve.port'] && !ctype_digit($retVal['sieve.port'])) {
- $retVal['err'] = 'port.num';
- return $retVal;
- }
- if ($retVal['sieve.port'] && ($retVal['sieve.port'] <= 0 || $retVal['sieve.port'] > 65535)) {
- $retVal['err'] = 'port.range';
- return $retVal;
- }
-
- $retVal['sieve.auth'] = self::get_field_value('sieve', 'auth') ?? (string)ident_switch::SIEVE_AUTH_IMAP;
- if (!ctype_digit($retVal['sieve.auth'])) {
- $retVal['err'] = 'auth.num';
- return $retVal;
- }
-
- // Custom Sieve credentials
- if ((int)$retVal['sieve.auth'] === ident_switch::SIEVE_AUTH_CUSTOM) {
- $retVal['sieve.user'] = self::get_field_value('sieve', 'username');
- if (strlen($retVal['sieve.user'] ?? '') > 64) {
- $retVal['err'] = 'user.long';
- return $retVal;
- }
- $retVal['sieve.pass'] = self::get_field_value('sieve', 'password', false, true);
- } else {
- $retVal['sieve.user'] = null;
- $retVal['sieve.pass'] = null;
- }
-
- // Notification settings
- $retVal['notify.check'] = self::get_field_value('notify', 'check', false) ? 1 : 0;
-
- $notifyBasic = self::get_field_value('notify', 'basic');
- $retVal['notify.basic'] = ($notifyBasic !== null && $notifyBasic !== '') ? (int)$notifyBasic : null;
-
- $notifySound = self::get_field_value('notify', 'sound');
- $retVal['notify.sound'] = ($notifySound !== null && $notifySound !== '') ? (int)$notifySound : null;
-
- $notifyDesktop = self::get_field_value('notify', 'desktop');
- $retVal['notify.desktop'] = ($notifyDesktop !== null && $notifyDesktop !== '') ? (int)$notifyDesktop : null;
-
- // Get also password
- $retVal['imap.pass'] = self::get_field_value('imap', 'password', false, true);
-
- // Flags: only enabled, security is now in host field scheme
- $retVal['flags'] = ident_switch::DB_ENABLED;
-
- 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, smtp_password, sieve_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 = ?, parent_id = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?,' .
- ' smtp_host = ?, smtp_port = ?, smtp_auth = ?, smtp_username = ?, smtp_password = ?,' .
- ' sieve_host = ?, sieve_port = ?, sieve_auth = ?, sieve_username = ?, sieve_password = ?,' .
- ' notify_check = ?, notify_basic = ?, notify_sound = ?, notify_desktop = ?,' .
- ' 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, parent_id, label, imap_host, imap_port, imap_delimiter, username, password,' .
- ' smtp_host, smtp_port, smtp_auth, smtp_username, smtp_password,' .
- ' sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password,' .
- ' notify_check, notify_basic, notify_sound, notify_desktop,' .
- ' user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
- } else {
- return false;
- }
-
- // Handle IMAP password: sentinel means unchanged, empty means clear, else encrypt
- if ($data['imap.pass'] === self::PASSWORD_SENTINEL) {
- $data['imap.pass'] = $r['password'] ?? null;
- } else {
- $data['imap.pass'] = $data['imap.pass'] ? $rc->encrypt($data['imap.pass']) : null;
- }
-
- // Handle SMTP password
- if ($data['smtp.pass'] === self::PASSWORD_SENTINEL) {
- $data['smtp.pass'] = $r['smtp_password'] ?? null;
- } else {
- $data['smtp.pass'] = $data['smtp.pass'] ? $rc->encrypt($data['smtp.pass']) : null;
- }
-
- // Handle Sieve password
- if ($data['sieve.pass'] === self::PASSWORD_SENTINEL) {
- $data['sieve.pass'] = $r['sieve_password'] ?? null;
- } else {
- $data['sieve.pass'] = $data['sieve.pass'] ? $rc->encrypt($data['sieve.pass']) : null;
- }
-
- $rc->db->query(
- $sql,
- $data['flags'],
- null, // parent_id: NULL for separate accounts
- $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'],
- $data['smtp.user'],
- $data['smtp.pass'],
- $data['sieve.host'],
- $data['sieve.port'],
- $data['sieve.auth'],
- $data['sieve.user'],
- $data['sieve.pass'],
- $data['notify.check'] ?? 1,
- $data['notify.basic'] ?? null,
- $data['notify.sound'] ?? null,
- $data['notify.desktop'] ?? null,
- $rc->user->ID,
- $data['id'],
- $r['id'] ?? null
- );
-
- return true;
- }
-
- /**
- * Save an alias link between an identity and a parent account.
- *
- * Creates or updates an ident_switch record with parent_id set and
- * all server fields cleared (alias uses parent's config).
- *
- * @param int $iid Identity ID of the alias.
- * @param int $parentId ident_switch.id of the parent account.
- */
- private function save_alias(int $iid, int $parentId): void
- {
- $rc = rcmail::get_instance();
-
- // Validate that parent_id exists and belongs to this user
- $sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE)
- . ' WHERE id = ? AND user_id = ? AND parent_id IS NULL';
- $q = $rc->db->query($sql, $parentId, $rc->user->ID);
- if (!$rc->db->fetch_assoc($q)) {
- ident_switch::write_log("Alias save: parent account with id={$parentId} not found for user.");
- return;
- }
-
- $label = self::get_field_value('common', 'label');
-
- // Check if record already exists
- $sql = 'SELECT id 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 ($r) {
- $sql = 'UPDATE ' . $rc->db->table_name(ident_switch::TABLE)
- . ' SET flags = ?, parent_id = ?, label = ?,'
- . ' imap_host = NULL, imap_port = NULL, imap_delimiter = NULL,'
- . ' username = NULL, password = NULL,'
- . ' smtp_host = NULL, smtp_port = NULL, smtp_auth = 1, smtp_username = NULL, smtp_password = NULL,'
- . ' sieve_host = NULL, sieve_port = NULL, sieve_auth = 1, sieve_username = NULL, sieve_password = NULL,'
- . ' notify_check = 0, notify_basic = NULL, notify_sound = NULL, notify_desktop = NULL'
- . ' WHERE id = ?';
- $rc->db->query($sql, ident_switch::DB_ENABLED, $parentId, $label, $r['id']);
- } else {
- $sql = 'INSERT INTO ' . $rc->db->table_name(ident_switch::TABLE)
- . ' (flags, parent_id, label, user_id, iid) VALUES (?, ?, ?, ?, ?)';
- $rc->db->query($sql, ident_switch::DB_ENABLED, $parentId, $label, $rc->user->ID, $iid);
- }
-
- ident_switch::write_log("Saved alias link: identity {$iid} → parent account {$parentId}.");
- }
-
- /**
- * 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);
- }
+ /** @var string Sentinel value placed in password fields to indicate an existing password. */
+ private const PASSWORD_SENTINEL = '__IDENT_SWITCH_UNCHANGED__';
+
+ private ident_switch $plugin;
+
+ /**
+ * Constructor.
+ *
+ * @param ident_switch $plugin Parent plugin instance.
+ */
+ public function __construct(ident_switch $plugin)
+ {
+ $this->plugin = $plugin;
+ }
+
+ /**
+ * Build the common form fields for identity settings.
+ *
+ * @param array $record Identity record data used for placeholders.
+ * @param boolean $domainAllowed Whether the identity domain has a matching preconfig.
+ * @param string $warningHtml HTML warning message when domain is not allowed.
+ * @return array Form field definitions for mode select, label, and readonly.
+ */
+ public function get_common_fields(array &$record, bool $domainAllowed = true, string $warningHtml = ''): array
+ {
+ $prefix = 'ident_switch.form.common.';
+ $rc = rcmail::get_instance();
+
+ // Build mode select: Primary / Alias of X / Separate account
+ $modeSelect = new html_select([
+ 'name' => "_{$prefix}mode",
+ 'onchange' => 'plugin_switchIdent_mode_onChange(this.value);',
+ ]);
+ $modeSelect->add($this->plugin->gettext('form.common.mode.primary'), 'primary');
+
+ // Add available accounts as alias targets
+ $accounts = $this->get_available_accounts($rc, $record['identity_id'] ?? null);
+ foreach ($accounts as $acc) {
+ $label = $acc['label'] ?: $acc['email'];
+ $modeSelect->add($label, 'alias:' . $acc['id']);
+ }
+
+ // Only offer "Separate account" when domain is allowed
+ if ($domainAllowed) {
+ $modeSelect->add($this->plugin->gettext('form.common.mode.separate'), 'separate');
+ }
+
+ $currentMode = $record["{$prefix}mode"] ?? 'primary';
+ $modeHtml = $modeSelect->show($currentMode)
+ . html::span(
+ ['class' => 'form-text'],
+ rcube::Q($this->plugin->gettext('form.common.mode.hint'))
+ )
+ . $warningHtml;
+
+ return [
+ $prefix . 'mode' => ['value' => $modeHtml],
+ $prefix . 'readonly' => ['type' => 'hidden'],
+ ];
+ }
+
+ /**
+ * Get all separate accounts available as alias targets.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @param integer|null $excludeIid Identity ID to exclude (the identity being edited).
+ * @return array List of account records with id, label, username, email.
+ */
+ private function get_available_accounts(rcmail $rc, ?int $excludeIid): array
+ {
+ $sql = 'SELECT isw.id, isw.label, isw.username, ii.email'
+ . ' FROM ' . $rc->db->table_name(ident_switch::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';
+
+ $params = [$rc->user->ID, ident_switch::DB_ENABLED];
+
+ if ($excludeIid !== null) {
+ $sql .= ' AND isw.iid != ?';
+ $params[] = $excludeIid;
+ }
+
+ $q = $rc->db->query($sql, ...$params);
+ $accounts = [];
+ while ($r = $rc->db->fetch_assoc($q)) {
+ $accounts[] = $r;
+ }
+ return $accounts;
+ }
+
+ /**
+ * Build the separate account header fields (label).
+ *
+ * @param array $record Identity record data.
+ * @return array Form field definitions for label.
+ */
+ public function get_separate_fields(array &$record): array
+ {
+ $prefix = 'ident_switch.form.common.';
+
+ $labelInput = new html_inputfield([
+ 'name' => "_{$prefix}label",
+ 'type' => 'text',
+ 'size' => 32,
+ 'placeholder' => $record['email'] ?? '',
+ ]);
+ $labelHtml = $labelInput->show($record["{$prefix}label"] ?? '')
+ . html::span(
+ ['class' => 'form-text'],
+ rcube::Q($this->plugin->gettext('form.common.label.hint'))
+ );
+
+ return [
+ $prefix . 'label' => ['value' => $labelHtml],
+ ];
+ }
+
+ /**
+ * 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, security, username, password, delimiter.
+ */
+ public function get_imap_fields(array &$record): array
+ {
+ $prefix = 'ident_switch.form.imap.';
+ return [
+ $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'],
+ $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'ssl')],
+ $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 993],
+ $prefix . 'username' => ['type' => 'text', 'size' => 64, 'placeholder' => $record['email'] ?? ''],
+ $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'],
+ $prefix . 'delimiter' => ['value' => $this->build_delimiter_field($prefix, $record)],
+ ];
+ }
+
+ /**
+ * 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);
+ $authType->add($this->plugin->gettext('form.smtp.auth.custom'), ident_switch::SMTP_AUTH_CUSTOM);
+
+ // Cast to int: html_select::show() uses strict comparison (===),
+ // option values are integers (constants), but POST/DB may return strings.
+ $authVal = isset($record[$prefix . 'auth']) ? (int)$record[$prefix . 'auth'] : null;
+
+ return [
+ $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'],
+ $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'tls')],
+ $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 587],
+ $prefix . 'auth' => ['value' => $authType->show($authVal !== null ? [$authVal] : [])],
+ $prefix . 'username' => ['type' => 'text', 'size' => 64, 'autocomplete' => 'off'],
+ $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'],
+ ];
+ }
+
+ /**
+ * Build the Sieve (managesieve) form fields for identity settings.
+ *
+ * @param array $record Identity record data used for default auth type selection.
+ * @return array Form field definitions for Sieve host, port, and auth type.
+ */
+ public function get_sieve_fields(array &$record): array
+ {
+ $prefix = 'ident_switch.form.sieve.';
+
+ $authType = new html_select(['name' => "_{$prefix}auth"]);
+ $authType->add($this->plugin->gettext('form.sieve.auth.imap'), ident_switch::SIEVE_AUTH_IMAP);
+ $authType->add($this->plugin->gettext('form.sieve.auth.none'), ident_switch::SIEVE_AUTH_NONE);
+ $authType->add($this->plugin->gettext('form.sieve.auth.custom'), ident_switch::SIEVE_AUTH_CUSTOM);
+
+ // Cast to int: html_select::show() uses strict comparison (===)
+ $authVal = isset($record[$prefix . 'auth']) ? (int)$record[$prefix . 'auth'] : null;
+
+ return [
+ $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'],
+ $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'tls')],
+ $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 4190],
+ $prefix . 'auth' => ['value' => $authType->show($authVal !== null ? [$authVal] : [])],
+ $prefix . 'username' => ['type' => 'text', 'size' => 64, 'autocomplete' => 'off'],
+ $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'],
+ ];
+ }
+
+ /**
+ * Build the notification form fields for identity settings.
+ *
+ * Shows the actual newmail_notifier default value in each tri-state select.
+ * Selecting the default option stores NULL so changes to global defaults propagate.
+ *
+ * @param array $record Identity record data used for default values.
+ * @return array Form field definitions for notification preferences.
+ */
+ public function get_notification_fields(array &$record): array
+ {
+ $rc = rcmail::get_instance();
+ $prefix = 'ident_switch.form.notify.';
+
+ $defaultKeys = [
+ 'basic' => 'newmail_notifier_basic',
+ 'sound' => 'newmail_notifier_sound',
+ 'desktop' => 'newmail_notifier_desktop',
+ ];
+
+ $triState = function (string $name) use ($prefix, &$record, $rc, $defaultKeys): string {
+ $defaultVal = $rc->config->get($defaultKeys[$name] ?? '', false);
+ $defaultLabel = $defaultVal
+ ? $this->plugin->gettext('form.notify.on')
+ : $this->plugin->gettext('form.notify.off');
+ $defaultLabel .= ' (' . strtolower($this->plugin->gettext('form.notify.default')) . ')';
+
+ $select = new html_select(['name' => "_{$prefix}{$name}"]);
+ $select->add($defaultLabel, '');
+ $select->add($this->plugin->gettext('form.notify.on'), '1');
+ $select->add($this->plugin->gettext('form.notify.off'), '0');
+ return $select->show($record[$prefix . $name] ?? '');
+ };
+
+ return [
+ $prefix . 'check' => ['type' => 'checkbox'],
+ $prefix . 'basic' => ['value' => $triState('basic')],
+ $prefix . 'sound' => ['value' => $triState('sound')],
+ $prefix . 'desktop' => ['value' => $triState('desktop')],
+ ];
+ }
+
+ /**
+ * Build a security dropdown select for a protocol section.
+ *
+ * @param string $prefix Form field prefix (e.g. 'ident_switch.form.imap.').
+ * @param array $record Identity record data.
+ * @param string $default Default security value ('', 'tls', 'ssl').
+ * @return string Rendered HTML select element.
+ */
+ private function build_security_select(string $prefix, array &$record, string $default): string
+ {
+ $select = new html_select(['name' => "_{$prefix}security"]);
+ $select->add($this->plugin->gettext('form.security.none'), '');
+ $select->add($this->plugin->gettext('form.security.starttls'), 'tls');
+ $select->add($this->plugin->gettext('form.security.ssl'), 'ssl');
+
+ $current = $record[$prefix . 'security'] ?? $default;
+ $proto = str_replace('ident_switch.form.', '', rtrim($prefix, '.'));
+ $hidden = $current !== '' ? ' style="display:none"' : '';
+ $warning = ''
+ . rcube::Q($this->plugin->gettext('form.security.none_warning'))
+ . '
';
+
+ return $select->show($current) . $warning;
+ }
+
+ /**
+ * Build the delimiter field with Auto/Manual mode select.
+ *
+ * @param string $prefix Form field prefix (e.g. 'ident_switch.form.imap.').
+ * @param array $record Identity record data.
+ * @return string Rendered HTML select + text input.
+ */
+ private function build_delimiter_field(string $prefix, array &$record): string
+ {
+ $delimValue = $record[$prefix . 'delimiter'] ?? '';
+ $mode = ($delimValue !== '' && $delimValue !== null) ? 'manual' : 'auto';
+
+ $select = new html_select(['name' => "_{$prefix}delimiter_mode"]);
+ $select->add($this->plugin->gettext('form.imap.delimiter.auto'), 'auto');
+ $select->add($this->plugin->gettext('form.imap.delimiter.manual'), 'manual');
+
+ $hidden = $mode === 'auto' ? ' style="display:none"' : '';
+ $input = new html_inputfield(['name' => "_{$prefix}delimiter", 'size' => 1]);
+
+ return $select->show($mode)
+ . ' '
+ . $input->show($delimValue)
+ . '';
+ }
+
+ /**
+ * Check if a domain is allowed for ident_switch configuration.
+ *
+ * When 'ident_switch.preconfig_only' is enabled, only domains with
+ * a matching preconfig entry are allowed.
+ *
+ * @param string $email Email address to check.
+ * @return boolean True if allowed, false if blocked by preconfig_only.
+ */
+ private function is_domain_allowed(string $email): bool
+ {
+ $rc = rcmail::get_instance();
+ if (!$rc->config->get('ident_switch.preconfig_only', false)) {
+ return true;
+ }
+ $preconfig = new IdentSwitchPreconfig($this->plugin);
+ return $preconfig->get($email) !== false;
+ }
+
+ /**
+ * Pass preconfig data and settings to JS environment.
+ *
+ * Parses each domain's protocol URLs into host/security/port components
+ * so the client can dynamically populate form fields on email change.
+ *
+ * @param rcmail $rc Roundcube instance.
+ */
+ private function pass_preconfig_to_js(rcmail $rc): void
+ {
+ $this->plugin->load_config();
+ $allPreconfig = $rc->config->get('ident_switch.preconfig', []);
+ $preconfigOnly = $rc->config->get('ident_switch.preconfig_only', false);
+
+ $jsPreconfig = [];
+ foreach ($allPreconfig as $domain => $cfg) {
+ $entry = [];
+
+ $protocols = [
+ 'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '',
+ 'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '',
+ 'sieve' => $cfg['sieve_host'] ?? '',
+ ];
+
+ foreach ($protocols as $proto => $url) {
+ if (empty($url)) {
+ continue;
+ }
+ $urlArr = parse_url($url);
+ if (!is_array($urlArr)) {
+ continue;
+ }
+ $scheme = strtolower($urlArr['scheme'] ?? '');
+ $entry[$proto] = [
+ 'host' => $urlArr['host'] ?? '',
+ 'security' => in_array($scheme, ['ssl', 'tls']) ? $scheme : '',
+ 'port' => !empty($urlArr['port']) ? intval($urlArr['port']) : '',
+ ];
+ }
+
+ $entry['user'] = $cfg['user'] ?? '';
+ $entry['readonly'] = !empty($cfg['readonly']);
+ $jsPreconfig[$domain] = $entry;
+ }
+
+ $rc->output->set_env('ident_switch_preconfig', $jsPreconfig);
+ $rc->output->set_env('ident_switch_preconfig_only', $preconfigOnly);
+ $rc->output->set_env('ident_switch_warning_tpl', $this->plugin->gettext('form.preconfig_only_warning'));
+ }
+
+ /**
+ * Delegate to ident_switch::parse_host_scheme().
+ *
+ * @param string $host Hostname possibly prefixed with ssl:// or tls://.
+ */
+ private static function parse_host_scheme(string $host): array
+ {
+ return ident_switch::parse_host_scheme($host);
+ }
+
+ /**
+ * Compose a host string with scheme prefix.
+ *
+ * @param string|null $host Bare host name.
+ * @param string|null $security Security type ('', 'tls', 'ssl').
+ * @return string|null Host with scheme prefix, or null if host is empty.
+ */
+ private static function compose_host_scheme(?string $host, ?string $security): ?string
+ {
+ if ($host === null || $host === '') {
+ return $host;
+ }
+ if ($security === 'ssl' || $security === 'tls') {
+ return $security . '://' . $host;
+ }
+ return $host;
+ }
+
+ /**
+ * Restore plugin form field values from POST data after a save error.
+ *
+ * When the identity save is aborted (validation or connection error),
+ * Roundcube re-renders the form. Core fields are preserved from POST by RC,
+ * but plugin fields would revert to DB values without this restoration.
+ *
+ * @param array &$record Identity record to overlay POST values onto.
+ */
+ private function restore_post_values(array &$record): void
+ {
+ // Text and select fields (trimmed)
+ $fields = [
+ ['common', 'label'],
+ ['imap', 'host'],
+ ['imap', 'port'],
+ ['imap', 'username'],
+ ['imap', 'delimiter'],
+ ['smtp', 'host'],
+ ['smtp', 'port'],
+ ['smtp', 'auth'],
+ ['smtp', 'username'],
+ ['sieve', 'host'],
+ ['sieve', 'port'],
+ ['sieve', 'auth'],
+ ['sieve', 'username'],
+ ['notify', 'basic'],
+ ['notify', 'sound'],
+ ['notify', 'desktop'],
+ ];
+
+ foreach ($fields as [$section, $field]) {
+ $rawVal = self::get_field_value($section, $field, false);
+ if ($rawVal !== null) {
+ $record["ident_switch.form.{$section}.{$field}"] = self::get_field_value($section, $field);
+ }
+ }
+
+ // Security selects: empty string means "None", must not be trimmed to null
+ foreach (['imap', 'smtp', 'sieve'] as $proto) {
+ $rawVal = self::get_field_value($proto, 'security', false);
+ if ($rawVal !== null) {
+ $record["ident_switch.form.{$proto}.security"] = $rawVal;
+ }
+ }
+
+ // Password fields (raw, no trim)
+ foreach (['imap', 'smtp', 'sieve'] as $proto) {
+ $rawVal = self::get_field_value($proto, 'password', false, true);
+ if ($rawVal !== null) {
+ $record["ident_switch.form.{$proto}.password"] = $rawVal;
+ }
+ }
+
+ // Mode select
+ $modeVal = self::get_field_value('common', 'mode', false);
+ if ($modeVal !== null) {
+ $record['ident_switch.form.common.mode'] = $modeVal;
+ }
+
+ // Checkbox: absent from POST means unchecked
+ $record['ident_switch.form.notify.check'] = !empty(self::get_field_value('notify', 'check', false));
+ }
+
+ /**
+ * Test IMAP, SMTP, and Sieve connections using validated form data.
+ *
+ * @param array $data Validated data from validate().
+ * @param string $email Email address (fallback username).
+ * @param string $imapPass Raw IMAP password (not encrypted).
+ * @return string|null Error key on failure, null on success.
+ */
+ private function test_connections(array $data, string $email, string $imapPass): ?string
+ {
+ $rc = rcmail::get_instance();
+
+ // --- IMAP test ---
+ $imapHostFull = $data['imap.host'] ?: 'localhost';
+ $parsed = self::parse_host_scheme($imapHostFull);
+ $imapHost = $parsed['host'];
+ $imapSsl = $parsed['scheme'] ?: null;
+ $imapDefPort = ($imapSsl === 'ssl') ? 993 : 143;
+ $imapPort = $data['imap.port'] ?: $imapDefPort;
+ $imapUser = $data['imap.user'] ?: $email;
+
+ $imap = new rcube_imap_generic();
+ $result = $imap->connect($imapHost, $imapUser, $imapPass, [
+ 'port' => (int)$imapPort,
+ 'ssl_mode' => $imapSsl,
+ 'timeout' => 10,
+ ]);
+ if (!$result) {
+ ident_switch::write_log("IMAP connection test failed: {$imap->error}");
+ return 'imap.connect';
+ }
+ $imap->closeConnection();
+
+ // --- SMTP test ---
+ $smtpAuth = (int)($data['smtp.auth'] ?? ident_switch::SMTP_AUTH_IMAP);
+ if ($smtpAuth !== ident_switch::SMTP_AUTH_NONE) {
+ $smtpHostFull = $data['smtp.host'] ?: 'localhost';
+ $smtpParsed = self::parse_host_scheme($smtpHostFull);
+ $smtpDefPort = ($smtpParsed['scheme'] === 'ssl') ? 465 : 587;
+ $smtpPort = $data['smtp.port'] ?: $smtpDefPort;
+ // Compose host with scheme for rcube_smtp (expects ssl://host:port or tls://host:port)
+ $smtpConnHost = self::compose_host_scheme($smtpParsed['host'], $smtpParsed['scheme'] ?: null);
+ $smtpConnHost .= ':' . $smtpPort;
+
+ if ($smtpAuth === ident_switch::SMTP_AUTH_CUSTOM) {
+ $smtpUser = $data['smtp.user'] ?: '';
+ $smtpPass = $data['smtp.pass'] ?: '';
+ } else {
+ $smtpUser = $imapUser;
+ $smtpPass = $imapPass;
+ }
+
+ $smtp = new rcube_smtp();
+ $result = $smtp->connect($smtpConnHost, null, $smtpUser, $smtpPass);
+ if (!$result) {
+ ident_switch::write_log("SMTP connection test failed");
+ return 'smtp.connect';
+ }
+ $smtp->disconnect();
+ }
+
+ // --- Sieve test (only if host is configured and auth is not None) ---
+ $sieveAuth = (int)($data['sieve.auth'] ?? ident_switch::SIEVE_AUTH_IMAP);
+ $sieveHostFull = $data['sieve.host'] ?? '';
+ if (!empty($sieveHostFull) && $sieveAuth !== ident_switch::SIEVE_AUTH_NONE) {
+ $sieveParsed = self::parse_host_scheme($sieveHostFull);
+ $sievePort = $data['sieve.port'] ?: 4190;
+ $useTls = ($sieveParsed['scheme'] === 'tls');
+ $sieveHost = $sieveParsed['host'];
+ if ($sieveParsed['scheme'] === 'ssl') {
+ $sieveHost = 'ssl://' . $sieveHost;
+ }
+
+ if ($sieveAuth === ident_switch::SIEVE_AUTH_CUSTOM) {
+ $sieveUser = $data['sieve.user'] ?: '';
+ $sievePass = $data['sieve.pass'] ?: '';
+ } else {
+ $sieveUser = $imapUser;
+ $sievePass = $imapPass;
+ }
+
+ if (class_exists('rcube_sieve')) {
+ $sieve = new rcube_sieve($sieveUser, $sievePass, $sieveHost, (int)$sievePort, null, $useTls);
+ if ($sieve->error()) {
+ ident_switch::write_log("Sieve connection test failed (error code: {$sieve->error()})");
+ return 'sieve.connect';
+ }
+ }
+ }
+
+ return 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/Sieve 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');
+
+ $preconfigOnly = $rc->config->get('ident_switch.preconfig_only', false);
+ $domainAllowed = empty($args['record']['email']) || $this->is_domain_allowed($args['record']['email']);
+
+ // Build domain warning HTML (included in mode field)
+ $warningHtml = '';
+ if ($preconfigOnly) {
+ $email = $args['record']['email'] ?? '';
+ $domain = '';
+ if (!empty($email) && str_contains($email, '@')) {
+ $domain = substr($email, strpos($email, '@') + 1);
+ }
+ $warningHtml = html::div(
+ ['class' => 'boxwarning', 'id' => 'ident-switch-domain-warning',
+ 'style' => $domainAllowed ? 'display:none' : ''],
+ rcube::Q(sprintf($this->plugin->gettext('form.preconfig_only_warning'), $domain))
+ );
+ }
+
+ // Pass preconfig data to JS for dynamic form updates
+ $this->pass_preconfig_to_js($rc);
+
+ $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'];
+
+ // Tell JS whether this identity has an existing ident_switch record
+ $rc->output->set_env('ident_switch_has_record', !empty($row));
+
+ // 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',
+ 'smtp_username' => 'smtp.username',
+ 'smtp_password' => 'smtp.password',
+ 'sieve_host' => 'sieve.host',
+ 'sieve_port' => 'sieve.port',
+ 'sieve_auth' => 'sieve.auth',
+ 'sieve_username' => 'sieve.username',
+ 'sieve_password' => 'sieve.password',
+ 'notify_check' => 'notify.check',
+ 'notify_basic' => 'notify.basic',
+ 'notify_sound' => 'notify.sound',
+ 'notify_desktop' => 'notify.desktop',
+ ];
+ foreach ($row as $k => $v) {
+ if (isset($dbToForm[$k])) {
+ $record['ident_switch.form.' . $dbToForm[$k]] = $v;
+ }
+ }
+
+ // Replace encrypted passwords with sentinel (shows dots in form, detectable on save)
+ foreach (['imap.password', 'smtp.password', 'sieve.password'] as $passKey) {
+ $fullKey = 'ident_switch.form.' . $passKey;
+ if (!empty($record[$fullKey])) {
+ $record[$fullKey] = self::PASSWORD_SENTINEL;
+ }
+ }
+
+ // Determine mode: alias (has parent_id) or separate account
+ if ($row['parent_id'] !== null) {
+ $record['ident_switch.form.common.mode'] = 'alias:' . $row['parent_id'];
+ } elseif ($row['flags'] & ident_switch::DB_ENABLED) {
+ $record['ident_switch.form.common.mode'] = 'separate';
+ } else {
+ $record['ident_switch.form.common.mode'] = 'primary';
+ }
+
+ // Parse host schemes into separate security fields
+ foreach (['imap', 'smtp', 'sieve'] as $proto) {
+ $key = "ident_switch.form.{$proto}.host";
+ $hostVal = $record[$key] ?? '';
+ if ($hostVal !== '') {
+ $parsed = self::parse_host_scheme($hostVal);
+ $record[$key] = $parsed['host'];
+ $record["ident_switch.form.{$proto}.security"] = $parsed['scheme'];
+ }
+ }
+
+ // Backward compat: if IMAP host had no scheme but TLS flag was set
+ if (empty($record['ident_switch.form.imap.security']) && ($row['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
+ $record['ident_switch.form.imap.security'] = '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;
+ }
+ // Override delimiter from preconfig (auto-detect if not specified)
+ $record['ident_switch.form.imap.delimiter'] = $cfg['delimiter'] ?? null;
+ }
+ } else {
+ $preconfig->apply($record);
+ }
+
+ // Restore POST values when form is re-displayed after a save error
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $this->restore_post_values($record);
+ }
+
+ $args['form']['ident_switch.common'] = [
+ 'name' => $this->plugin->gettext('form.common.general'),
+ 'content' => $this->get_common_fields($record, $domainAllowed, $warningHtml),
+ ];
+ $args['form']['ident_switch.separate'] = [
+ 'name' => $this->plugin->gettext('form.caption'),
+ 'content' => $this->get_separate_fields($record),
+ ];
+ $args['form']['ident_switch.imap'] = [
+ 'name' => $this->plugin->gettext('form.imap.caption'),
+ 'content' => $this->get_imap_fields($record),
+ ];
+ $args['form']['ident_switch.smtp'] = [
+ 'name' => $this->plugin->gettext('form.smtp.caption'),
+ 'content' => $this->get_smtp_fields($record),
+ ];
+ $args['form']['ident_switch.sieve'] = [
+ 'name' => $this->plugin->gettext('form.sieve.caption'),
+ 'content' => $this->get_sieve_fields($record),
+ ];
+ if (!$rc->config->get('ident_switch.check_mail', true)) {
+ // Admin disabled background mail checking
+ } elseif ($rc->plugins->get_plugin('newmail_notifier')) {
+ $args['form']['ident_switch.notify'] = [
+ 'name' => $this->plugin->gettext('form.notify.caption'),
+ 'content' => $this->get_notification_fields($record),
+ ];
+ } elseif (!$rc->config->get('ident_switch.hide_notifier_warning', false)) {
+ $args['form']['ident_switch.notify'] = [
+ 'name' => $this->plugin->gettext('form.notify.caption'),
+ 'content' => html::div(
+ ['class' => 'boxinformation'],
+ rcube::Q($this->plugin->gettext('form.notify.requires_newmail_notifier'))
+ ),
+ ];
+ }
+
+ 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;
+ }
+
+ $mode = self::get_field_value('common', 'mode', false) ?? 'primary';
+
+ // Block separate mode for non-preconfigured domains (alias and primary are always allowed)
+ if (!$this->is_domain_allowed($args['record']['email']) && $mode === 'separate') {
+ $this->disable($args['id']);
+ return $args;
+ }
+
+ if ($mode === 'primary') {
+ $this->disable($args['id']);
+ return $args;
+ }
+
+ if (str_starts_with($mode, 'alias:')) {
+ $parentId = (int)substr($mode, 6);
+ $this->save_alias($args['id'], $parentId);
+ return $args;
+ }
+
+ // mode === 'separate': full validation + save
+ $data = $this->validate();
+ if (!empty($data['err'])) {
+ $this->plugin->add_texts('localization');
+ $args['abort'] = true;
+ $args['result'] = false;
+ $args['message'] = 'ident_switch.err.' . $data['err'];
+ return $args;
+ }
+
+ $this->apply_readonly_preconfig($data, $args['record']['email']);
+
+ // Resolve sentinel passwords to actual passwords for connection testing
+ $testPass = $this->resolve_password_for_test($rc, $args['id'], $data['imap.pass']);
+
+ // Test connections before saving
+ $connErr = $this->test_connections($data, $args['record']['email'], $testPass);
+ if ($connErr) {
+ $this->plugin->add_texts('localization');
+ $args['abort'] = true;
+ $args['result'] = false;
+ $args['message'] = 'ident_switch.err.' . $connErr;
+ return $args;
+ }
+
+ $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;
+ }
+
+ $mode = self::get_field_value('common', 'mode', false) ?? 'primary';
+
+ // Block separate mode for non-preconfigured domains (alias and primary are always allowed)
+ if (!$this->is_domain_allowed($args['record']['email']) && $mode === 'separate') {
+ return $args;
+ }
+
+ if ($mode === 'primary') {
+ return $args;
+ }
+
+ if (str_starts_with($mode, 'alias:')) {
+ $parentId = (int)substr($mode, 6);
+ $_SESSION['createData' . ident_switch::MY_POSTFIX] = [
+ 'mode' => 'alias',
+ 'parent_id' => $parentId,
+ 'label' => self::get_field_value('common', 'label'),
+ ];
+ return $args;
+ }
+
+ // mode === 'separate': full validation
+ $data = $this->validate();
+ if (!empty($data['err'])) {
+ $this->plugin->add_texts('localization');
+ $args['abort'] = true;
+ $args['result'] = false;
+ $args['message'] = 'ident_switch.err.' . $data['err'];
+ return $args;
+ }
+
+ $this->apply_readonly_preconfig($data, $args['record']['email']);
+
+ // For new identities, sentinel should not appear, but handle defensively
+ $testPass = ($data['imap.pass'] === self::PASSWORD_SENTINEL) ? '' : $data['imap.pass'];
+
+ // Test connections before saving
+ $connErr = $this->test_connections($data, $args['record']['email'], $testPass);
+ if ($connErr) {
+ $this->plugin->add_texts('localization');
+ $args['abort'] = true;
+ $args['result'] = false;
+ $args['message'] = 'ident_switch.err.' . $connErr;
+ return $args;
+ }
+
+ // 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']}.");
+ } elseif (($data['mode'] ?? '') === 'alias') {
+ $this->save_alias($args['id'], $data['parent_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 simple preconfig record keys to validated data keys
+ $map = [
+ 'ident_switch.form.imap.port' => 'imap.port',
+ 'ident_switch.form.imap.delimiter' => 'imap.delimiter',
+ 'ident_switch.form.imap.username' => 'imap.user',
+ 'ident_switch.form.smtp.port' => 'smtp.port',
+ 'ident_switch.form.sieve.port' => 'sieve.port',
+ ];
+
+ foreach ($map as $recordKey => $dataKey) {
+ if (empty($data[$dataKey]) && isset($record[$recordKey])) {
+ $data[$dataKey] = $record[$recordKey];
+ }
+ }
+
+ // Compose hosts with security scheme (preconfig stores them separately)
+ foreach (['imap', 'smtp', 'sieve'] as $proto) {
+ $hostKey = "ident_switch.form.{$proto}.host";
+ $secKey = "ident_switch.form.{$proto}.security";
+ if (empty($data["{$proto}.host"]) && !empty($record[$hostKey])) {
+ $security = $record[$secKey] ?? '';
+ $data["{$proto}.host"] = self::compose_host_scheme($record[$hostKey], $security);
+ }
+ }
+ }
+
+ /**
+ * Resolve IMAP password for connection testing.
+ *
+ * If the password is the sentinel (unchanged), decrypt from DB.
+ * Otherwise return the raw password as-is.
+ *
+ * @param rcmail $rc Roundcube instance.
+ * @param integer $iid Identity ID.
+ * @param string|null $pass Raw password from POST.
+ * @return string Decrypted or raw password for testing.
+ */
+ private function resolve_password_for_test(rcmail $rc, int $iid, ?string $pass): string
+ {
+ if ($pass !== self::PASSWORD_SENTINEL) {
+ return $pass ?? '';
+ }
+
+ $sql = 'SELECT 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 (!$r || empty($r['password'])) {
+ return '';
+ }
+
+ $decrypted = $rc->decrypt($r['password']);
+ return ($decrypted !== false) ? $decrypted : '';
+ }
+
+ /**
+ * 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;
+ }
+
+ // Validate and compose IMAP host with security scheme
+ $imapBareHost = self::get_field_value('imap', 'host');
+ $retVal['imap.host'] = $imapBareHost;
+ $imapSecurity = self::get_field_value('imap', 'security') ?? '';
+ $retVal['imap.host'] = self::compose_host_scheme($retVal['imap.host'], $imapSecurity);
+ 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;
+ }
+
+ $delimMode = self::get_field_value('imap', 'delimiter_mode');
+ if ($delimMode === 'manual') {
+ $retVal['imap.delimiter'] = self::get_field_value('imap', 'delimiter');
+ if (strlen($retVal['imap.delimiter'] ?? '') > 1) {
+ $retVal['err'] = 'delim.long';
+ return $retVal;
+ }
+ } else {
+ $retVal['imap.delimiter'] = null;
+ }
+
+ $retVal['imap.user'] = self::get_field_value('imap', 'username');
+ if (strlen($retVal['imap.user'] ?? '') > 64) {
+ $retVal['err'] = 'user.long';
+ return $retVal;
+ }
+
+ // Validate and compose SMTP host with security scheme (fallback to IMAP host)
+ $retVal['smtp.host'] = self::get_field_value('smtp', 'host') ?: $imapBareHost;
+ $smtpSecurity = self::get_field_value('smtp', 'security') ?? '';
+ $retVal['smtp.host'] = self::compose_host_scheme($retVal['smtp.host'], $smtpSecurity);
+ 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') ?? (string)ident_switch::SMTP_AUTH_IMAP;
+ if (!ctype_digit($retVal['smtp.auth'])) {
+ $retVal['err'] = 'auth.num';
+ return $retVal;
+ }
+
+ // Custom SMTP credentials
+ if ((int)$retVal['smtp.auth'] === ident_switch::SMTP_AUTH_CUSTOM) {
+ $retVal['smtp.user'] = self::get_field_value('smtp', 'username');
+ if (strlen($retVal['smtp.user'] ?? '') > 64) {
+ $retVal['err'] = 'user.long';
+ return $retVal;
+ }
+ $retVal['smtp.pass'] = self::get_field_value('smtp', 'password', false, true);
+ } else {
+ $retVal['smtp.user'] = null;
+ $retVal['smtp.pass'] = null;
+ }
+
+ // Validate and compose Sieve host with security scheme (fallback to IMAP host)
+ $retVal['sieve.host'] = self::get_field_value('sieve', 'host') ?: $imapBareHost;
+ $sieveSecurity = self::get_field_value('sieve', 'security') ?? '';
+ $retVal['sieve.host'] = self::compose_host_scheme($retVal['sieve.host'], $sieveSecurity);
+ if (strlen($retVal['sieve.host'] ?? '') > 64) {
+ $retVal['err'] = 'host.long';
+ return $retVal;
+ }
+
+ $retVal['sieve.port'] = self::get_field_value('sieve', 'port');
+ if ($retVal['sieve.port'] && !ctype_digit($retVal['sieve.port'])) {
+ $retVal['err'] = 'port.num';
+ return $retVal;
+ }
+ if ($retVal['sieve.port'] && ($retVal['sieve.port'] <= 0 || $retVal['sieve.port'] > 65535)) {
+ $retVal['err'] = 'port.range';
+ return $retVal;
+ }
+
+ $retVal['sieve.auth'] = self::get_field_value('sieve', 'auth') ?? (string)ident_switch::SIEVE_AUTH_IMAP;
+ if (!ctype_digit($retVal['sieve.auth'])) {
+ $retVal['err'] = 'auth.num';
+ return $retVal;
+ }
+
+ // Custom Sieve credentials
+ if ((int)$retVal['sieve.auth'] === ident_switch::SIEVE_AUTH_CUSTOM) {
+ $retVal['sieve.user'] = self::get_field_value('sieve', 'username');
+ if (strlen($retVal['sieve.user'] ?? '') > 64) {
+ $retVal['err'] = 'user.long';
+ return $retVal;
+ }
+ $retVal['sieve.pass'] = self::get_field_value('sieve', 'password', false, true);
+ } else {
+ $retVal['sieve.user'] = null;
+ $retVal['sieve.pass'] = null;
+ }
+
+ // Notification settings
+ $retVal['notify.check'] = self::get_field_value('notify', 'check', false) ? 1 : 0;
+
+ $notifyBasic = self::get_field_value('notify', 'basic');
+ $retVal['notify.basic'] = ($notifyBasic !== null && $notifyBasic !== '') ? (int)$notifyBasic : null;
+
+ $notifySound = self::get_field_value('notify', 'sound');
+ $retVal['notify.sound'] = ($notifySound !== null && $notifySound !== '') ? (int)$notifySound : null;
+
+ $notifyDesktop = self::get_field_value('notify', 'desktop');
+ $retVal['notify.desktop'] = ($notifyDesktop !== null && $notifyDesktop !== '') ? (int)$notifyDesktop : null;
+
+ // Get also password
+ $retVal['imap.pass'] = self::get_field_value('imap', 'password', false, true);
+
+ // Flags: only enabled, security is now in host field scheme
+ $retVal['flags'] = ident_switch::DB_ENABLED;
+
+ 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 boolean $trim Whether to trim and nullify empty values.
+ * @param boolean $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 boolean True if a query was executed, false otherwise.
+ */
+ public function save(rcmail $rc, array $data): bool
+ {
+ $sql = 'SELECT id, password, smtp_password, sieve_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 = ?, parent_id = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?,' .
+ ' smtp_host = ?, smtp_port = ?, smtp_auth = ?, smtp_username = ?, smtp_password = ?,' .
+ ' sieve_host = ?, sieve_port = ?, sieve_auth = ?, sieve_username = ?, sieve_password = ?,' .
+ ' notify_check = ?, notify_basic = ?, notify_sound = ?, notify_desktop = ?,' .
+ ' 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, parent_id, label, imap_host, imap_port, imap_delimiter, username, password,' .
+ ' smtp_host, smtp_port, smtp_auth, smtp_username, smtp_password,' .
+ ' sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password,' .
+ ' notify_check, notify_basic, notify_sound, notify_desktop,' .
+ ' user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+ } else {
+ return false;
+ }
+
+ // Handle IMAP password: sentinel means unchanged, empty means clear, else encrypt
+ if ($data['imap.pass'] === self::PASSWORD_SENTINEL) {
+ $data['imap.pass'] = $r['password'] ?? null;
+ } else {
+ $data['imap.pass'] = $data['imap.pass'] ? $rc->encrypt($data['imap.pass']) : null;
+ }
+
+ // Handle SMTP password
+ if ($data['smtp.pass'] === self::PASSWORD_SENTINEL) {
+ $data['smtp.pass'] = $r['smtp_password'] ?? null;
+ } else {
+ $data['smtp.pass'] = $data['smtp.pass'] ? $rc->encrypt($data['smtp.pass']) : null;
+ }
+
+ // Handle Sieve password
+ if ($data['sieve.pass'] === self::PASSWORD_SENTINEL) {
+ $data['sieve.pass'] = $r['sieve_password'] ?? null;
+ } else {
+ $data['sieve.pass'] = $data['sieve.pass'] ? $rc->encrypt($data['sieve.pass']) : null;
+ }
+
+ $rc->db->query(
+ $sql,
+ $data['flags'],
+ null, // parent_id: NULL for separate accounts
+ $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'],
+ $data['smtp.user'],
+ $data['smtp.pass'],
+ $data['sieve.host'],
+ $data['sieve.port'],
+ $data['sieve.auth'],
+ $data['sieve.user'],
+ $data['sieve.pass'],
+ $data['notify.check'] ?? 1,
+ $data['notify.basic'] ?? null,
+ $data['notify.sound'] ?? null,
+ $data['notify.desktop'] ?? null,
+ $rc->user->ID,
+ $data['id'],
+ $r['id'] ?? null
+ );
+
+ return true;
+ }
+
+ /**
+ * Save an alias link between an identity and a parent account.
+ *
+ * Creates or updates an ident_switch record with parent_id set and
+ * all server fields cleared (alias uses parent's config).
+ *
+ * @param integer $iid Identity ID of the alias.
+ * @param integer $parentId Ident_switch.id of the parent account.
+ */
+ private function save_alias(int $iid, int $parentId): void
+ {
+ $rc = rcmail::get_instance();
+
+ // Validate that parent_id exists and belongs to this user
+ $sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE)
+ . ' WHERE id = ? AND user_id = ? AND parent_id IS NULL';
+ $q = $rc->db->query($sql, $parentId, $rc->user->ID);
+ if (!$rc->db->fetch_assoc($q)) {
+ ident_switch::write_log("Alias save: parent account with id={$parentId} not found for user.");
+ return;
+ }
+
+ $label = self::get_field_value('common', 'label');
+
+ // Check if record already exists
+ $sql = 'SELECT id 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 ($r) {
+ $sql = 'UPDATE ' . $rc->db->table_name(ident_switch::TABLE)
+ . ' SET flags = ?, parent_id = ?, label = ?,'
+ . ' imap_host = NULL, imap_port = NULL, imap_delimiter = NULL,'
+ . ' username = NULL, password = NULL,'
+ . ' smtp_host = NULL, smtp_port = NULL, smtp_auth = 1, smtp_username = NULL, smtp_password = NULL,'
+ . ' sieve_host = NULL, sieve_port = NULL, sieve_auth = 1, sieve_username = NULL, sieve_password = NULL,'
+ . ' notify_check = 0, notify_basic = NULL, notify_sound = NULL, notify_desktop = NULL'
+ . ' WHERE id = ?';
+ $rc->db->query($sql, ident_switch::DB_ENABLED, $parentId, $label, $r['id']);
+ } else {
+ $sql = 'INSERT INTO ' . $rc->db->table_name(ident_switch::TABLE)
+ . ' (flags, parent_id, label, user_id, iid) VALUES (?, ?, ?, ?, ?)';
+ $rc->db->query($sql, ident_switch::DB_ENABLED, $parentId, $label, $rc->user->ID, $iid);
+ }
+
+ ident_switch::write_log("Saved alias link: identity {$iid} → parent account {$parentId}.");
+ }
+
+ /**
+ * Disable the ident_switch flag for a given identity.
+ *
+ * @param integer $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
index 75f37ea..277568b 100644
--- a/lib/IdentSwitchPreconfig.php
+++ b/lib/IdentSwitchPreconfig.php
@@ -1,4 +1,5 @@
plugin = $plugin;
- }
+ /**
+ * Constructor.
+ *
+ * @param ident_switch $plugin Parent plugin instance.
+ */
+ public function __construct(ident_switch $plugin)
+ {
+ $this->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;
- }
+ /**
+ * 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();
+ $this->plugin->load_config();
- $cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []);
- $cfg = $cfg[$dom] ?? $cfg['*'] ?? null;
+ $cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []);
+ $cfg = $cfg[$dom] ?? $cfg['*'] ?? null;
- if ($cfg) {
- if (empty($cfg['imap_host']) && empty($cfg['host'])) {
- return false;
- }
- }
- return $cfg ?: false;
- }
+ if ($cfg) {
+ if (empty($cfg['imap_host']) && empty($cfg['host'])) {
+ return false;
+ }
+ }
+ return $cfg ?: false;
+ }
- /**
- * Apply preconfigured settings to an identity form record.
- *
- * Parses IMAP, SMTP, and Sieve host URLs to extract scheme, host,
- * and port, then sets the username and delimiter based on config values.
- *
- * @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;
- }
+ /**
+ * Apply preconfigured settings to an identity form record.
+ *
+ * Parses IMAP, SMTP, and Sieve host URLs to extract scheme, host,
+ * and port, then sets the username and delimiter based on config values.
+ *
+ * @param array $record Identity record to modify (passed by reference).
+ * @return boolean 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}'.");
+ $cfg = $this->get($email);
+ if (is_array($cfg)) {
+ ident_switch::write_log("Applying predefined configuration for '{$email}'.");
- // Parse each protocol URL into host, security, and port
- $protocols = [
- 'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '',
- 'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '',
- 'sieve' => $cfg['sieve_host'] ?? '',
- ];
+ // Parse each protocol URL into host, security, and port
+ $protocols = [
+ 'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '',
+ 'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '',
+ 'sieve' => $cfg['sieve_host'] ?? '',
+ ];
- foreach ($protocols as $proto => $url) {
- if (empty($url)) {
- continue;
- }
- $urlArr = parse_url($url);
- $host = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : '';
- $scheme = strtolower($urlArr['scheme'] ?? '');
+ foreach ($protocols as $proto => $url) {
+ if (empty($url)) {
+ continue;
+ }
+ $urlArr = parse_url($url);
+ $host = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : '';
+ $scheme = strtolower($urlArr['scheme'] ?? '');
- $record["ident_switch.form.{$proto}.host"] = $host;
- $record["ident_switch.form.{$proto}.security"] = in_array($scheme, ['ssl', 'tls']) ? $scheme : '';
- $record["ident_switch.form.{$proto}.port"] = !empty($urlArr['port']) ? intval($urlArr['port']) : '';
- }
+ $record["ident_switch.form.{$proto}.host"] = $host;
+ $record["ident_switch.form.{$proto}.security"] = in_array($scheme, ['ssl', 'tls']) ? $scheme : '';
+ $record["ident_switch.form.{$proto}.port"] = !empty($urlArr['port']) ? intval($urlArr['port']) : '';
+ }
- $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,
- };
- }
+ $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;
- }
+ if (!empty($cfg['readonly'])) {
+ $record['ident_switch.form.common.readonly'] = $loginSet ? 2 : 1;
+ }
- // IMAP folder hierarchy delimiter (null or absent = auto-detect)
- if (isset($cfg['delimiter'])) {
- $record['ident_switch.form.imap.delimiter'] = $cfg['delimiter'];
- }
+ // IMAP folder hierarchy delimiter (null or absent = auto-detect)
+ if (isset($cfg['delimiter'])) {
+ $record['ident_switch.form.imap.delimiter'] = $cfg['delimiter'];
+ }
- // Notification defaults from preconfig
- if (isset($cfg['notify_check'])) {
- $record['ident_switch.form.notify.check'] = $cfg['notify_check'] ? 1 : 0;
- }
- foreach (['notify_basic', 'notify_sound', 'notify_desktop'] as $key) {
- if (isset($cfg[$key])) {
- $formKey = 'ident_switch.form.notify.' . substr($key, 7);
- $record[$formKey] = $cfg[$key] === null ? '' : ($cfg[$key] ? '1' : '0');
- }
- }
+ // Notification defaults from preconfig
+ if (isset($cfg['notify_check'])) {
+ $record['ident_switch.form.notify.check'] = $cfg['notify_check'] ? 1 : 0;
+ }
+ foreach (['notify_basic', 'notify_sound', 'notify_desktop'] as $key) {
+ if (isset($cfg[$key])) {
+ $formKey = 'ident_switch.form.notify.' . substr($key, 7);
+ $record[$formKey] = $cfg[$key] === null ? '' : ($cfg[$key] ? '1' : '0');
+ }
+ }
- return (bool)($cfg['readonly'] ?? false);
- }
+ return (bool)($cfg['readonly'] ?? false);
+ }
- return false;
- }
+ return false;
+ }
}
diff --git a/lib/IdentSwitchSwitcher.php b/lib/IdentSwitchSwitcher.php
index 362263e..0dca984 100644
--- a/lib/IdentSwitchSwitcher.php
+++ b/lib/IdentSwitchSwitcher.php
@@ -1,4 +1,5 @@
session->remove('folders');
- $rc->session->remove('unseen_count');
+ $rc->session->remove('folders');
+ $rc->session->remove('unseen_count');
- // Reset baseline for the target account so delta goes back to 0
- $this->reset_baseline($identId === -1 ? 0 : null, $rc, $identId);
+ // Reset baseline for the target account so delta goes back to 0
+ $this->reset_baseline($identId === -1 ? 0 : null, $rc, $identId);
- if ($identId === -1) {
- // Switch to main account
- ident_switch::write_log('Switching mailbox back to default.');
+ 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);
- }
- }
+ // 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] ?? null;
- $_SESSION['username'] = $rc->user->data['username'];
- $_SESSION['password'] = $_SESSION['password' . ident_switch::MY_POSTFIX];
- $_SESSION['iid' . ident_switch::MY_POSTFIX] = -1;
+ $_SESSION['imap_delimiter'] = $_SESSION['imap_delimiter' . ident_switch::MY_POSTFIX] ?? null;
+ $_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)) {
- $r['username'] = ident_switch::resolve_username((int)$r['iid'], $r['username']);
+ 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)) {
+ $r['username'] = ident_switch::resolve_username((int)$r['iid'], $r['username']);
- ident_switch::write_log("Switching mailbox to one for identity with ID = {$r['iid']} (username = '{$r['username']}').");
+ 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);
- }
- }
+ 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);
- }
- }
+ 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);
+ }
+ }
- $parsed = ident_switch::parse_host_scheme($r['imap_host'] ?: 'localhost');
- $host = $parsed['host'];
- $ssl = $parsed['scheme'] ?: null;
+ $parsed = ident_switch::parse_host_scheme($r['imap_host'] ?: 'localhost');
+ $host = $parsed['host'];
+ $ssl = $parsed['scheme'] ?: null;
- if (!$ssl && ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
- $ssl = 'tls'; // Backward compat: old records without scheme in host
- }
+ if (!$ssl && ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
+ $ssl = 'tls'; // Backward compat: old records without scheme in host
+ }
- $def_port = ($ssl === 'ssl') ? 993 : 143;
- $port = $r['imap_port'] ?: $def_port;
+ $def_port = ($ssl === 'ssl') ? 993 : 143;
+ $port = $r['imap_port'] ?: $def_port;
- $delimiter = $r['imap_delimiter'] ?: null;
+ $delimiter = $r['imap_delimiter'] ?: null;
- $_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'];
+ $_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;
- }
- }
+ 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',
- ]);
- }
+ $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) || (int)$iid === -1) {
- ident_switch::debug_log('SMTP: no active switch, resolving from _from header');
- $requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
- if (empty($requestFrom)) {
- ident_switch::debug_log('SMTP: no _from parameter, using default config');
- return $args;
- }
+ /**
+ * 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) || (int)$iid === -1) {
+ ident_switch::debug_log('SMTP: no active switch, resolving from _from header');
+ $requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
+ if (empty($requestFrom)) {
+ ident_switch::debug_log('SMTP: no _from parameter, using default config');
+ return $args;
+ }
- $iid = intval($requestFrom);
- if ($iid === 0) {
- ident_switch::debug_log('SMTP: _from is not an integer, using default config');
- return $args;
- }
- }
+ $iid = intval($requestFrom);
+ if ($iid === 0) {
+ ident_switch::debug_log('SMTP: _from is not an integer, using default config');
+ return $args;
+ }
+ }
- $rc = rcmail::get_instance();
+ $rc = rcmail::get_instance();
- $sql = 'SELECT parent_id, smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid 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 this is an alias, follow parent_id to get the parent's SMTP config
- if (!empty($r['parent_id'])) {
- ident_switch::debug_log("SMTP: identity {$iid} is alias, following parent_id={$r['parent_id']}");
- $sql = 'SELECT smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
- $q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
- $r = $rc->db->fetch_assoc($q);
- if (!is_array($r)) {
- ident_switch::debug_log("SMTP: parent account not found, using default config");
- return $args;
- }
- $iid = $r['iid'];
- }
+ $sql = 'SELECT parent_id, smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid 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 this is an alias, follow parent_id to get the parent's SMTP config
+ if (!empty($r['parent_id'])) {
+ ident_switch::debug_log("SMTP: identity {$iid} is alias, following parent_id={$r['parent_id']}");
+ $sql = 'SELECT smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
+ $q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
+ $r = $rc->db->fetch_assoc($q);
+ if (!is_array($r)) {
+ ident_switch::debug_log("SMTP: parent account not found, using default config");
+ return $args;
+ }
+ $iid = $r['iid'];
+ }
- $r['username'] = ident_switch::resolve_username($iid, $r['username']);
+ $r['username'] = ident_switch::resolve_username($iid, $r['username']);
- $authMode = (int)$r['smtp_auth'];
- if ($authMode === ident_switch::SMTP_AUTH_CUSTOM) {
- $args['smtp_user'] = $r['smtp_username'] ?: '';
- $args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : '';
- } elseif ($authMode === ident_switch::SMTP_AUTH_IMAP) {
- $args['smtp_user'] = $r['username'];
- $args['smtp_pass'] = $rc->decrypt($r['password']) ?: '';
- } else {
- $args['smtp_user'] = '';
- $args['smtp_pass'] = '';
- }
+ $authMode = (int)$r['smtp_auth'];
+ if ($authMode === ident_switch::SMTP_AUTH_CUSTOM) {
+ $args['smtp_user'] = $r['smtp_username'] ?: '';
+ $args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : '';
+ } elseif ($authMode === ident_switch::SMTP_AUTH_IMAP) {
+ $args['smtp_user'] = $r['username'];
+ $args['smtp_pass'] = $rc->decrypt($r['password']) ?: '';
+ } else {
+ $args['smtp_user'] = '';
+ $args['smtp_pass'] = '';
+ }
- // Host already contains scheme (ssl:// or tls://) from form
- $smtpHost = $r['smtp_host'] ?: 'localhost';
- $smtpPort = $r['smtp_port'] ?: 587;
- $args['smtp_host'] = $smtpHost . ':' . $smtpPort;
+ // Host already contains scheme (ssl:// or tls://) from form
+ $smtpHost = $r['smtp_host'] ?: 'localhost';
+ $smtpPort = $r['smtp_port'] ?: 587;
+ $args['smtp_host'] = $smtpHost . ':' . $smtpPort;
- $authLabel = match ($authMode) {
- ident_switch::SMTP_AUTH_IMAP => 'imap',
- ident_switch::SMTP_AUTH_NONE => 'none',
- ident_switch::SMTP_AUTH_CUSTOM => 'custom',
- default => "unknown({$authMode})",
- };
- ident_switch::debug_log("SMTP: iid={$iid}, host={$args['smtp_host']}, user={$args['smtp_user']}, auth={$authLabel}");
- }
+ $authLabel = match ($authMode) {
+ ident_switch::SMTP_AUTH_IMAP => 'imap',
+ ident_switch::SMTP_AUTH_NONE => 'none',
+ ident_switch::SMTP_AUTH_CUSTOM => 'custom',
+ default => "unknown({$authMode})",
+ };
+ ident_switch::debug_log("SMTP: iid={$iid}, host={$args['smtp_host']}, user={$args['smtp_user']}, auth={$authLabel}");
+ }
- return $args;
- }
+ return $args;
+ }
- /**
- * Handle managesieve_connect hook: configure Sieve settings for the active account.
- *
- * Loads Sieve host, port, and credentials from the database
- * for the currently selected identity.
- *
- * @param array $args Hook arguments containing Sieve connection parameters.
- * @return array Modified hook arguments with updated Sieve settings.
- */
- public function configure_managesieve(array $args): array
- {
- $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
- if (!is_numeric($iid) || (int)$iid === -1) {
- return $args;
- }
+ /**
+ * Handle managesieve_connect hook: configure Sieve settings for the active account.
+ *
+ * Loads Sieve host, port, and credentials from the database
+ * for the currently selected identity.
+ *
+ * @param array $args Hook arguments containing Sieve connection parameters.
+ * @return array Modified hook arguments with updated Sieve settings.
+ */
+ public function configure_managesieve(array $args): array
+ {
+ $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
+ if (!is_numeric($iid) || (int)$iid === -1) {
+ return $args;
+ }
- $rc = rcmail::get_instance();
+ $rc = rcmail::get_instance();
- $sql = 'SELECT parent_id, sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid 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 this is an alias, follow parent_id to get the parent's Sieve config
- if (!empty($r['parent_id'])) {
- ident_switch::debug_log("Sieve: identity {$iid} is alias, following parent_id={$r['parent_id']}");
- $sql = 'SELECT sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
- $q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
- $r = $rc->db->fetch_assoc($q);
- if (!is_array($r)) {
- ident_switch::debug_log("Sieve: parent account not found, using default config");
- return $args;
- }
- $iid = $r['iid'];
- }
+ $sql = 'SELECT parent_id, sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid 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 this is an alias, follow parent_id to get the parent's Sieve config
+ if (!empty($r['parent_id'])) {
+ ident_switch::debug_log("Sieve: identity {$iid} is alias, following parent_id={$r['parent_id']}");
+ $sql = 'SELECT sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
+ $q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
+ $r = $rc->db->fetch_assoc($q);
+ if (!is_array($r)) {
+ ident_switch::debug_log("Sieve: parent account not found, using default config");
+ return $args;
+ }
+ $iid = $r['iid'];
+ }
- if (empty($r['sieve_host'])) {
- return $args;
- }
+ if (empty($r['sieve_host'])) {
+ return $args;
+ }
- $r['username'] = ident_switch::resolve_username($iid, $r['username']);
+ $r['username'] = ident_switch::resolve_username($iid, $r['username']);
- $sieveHost = $r['sieve_host'];
- $sievePort = $r['sieve_port'] ?: 4190;
- $args['host'] = $sieveHost . ':' . $sievePort;
+ $sieveHost = $r['sieve_host'];
+ $sievePort = $r['sieve_port'] ?: 4190;
+ $args['host'] = $sieveHost . ':' . $sievePort;
- $authMode = (int)$r['sieve_auth'];
- if ($authMode === ident_switch::SIEVE_AUTH_CUSTOM) {
- $args['user'] = $r['sieve_username'] ?: '';
- $args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : '';
- } elseif ($authMode === ident_switch::SIEVE_AUTH_IMAP) {
- $args['user'] = $r['username'];
- $args['password'] = $rc->decrypt($r['password']) ?: '';
- } else {
- $args['user'] = '';
- $args['password'] = '';
- }
+ $authMode = (int)$r['sieve_auth'];
+ if ($authMode === ident_switch::SIEVE_AUTH_CUSTOM) {
+ $args['user'] = $r['sieve_username'] ?: '';
+ $args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : '';
+ } elseif ($authMode === ident_switch::SIEVE_AUTH_IMAP) {
+ $args['user'] = $r['username'];
+ $args['password'] = $rc->decrypt($r['password']) ?: '';
+ } else {
+ $args['user'] = '';
+ $args['password'] = '';
+ }
- ident_switch::debug_log("Sieve: iid={$iid}, host={$args['host']}, user={$args['user']}");
- }
+ ident_switch::debug_log("Sieve: iid={$iid}, host={$args['host']}, user={$args['user']}");
+ }
- return $args;
- }
+ 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();
+ /**
+ * 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',
- ]);
+ 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')) . ': ' . rcube::Q($r['label']) : 'remote') . ')';
+ $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')) . ': ' . rcube::Q($r['label']) : 'remote') . ')';
- foreach (rcube_storage::$folder_types as $type) {
- if (isset($no_override[$type . '_mbox'])) {
- continue;
- }
+ 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);
- }
- }
+ $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;
- }
+ 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();
+ /**
+ * 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 = ?';
+ 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']
- );
+ $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;
+ // 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;
- }
+ 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;
- }
+ $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;
- }
+ 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;
+ }
- /**
- * Reset the baseline for a target account so delta display resets to 0.
- *
- * For primary account (identId=-1), iid is 0.
- * For secondary accounts, look up iid from the ident_switch table.
- *
- * @param int|null $iid Known iid (0 for primary), or null to look up.
- * @param rcmail $rc Roundcube instance.
- * @param mixed $identId The ident_switch.id value for secondary accounts.
- */
- private function reset_baseline(?int $iid, rcmail $rc, mixed $identId): void
- {
- if ($iid === null) {
- // Look up iid from ident_switch table for secondary account
- $sql = 'SELECT 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 (!$r) {
- return;
- }
- $iid = (int)$r['iid'];
- }
+ /**
+ * Reset the baseline for a target account so delta display resets to 0.
+ *
+ * For primary account (identId=-1), iid is 0.
+ * For secondary accounts, look up iid from the ident_switch table.
+ *
+ * @param integer|null $iid Known iid (0 for primary), or null to look up.
+ * @param rcmail $rc Roundcube instance.
+ * @param mixed $identId The ident_switch.id value for secondary accounts.
+ */
+ private function reset_baseline(?int $iid, rcmail $rc, mixed $identId): void
+ {
+ if ($iid === null) {
+ // Look up iid from ident_switch table for secondary account
+ $sql = 'SELECT 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 (!$r) {
+ return;
+ }
+ $iid = (int)$r['iid'];
+ }
- $counts = $_SESSION['ident_switch_counts'] ?? [];
- if (isset($counts[$iid])) {
- unset($counts[$iid]['baseline']);
- $_SESSION['ident_switch_counts'] = $counts;
- }
- }
+ $counts = $_SESSION['ident_switch_counts'] ?? [];
+ if (isset($counts[$iid])) {
+ unset($counts[$iid]['baseline']);
+ $_SESSION['ident_switch_counts'] = $counts;
+ }
+ }
}