change plugin name
Some checks failed
PHP Lint / lint (8.2) (pull_request) Has been cancelled
PHP Lint / lint (8.3) (pull_request) Has been cancelled
PHP Lint / lint (8.4) (pull_request) Has been cancelled
PHP CodeSniffer / phpcs (pull_request) Has been cancelled

This commit is contained in:
Milos Sen
2026-08-08 09:28:20 +03:00
parent 64d2ff3e98
commit 1a5b07273d
2 changed files with 1 additions and 1 deletions

528
s_ident_switch.php Normal file
View File

@@ -0,0 +1,528 @@
<?php
/**
* ident_switch - Roundcube plugin for fast switching between accounts.
*
* Copyright (C) 2016-2022 Boris Gulay
* Copyright (C) 2019 Christian Landvogt
* Copyright (C) 2021 Gergely Papp
* Copyright (C) 2022 Mickael
* Copyright (C) 2026 Gecka
*
* Original code licensed under GPL-3.0+.
* New contributions licensed under AGPL-3.0+.
*
* @url https://github.com/Gecka-apps/ident_switch
*/
require_once __DIR__ . '/lib/IdentSwitchPreconfig.php';
require_once __DIR__ . '/lib/IdentSwitchForm.php';
require_once __DIR__ . '/lib/IdentSwitchSwitcher.php';
require_once __DIR__ . '/lib/IdentSwitchChecker.php';
/**
* Roundcube plugin for fast switching between multiple IMAP accounts.
*
* Allows users to configure and switch between multiple mail accounts
* (including remote) within a single Roundcube session, with alias support.
*
* @url https://github.com/Gecka-apps/ident_switch
*/
class ident_switch extends rcube_plugin
{
/** @var string Task regex: active on all tasks except login/logout. */
public $task = '?(?!login|logout).*';
/** @var string Database table name for this plugin. */
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,
};
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);
}
$primaryIdentity = $rc->user->get_identity();
$primaryName = !empty($primaryIdentity['name']) ? $primaryIdentity['name'] : null;
$accNames = [$_SESSION['global_alias'] ?? $primaryName ?? $rc->user->data['username']];
$accValues = [-1];
$accSelected = -1;
$iidMap = [0 => -1]; // primary account: iid 0 → select value -1
// Get list of alternative accounts
$sql = "SELECT "
. "isw.id, isw.iid, isw.label, isw.username, ii.email"
. " FROM"
. " {$rc->db->table_name(self::TABLE)} isw"
. " INNER JOIN {$rc->db->table_name('identities')} ii ON isw.iid=ii.identity_id"
. " WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.parent_id IS NULL";
$qRec = $rc->db->query($sql, $rc->user->data['user_id'], self::DB_ENABLED);
while ($r = $rc->db->fetch_assoc($qRec)) {
$accValues[] = $r['id'];
$iidMap[$r['iid']] = $r['id'];
if ($iid === (int)$r['iid']) {
$accSelected = $r['id'];
}
$lbl = $r['label'] ?: $r['username'] ?: $r['email'];
$accNames[] = $lbl;
}
if (count($accValues) <= 1) {
return;
}
$this->include_stylesheet('ident_switch.css');
$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;',
'onchange' => 'plugin_switchIdent_switch(this.value);',
]);
$select->add($accNames, $accValues);
$html = '<span id="ident-switch-wrapper" class="ident-switch-wrapper">'
. $select->show([$accSelected])
. '<span id="ident-switch-badge" class="ident-switch-badge" style="display:none"></span>'
. '</span>';
$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);
}
}
}