rollback plugin name 1
This commit is contained in:
528
ident_switch.php
Normal file
528
ident_switch.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user