PSR-12 coding standards compliance

Add phpcs/phpcbf tooling (composer require-dev, scripts, .phpcs.xml).
Auto-fix tabs to spaces across all PHP files (phpcbf).
Add missing doc comments for classes and constructors.
Add missing @param tags on public/private methods.
This commit is contained in:
Laurent Dinclaux
2026-02-10 22:52:30 +11:00
parent 05a7a2967f
commit b9e16f186d
8 changed files with 2679 additions and 2433 deletions

47
.phpcs.xml Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<ruleset name="NextBridge">
<description>PSR-12 coding standard for NextBridge RoundCube plugin</description>
<!-- Use PSR-12 as base with RoundCube-specific exclusions -->
<rule ref="PSR12">
<!-- Allow longer lines for readability -->
<exclude name="Generic.Files.LineLength"/>
<!-- RoundCube plugins don't use namespaces -->
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
<!-- RoundCube plugins use lowercase class names by convention -->
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/>
<!-- RoundCube plugins can have side effects (require_once) -->
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/>
<!-- RoundCube hooks use snake_case method names by convention -->
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
</rule>
<!-- Allow @url/@link in class doc comments -->
<rule ref="Squiz.Commenting.ClassComment">
<exclude name="Squiz.Commenting.ClassComment.TagNotAllowed"/>
</rule>
<rule ref="Squiz.Commenting.FunctionComment">
<exclude name="Squiz.Commenting.FunctionComment.MissingReturn"/>
</rule>
<!-- Line length: warn at 120, no hard limit -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
<!-- Require class documentation -->
<rule ref="Squiz.Commenting.ClassComment"/>
<!-- Require function documentation with @param and @return -->
<rule ref="Squiz.Commenting.FunctionComment"/>
<!-- Files to check -->
<file>ident_switch.php</file>
<file>lib/</file>
<!-- Exclude vendor directories -->
<exclude-pattern>*/vendor/*</exclude-pattern>
</ruleset>

View File

@@ -38,6 +38,9 @@
"roundcube/plugin-installer": ">=0.3.0", "roundcube/plugin-installer": ">=0.3.0",
"ext-ctype": "*" "ext-ctype": "*"
}, },
"require-dev": {
"squizlabs/php_codesniffer": "^3.0"
},
"conflict": { "conflict": {
"boressoft/ident_switch": "*", "boressoft/ident_switch": "*",
"elm/identity_smtp": "*", "elm/identity_smtp": "*",
@@ -51,5 +54,14 @@
"min-version": "1.6", "min-version": "1.6",
"sql-dir": "SQL" "sql-dir": "SQL"
} }
},
"scripts": {
"phpcs": "phpcs",
"phpcbf": "phpcbf"
},
"config": {
"allow-plugins": {
"roundcube/plugin-installer": true
}
} }
} }

158
composer.lock generated Normal file
View File

@@ -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"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<?php <?php
/** /**
* ident_switch - Background mail checker. * ident_switch - Background mail checker.
* *
@@ -14,250 +15,250 @@
*/ */
class IdentSwitchChecker class IdentSwitchChecker
{ {
/** /**
* Called on each refresh cycle. * Called on each refresh cycle.
* *
* Builds the list of identities to check (excluding the currently active one, * Builds the list of identities to check (excluding the currently active one,
* including the primary account when impersonating), then checks them * including the primary account when impersonating), then checks them
* in round-robin or all-at-once mode. * in round-robin or all-at-once mode.
* *
* @param array $args Hook arguments (empty for refresh hook). * @param array $args Hook arguments (empty for refresh hook).
* @return array Unmodified hook arguments. * @return array Unmodified hook arguments.
*/ */
public function check_new_mail(array $args): array public function check_new_mail(array $args): array
{ {
$rc = rcmail::get_instance(); $rc = rcmail::get_instance();
$identities = $this->get_checkable_identities($rc); $identities = $this->get_checkable_identities($rc);
// Exclude the currently active secondary identity (RC already checks it) // Exclude the currently active secondary identity (RC already checks it)
$activeIid = (int)($_SESSION['iid' . ident_switch::MY_POSTFIX] ?? -1); $activeIid = (int)($_SESSION['iid' . ident_switch::MY_POSTFIX] ?? -1);
$identities = array_values(array_filter($identities, function ($id) use ($activeIid) { $identities = array_values(array_filter($identities, function ($id) use ($activeIid) {
return (int)$id['iid'] !== $activeIid; return (int)$id['iid'] !== $activeIid;
})); }));
// When impersonating, also check the primary account // When impersonating, also check the primary account
$isImpersonating = strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0; $isImpersonating = strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0;
if ($isImpersonating) { if ($isImpersonating) {
$primary = $this->get_primary_identity($rc); $primary = $this->get_primary_identity($rc);
if ($primary) { if ($primary) {
$identities[] = $primary; $identities[] = $primary;
} }
} }
if (empty($identities)) { if (empty($identities)) {
$this->send_counts($rc); $this->send_counts($rc);
return $args; return $args;
} }
if ($rc->config->get('ident_switch.round_robin', false)) { if ($rc->config->get('ident_switch.round_robin', false)) {
// Round-robin: check one identity per refresh cycle // Round-robin: check one identity per refresh cycle
$index = ($_SESSION['ident_switch_check_index'] ?? -1) + 1; $index = ($_SESSION['ident_switch_check_index'] ?? -1) + 1;
if ($index >= count($identities)) { if ($index >= count($identities)) {
$index = 0; $index = 0;
} }
$_SESSION['ident_switch_check_index'] = $index; $_SESSION['ident_switch_check_index'] = $index;
$this->check_identity($rc, $identities[$index]); $this->check_identity($rc, $identities[$index]);
} else { } else {
foreach ($identities as $identity) { foreach ($identities as $identity) {
$this->check_identity($rc, $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. * Check a single identity for unseen messages and notify if new mail.
* *
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
* @param array $identity Identity record from the database. * @param array $identity Identity record from the database.
*/ */
private function check_identity(rcmail $rc, array $identity): void private function check_identity(rcmail $rc, array $identity): void
{ {
$counts = $_SESSION['ident_switch_counts'] ?? []; $counts = $_SESSION['ident_switch_counts'] ?? [];
$previousCount = $counts[$identity['iid']]['unseen'] ?? 0; $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 // Set baseline on first check; preserve it across subsequent checks
$baseline = $counts[$identity['iid']]['baseline'] ?? $count; $baseline = $counts[$identity['iid']]['baseline'] ?? $count;
$counts[$identity['iid']] = [ $counts[$identity['iid']] = [
'unseen' => $count, 'unseen' => $count,
'baseline' => $baseline, 'baseline' => $baseline,
'checked_at' => time(), 'checked_at' => time(),
]; ];
$_SESSION['ident_switch_counts'] = $counts; $_SESSION['ident_switch_counts'] = $counts;
if ($count > $previousCount) { if ($count > $previousCount) {
$this->send_notification($rc, $identity, $count); $this->send_notification($rc, $identity, $count);
} }
} }
/** /**
* Connect to an identity's IMAP server and return INBOX unseen count. * Connect to an identity's IMAP server and return INBOX unseen count.
* *
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
* @param array $identity Identity DB record. * @param array $identity Identity DB record.
* @param int $previousCount Previous unseen count (returned on error). * @param integer $previousCount Previous unseen count (returned on error).
* @return int Unseen message count. * @return integer Unseen message count.
*/ */
private function check_unseen(rcmail $rc, array $identity, int $previousCount): int private function check_unseen(rcmail $rc, array $identity, int $previousCount): int
{ {
$imap = new rcube_imap_generic(); $imap = new rcube_imap_generic();
$parsed = ident_switch::parse_host_scheme($identity['imap_host'] ?: 'localhost'); $parsed = ident_switch::parse_host_scheme($identity['imap_host'] ?: 'localhost');
$host = $parsed['host']; $host = $parsed['host'];
$ssl = $parsed['scheme'] ?: null; $ssl = $parsed['scheme'] ?: null;
if (!$ssl && !empty($identity['flags']) && ($identity['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) { if (!$ssl && !empty($identity['flags']) && ($identity['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
$ssl = 'tls'; // Backward compat: old records without scheme in host $ssl = 'tls'; // Backward compat: old records without scheme in host
} }
$def_port = ($ssl === 'ssl') ? 993 : 143; $def_port = ($ssl === 'ssl') ? 993 : 143;
$port = $identity['imap_port'] ?: $def_port; $port = $identity['imap_port'] ?: $def_port;
$username = $identity['username'] ?: $identity['email']; $username = $identity['username'] ?: $identity['email'];
$password = $rc->decrypt($identity['password']); $password = $rc->decrypt($identity['password']);
if ($password === false) { if ($password === false) {
ident_switch::write_log("Failed to decrypt password for identity {$identity['iid']}"); ident_switch::write_log("Failed to decrypt password for identity {$identity['iid']}");
return $previousCount; return $previousCount;
} }
$result = $imap->connect($host, $username, $password, [ $result = $imap->connect($host, $username, $password, [
'port' => $port, 'port' => $port,
'ssl_mode' => $ssl, 'ssl_mode' => $ssl,
'timeout' => 5, 'timeout' => 5,
]); ]);
if (!$result) { if (!$result) {
ident_switch::write_log("Failed to check mail for identity {$identity['iid']}: " . $imap->error); ident_switch::write_log("Failed to check mail for identity {$identity['iid']}: " . $imap->error);
return $previousCount; return $previousCount;
} }
$status = $imap->status('INBOX', ['UNSEEN']); $status = $imap->status('INBOX', ['UNSEEN']);
$unseen = $status['UNSEEN'] ?? 0; $unseen = $status['UNSEEN'] ?? 0;
$imap->closeConnection(); $imap->closeConnection();
return $unseen; return $unseen;
} }
/** /**
* Build a virtual identity record for the primary account. * Build a virtual identity record for the primary account.
* *
* When the user has switched to a secondary account, the primary account's * When the user has switched to a secondary account, the primary account's
* connection details are saved in session with the MY_POSTFIX suffix. * connection details are saved in session with the MY_POSTFIX suffix.
* *
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
* @return array|null Identity-like array, or null if session data is missing. * @return array|null Identity-like array, or null if session data is missing.
*/ */
private function get_primary_identity(rcmail $rc): ?array private function get_primary_identity(rcmail $rc): ?array
{ {
$postfix = ident_switch::MY_POSTFIX; $postfix = ident_switch::MY_POSTFIX;
if (!isset($_SESSION['password' . $postfix])) { if (!isset($_SESSION['password' . $postfix])) {
return null; return null;
} }
$host = $_SESSION['storage_host' . $postfix] ?? 'localhost'; $host = $_SESSION['storage_host' . $postfix] ?? 'localhost';
$port = $_SESSION['storage_port' . $postfix] ?? 143; $port = $_SESSION['storage_port' . $postfix] ?? 143;
$ssl = $_SESSION['storage_ssl' . $postfix] ?? null; $ssl = $_SESSION['storage_ssl' . $postfix] ?? null;
// Prepend protocol prefix so check_unseen() can parse it // Prepend protocol prefix so check_unseen() can parse it
if ($ssl === 'ssl' && !str_starts_with(strtolower($host), 'ssl://')) { if ($ssl === 'ssl' && !str_starts_with(strtolower($host), 'ssl://')) {
$host = 'ssl://' . $host; $host = 'ssl://' . $host;
} elseif ($ssl === 'tls' && !str_starts_with(strtolower($host), 'tls://')) { } elseif ($ssl === 'tls' && !str_starts_with(strtolower($host), 'tls://')) {
$host = 'tls://' . $host; $host = 'tls://' . $host;
} }
return [ return [
'iid' => 0, 'iid' => 0,
'imap_host' => $host, 'imap_host' => $host,
'imap_port' => $port, 'imap_port' => $port,
'flags' => 0, 'flags' => 0,
'username' => $rc->user->data['username'], 'username' => $rc->user->data['username'],
'password' => $_SESSION['password' . $postfix], 'password' => $_SESSION['password' . $postfix],
'email' => $rc->user->data['username'], 'email' => $rc->user->data['username'],
'label' => $_SESSION['global_alias'] ?? $rc->user->data['username'], 'label' => $_SESSION['global_alias'] ?? $rc->user->data['username'],
'notify_basic' => null, 'notify_basic' => null,
'notify_sound' => null, 'notify_sound' => null,
'notify_desktop' => null, 'notify_desktop' => null,
]; ];
} }
/** /**
* Get all enabled identities that have mail checking enabled. * Get all enabled identities that have mail checking enabled.
* *
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
* @return array List of identity records. * @return array List of identity records.
*/ */
private function get_checkable_identities(rcmail $rc): array private function get_checkable_identities(rcmail $rc): array
{ {
$sql = 'SELECT isw.iid, isw.imap_host, isw.imap_port, isw.flags, ' $sql = 'SELECT isw.iid, isw.imap_host, isw.imap_port, isw.flags, '
. 'isw.username, isw.password, isw.label, ' . 'isw.username, isw.password, isw.label, '
. 'isw.notify_basic, isw.notify_sound, isw.notify_desktop, ' . 'isw.notify_basic, isw.notify_sound, isw.notify_desktop, '
. 'ii.email ' . 'ii.email '
. 'FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' isw ' . 'FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' isw '
. 'INNER JOIN ' . $rc->db->table_name('identities') . ' ii ON isw.iid = ii.identity_id ' . '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'; . '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 = []; $identities = [];
while ($r = $rc->db->fetch_assoc($q)) { while ($r = $rc->db->fetch_assoc($q)) {
$identities[] = $r; $identities[] = $r;
} }
return $identities; return $identities;
} }
/** /**
* Send all cached unseen counts to client JS. * Send all cached unseen counts to client JS.
* *
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
*/ */
private function send_counts(rcmail $rc): void private function send_counts(rcmail $rc): void
{ {
$counts = $_SESSION['ident_switch_counts'] ?? []; $counts = $_SESSION['ident_switch_counts'] ?? [];
$data = []; $data = [];
foreach ($counts as $iid => $info) { foreach ($counts as $iid => $info) {
$data[$iid] = [ $data[$iid] = [
'unseen' => $info['unseen'], 'unseen' => $info['unseen'],
'baseline' => $info['baseline'] ?? $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. * Send notification command to client for a specific identity.
* *
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
* @param array $identity Identity record. * @param array $identity Identity record.
* @param int $count New unseen count. * @param integer $count New unseen count.
*/ */
private function send_notification(rcmail $rc, array $identity, int $count): void private function send_notification(rcmail $rc, array $identity, int $count): void
{ {
$basic = $identity['notify_basic'] ?? $rc->config->get('newmail_notifier_basic', false); $basic = $identity['notify_basic'] ?? $rc->config->get('newmail_notifier_basic', false);
$sound = $identity['notify_sound'] ?? $rc->config->get('newmail_notifier_sound', false); $sound = $identity['notify_sound'] ?? $rc->config->get('newmail_notifier_sound', false);
$desktop = $identity['notify_desktop'] ?? $rc->config->get('newmail_notifier_desktop', 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', [ $rc->output->command('plugin.ident_switch.notify', [
'iid' => $identity['iid'], 'iid' => $identity['iid'],
'label' => $label, 'label' => $label,
'count' => $count, 'count' => $count,
'basic' => (bool)$basic, 'basic' => (bool)$basic,
'sound' => (bool)$sound, 'sound' => (bool)$sound,
'desktop' => (bool)$desktop, 'desktop' => (bool)$desktop,
]); ]);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<?php <?php
/** /**
* ident_switch - Preconfiguration handler. * ident_switch - Preconfiguration handler.
* *
@@ -15,111 +16,116 @@
*/ */
class IdentSwitchPreconfig class IdentSwitchPreconfig
{ {
private ident_switch $plugin; private ident_switch $plugin;
public function __construct(ident_switch $plugin) /**
{ * Constructor.
$this->plugin = $plugin; *
} * @param ident_switch $plugin Parent plugin instance.
*/
public function __construct(ident_switch $plugin)
{
$this->plugin = $plugin;
}
/** /**
* Load preconfigured settings for a domain from config. * Load preconfigured settings for a domain from config.
* *
* @param string $email Email address to extract domain from. * @param string $email Email address to extract domain from.
* @return array|false Preconfig array for the domain, or false if not found. * @return array|false Preconfig array for the domain, or false if not found.
*/ */
public function get(string $email): array|false public function get(string $email): array|false
{ {
$dom = substr(strstr($email, '@'), 1); $dom = substr(strstr($email, '@'), 1);
if (!$dom) { if (!$dom) {
return false; return false;
} }
$this->plugin->load_config(); $this->plugin->load_config();
$cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []); $cfg = rcmail::get_instance()->config->get('ident_switch.preconfig', []);
$cfg = $cfg[$dom] ?? $cfg['*'] ?? null; $cfg = $cfg[$dom] ?? $cfg['*'] ?? null;
if ($cfg) { if ($cfg) {
if (empty($cfg['imap_host']) && empty($cfg['host'])) { if (empty($cfg['imap_host']) && empty($cfg['host'])) {
return false; return false;
} }
} }
return $cfg ?: false; return $cfg ?: false;
} }
/** /**
* Apply preconfigured settings to an identity form record. * Apply preconfigured settings to an identity form record.
* *
* Parses IMAP, SMTP, and Sieve host URLs to extract scheme, host, * Parses IMAP, SMTP, and Sieve host URLs to extract scheme, host,
* and port, then sets the username and delimiter based on config values. * and port, then sets the username and delimiter based on config values.
* *
* @param array $record Identity record to modify (passed by reference). * @param array $record Identity record to modify (passed by reference).
* @return bool True if the preconfig is readonly, false otherwise. * @return boolean True if the preconfig is readonly, false otherwise.
*/ */
public function apply(array &$record): bool public function apply(array &$record): bool
{ {
$email = $record['email'] ?? ''; $email = $record['email'] ?? '';
if (empty($email)) { if (empty($email)) {
return false; return false;
} }
$cfg = $this->get($email); $cfg = $this->get($email);
if (is_array($cfg)) { if (is_array($cfg)) {
ident_switch::write_log("Applying predefined configuration for '{$email}'."); ident_switch::write_log("Applying predefined configuration for '{$email}'.");
// Parse each protocol URL into host, security, and port // Parse each protocol URL into host, security, and port
$protocols = [ $protocols = [
'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '', 'imap' => $cfg['imap_host'] ?? $cfg['host'] ?? '',
'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '', 'smtp' => $cfg['smtp_host'] ?? $cfg['host'] ?? '',
'sieve' => $cfg['sieve_host'] ?? '', 'sieve' => $cfg['sieve_host'] ?? '',
]; ];
foreach ($protocols as $proto => $url) { foreach ($protocols as $proto => $url) {
if (empty($url)) { if (empty($url)) {
continue; continue;
} }
$urlArr = parse_url($url); $urlArr = parse_url($url);
$host = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : ''; $host = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : '';
$scheme = strtolower($urlArr['scheme'] ?? ''); $scheme = strtolower($urlArr['scheme'] ?? '');
$record["ident_switch.form.{$proto}.host"] = $host; $record["ident_switch.form.{$proto}.host"] = $host;
$record["ident_switch.form.{$proto}.security"] = in_array($scheme, ['ssl', 'tls']) ? $scheme : ''; $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}.port"] = !empty($urlArr['port']) ? intval($urlArr['port']) : '';
} }
$loginSet = false; $loginSet = false;
if (!empty($cfg['user'])) { if (!empty($cfg['user'])) {
match (strtoupper($cfg['user'])) { match (strtoupper($cfg['user'])) {
'EMAIL' => ($record['ident_switch.form.imap.username'] = $email) && ($loginSet = true), 'EMAIL' => ($record['ident_switch.form.imap.username'] = $email) && ($loginSet = true),
'MBOX' => ($record['ident_switch.form.imap.username'] = strstr($email, '@', true)) && ($loginSet = true), 'MBOX' => ($record['ident_switch.form.imap.username'] = strstr($email, '@', true)) && ($loginSet = true),
default => null, default => null,
}; };
} }
if (!empty($cfg['readonly'])) { if (!empty($cfg['readonly'])) {
$record['ident_switch.form.common.readonly'] = $loginSet ? 2 : 1; $record['ident_switch.form.common.readonly'] = $loginSet ? 2 : 1;
} }
// IMAP folder hierarchy delimiter (null or absent = auto-detect) // IMAP folder hierarchy delimiter (null or absent = auto-detect)
if (isset($cfg['delimiter'])) { if (isset($cfg['delimiter'])) {
$record['ident_switch.form.imap.delimiter'] = $cfg['delimiter']; $record['ident_switch.form.imap.delimiter'] = $cfg['delimiter'];
} }
// Notification defaults from preconfig // Notification defaults from preconfig
if (isset($cfg['notify_check'])) { if (isset($cfg['notify_check'])) {
$record['ident_switch.form.notify.check'] = $cfg['notify_check'] ? 1 : 0; $record['ident_switch.form.notify.check'] = $cfg['notify_check'] ? 1 : 0;
} }
foreach (['notify_basic', 'notify_sound', 'notify_desktop'] as $key) { foreach (['notify_basic', 'notify_sound', 'notify_desktop'] as $key) {
if (isset($cfg[$key])) { if (isset($cfg[$key])) {
$formKey = 'ident_switch.form.notify.' . substr($key, 7); $formKey = 'ident_switch.form.notify.' . substr($key, 7);
$record[$formKey] = $cfg[$key] === null ? '' : ($cfg[$key] ? '1' : '0'); $record[$formKey] = $cfg[$key] === null ? '' : ($cfg[$key] ? '1' : '0');
} }
} }
return (bool)($cfg['readonly'] ?? false); return (bool)($cfg['readonly'] ?? false);
} }
return false; return false;
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
/** /**
* ident_switch - Account switching handler. * ident_switch - Account switching handler.
* *
@@ -17,392 +18,394 @@
*/ */
class IdentSwitchSwitcher class IdentSwitchSwitcher
{ {
/** /**
* Handle the account switch action (AJAX). * Handle the account switch action (AJAX).
* *
* Saves current account state to session, loads the target account's * Saves current account state to session, loads the target account's
* IMAP/SMTP configuration, and redirects to INBOX. * IMAP/SMTP configuration, and redirects to INBOX.
* When switching back to default (id=-1), restores the original session state. * When switching back to default (id=-1), restores the original session state.
*/ */
public function switch_account(): void public function switch_account(): void
{ {
$rc = rcmail::get_instance(); $rc = rcmail::get_instance();
$my_postfix_len = strlen(ident_switch::MY_POSTFIX); $my_postfix_len = strlen(ident_switch::MY_POSTFIX);
$identId = (int)rcube_utils::get_input_value('_ident-id', rcube_utils::INPUT_POST); $identId = (int)rcube_utils::get_input_value('_ident-id', rcube_utils::INPUT_POST);
$rc->session->remove('folders'); $rc->session->remove('folders');
$rc->session->remove('unseen_count'); $rc->session->remove('unseen_count');
// Reset baseline for the target account so delta goes back to 0 // Reset baseline for the target account so delta goes back to 0
$this->reset_baseline($identId === -1 ? 0 : null, $rc, $identId); $this->reset_baseline($identId === -1 ? 0 : null, $rc, $identId);
if ($identId === -1) { if ($identId === -1) {
// Switch to main account // Switch to main account
ident_switch::write_log('Switching mailbox back to default.'); ident_switch::write_log('Switching mailbox back to default.');
// Restore everything with STORAGE*my_postfix // Restore everything with STORAGE*my_postfix
foreach ($_SESSION as $k => $v) { foreach ($_SESSION as $k => $v) {
if (str_starts_with(strtolower($k), 'storage') && str_ends_with($k, ident_switch::MY_POSTFIX)) { if (str_starts_with(strtolower($k), 'storage') && str_ends_with($k, ident_switch::MY_POSTFIX)) {
$realKey = substr($k, 0, -$my_postfix_len); $realKey = substr($k, 0, -$my_postfix_len);
$_SESSION[$realKey] = $_SESSION[$k]; $_SESSION[$realKey] = $_SESSION[$k];
$rc->session->remove($k); $rc->session->remove($k);
} }
} }
$_SESSION['imap_delimiter'] = $_SESSION['imap_delimiter' . ident_switch::MY_POSTFIX] ?? null; $_SESSION['imap_delimiter'] = $_SESSION['imap_delimiter' . ident_switch::MY_POSTFIX] ?? null;
$_SESSION['username'] = $rc->user->data['username']; $_SESSION['username'] = $rc->user->data['username'];
$_SESSION['password'] = $_SESSION['password' . ident_switch::MY_POSTFIX]; $_SESSION['password'] = $_SESSION['password' . ident_switch::MY_POSTFIX];
$_SESSION['iid' . ident_switch::MY_POSTFIX] = -1; $_SESSION['iid' . ident_switch::MY_POSTFIX] = -1;
foreach (rcube_storage::$folder_types as $type) { foreach (rcube_storage::$folder_types as $type) {
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
if (isset($_SESSION[$otherKey])) { if (isset($_SESSION[$otherKey])) {
$rc->session->remove($otherKey); $rc->session->remove($otherKey);
} }
} }
} else { } 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 = ?'; $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); $q = $rc->db->query($sql, $identId, $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (is_array($r)) { if (is_array($r)) {
$r['username'] = ident_switch::resolve_username((int)$r['iid'], $r['username']); $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 ($_SESSION['username'] === $rc->user->data['username']) {
// If we are in default account now - save values // If we are in default account now - save values
foreach ($_SESSION as $k => $v) { foreach ($_SESSION as $k => $v) {
if (str_starts_with(strtolower($k), 'storage') && !str_ends_with($k, ident_switch::MY_POSTFIX)) { if (str_starts_with(strtolower($k), 'storage') && !str_ends_with($k, ident_switch::MY_POSTFIX)) {
if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) { if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) {
$_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k]; $_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k];
} }
$rc->session->remove($k); $rc->session->remove($k);
} }
} }
foreach (['password', 'imap_delimiter'] as $k) { foreach (['password', 'imap_delimiter'] as $k) {
if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) { if (!isset($_SESSION[$k . ident_switch::MY_POSTFIX])) {
$_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k]; $_SESSION[$k . ident_switch::MY_POSTFIX] = $_SESSION[$k];
} }
$rc->session->remove($k); $rc->session->remove($k);
} }
} }
$parsed = ident_switch::parse_host_scheme($r['imap_host'] ?: 'localhost'); $parsed = ident_switch::parse_host_scheme($r['imap_host'] ?: 'localhost');
$host = $parsed['host']; $host = $parsed['host'];
$ssl = $parsed['scheme'] ?: null; $ssl = $parsed['scheme'] ?: null;
if (!$ssl && ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) { if (!$ssl && ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS)) {
$ssl = 'tls'; // Backward compat: old records without scheme in host $ssl = 'tls'; // Backward compat: old records without scheme in host
} }
$def_port = ($ssl === 'ssl') ? 993 : 143; $def_port = ($ssl === 'ssl') ? 993 : 143;
$port = $r['imap_port'] ?: $def_port; $port = $r['imap_port'] ?: $def_port;
$delimiter = $r['imap_delimiter'] ?: null; $delimiter = $r['imap_delimiter'] ?: null;
$_SESSION['storage_host'] = $host; $_SESSION['storage_host'] = $host;
$_SESSION['storage_ssl'] = $ssl; $_SESSION['storage_ssl'] = $ssl;
$_SESSION['storage_port'] = $port; $_SESSION['storage_port'] = $port;
$_SESSION['imap_delimiter'] = $delimiter; $_SESSION['imap_delimiter'] = $delimiter;
$_SESSION['username'] = $r['username']; $_SESSION['username'] = $r['username'];
$_SESSION['password'] = $r['password']; $_SESSION['password'] = $r['password'];
$_SESSION['iid' . ident_switch::MY_POSTFIX] = $r['iid']; $_SESSION['iid' . ident_switch::MY_POSTFIX] = $r['iid'];
foreach (rcube_storage::$folder_types as $type) { foreach (rcube_storage::$folder_types as $type) {
if (!empty($r[$type . '_mbox'])) { if (!empty($r[$type . '_mbox'])) {
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
$_SESSION[$otherKey] = $r[$type . '_mbox']; $_SESSION[$otherKey] = $r[$type . '_mbox'];
} }
} }
} else { } else {
ident_switch::write_log("Requested remote mailbox with ID = {$identId} not found."); ident_switch::write_log("Requested remote mailbox with ID = {$identId} not found.");
return; return;
} }
} }
$rc->output->redirect([ $rc->output->redirect([
'_task' => 'mail', '_task' => 'mail',
'_mbox' => 'INBOX', '_mbox' => 'INBOX',
]); ]);
} }
/** /**
* Handle smtp_connect hook: configure SMTP settings for the active account. * Handle smtp_connect hook: configure SMTP settings for the active account.
* *
* Loads SMTP host, port, credentials, and TLS settings from the database * Loads SMTP host, port, credentials, and TLS settings from the database
* for the currently selected identity. * for the currently selected identity.
* *
* @param array $args Hook arguments containing SMTP connection parameters. * @param array $args Hook arguments containing SMTP connection parameters.
* @return array Modified hook arguments with updated SMTP settings. * @return array Modified hook arguments with updated SMTP settings.
*/ */
public function configure_smtp(array $args): array public function configure_smtp(array $args): array
{ {
$iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null; $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
if (!is_numeric($iid) || (int)$iid === -1) { if (!is_numeric($iid) || (int)$iid === -1) {
ident_switch::debug_log('SMTP: no active switch, resolving from _from header'); ident_switch::debug_log('SMTP: no active switch, resolving from _from header');
$requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST); $requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
if (empty($requestFrom)) { if (empty($requestFrom)) {
ident_switch::debug_log('SMTP: no _from parameter, using default config'); ident_switch::debug_log('SMTP: no _from parameter, using default config');
return $args; return $args;
} }
$iid = intval($requestFrom); $iid = intval($requestFrom);
if ($iid === 0) { if ($iid === 0) {
ident_switch::debug_log('SMTP: _from is not an integer, using default config'); ident_switch::debug_log('SMTP: _from is not an integer, using default config');
return $args; 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 = ?'; $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); $q = $rc->db->query($sql, $iid, $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (is_array($r)) { if (is_array($r)) {
// If this is an alias, follow parent_id to get the parent's SMTP config // If this is an alias, follow parent_id to get the parent's SMTP config
if (!empty($r['parent_id'])) { if (!empty($r['parent_id'])) {
ident_switch::debug_log("SMTP: identity {$iid} is alias, following parent_id={$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 = ?'; $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); $q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (!is_array($r)) { if (!is_array($r)) {
ident_switch::debug_log("SMTP: parent account not found, using default config"); ident_switch::debug_log("SMTP: parent account not found, using default config");
return $args; return $args;
} }
$iid = $r['iid']; $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']; $authMode = (int)$r['smtp_auth'];
if ($authMode === ident_switch::SMTP_AUTH_CUSTOM) { if ($authMode === ident_switch::SMTP_AUTH_CUSTOM) {
$args['smtp_user'] = $r['smtp_username'] ?: ''; $args['smtp_user'] = $r['smtp_username'] ?: '';
$args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : ''; $args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : '';
} elseif ($authMode === ident_switch::SMTP_AUTH_IMAP) { } elseif ($authMode === ident_switch::SMTP_AUTH_IMAP) {
$args['smtp_user'] = $r['username']; $args['smtp_user'] = $r['username'];
$args['smtp_pass'] = $rc->decrypt($r['password']) ?: ''; $args['smtp_pass'] = $rc->decrypt($r['password']) ?: '';
} else { } else {
$args['smtp_user'] = ''; $args['smtp_user'] = '';
$args['smtp_pass'] = ''; $args['smtp_pass'] = '';
} }
// Host already contains scheme (ssl:// or tls://) from form // Host already contains scheme (ssl:// or tls://) from form
$smtpHost = $r['smtp_host'] ?: 'localhost'; $smtpHost = $r['smtp_host'] ?: 'localhost';
$smtpPort = $r['smtp_port'] ?: 587; $smtpPort = $r['smtp_port'] ?: 587;
$args['smtp_host'] = $smtpHost . ':' . $smtpPort; $args['smtp_host'] = $smtpHost . ':' . $smtpPort;
$authLabel = match ($authMode) { $authLabel = match ($authMode) {
ident_switch::SMTP_AUTH_IMAP => 'imap', ident_switch::SMTP_AUTH_IMAP => 'imap',
ident_switch::SMTP_AUTH_NONE => 'none', ident_switch::SMTP_AUTH_NONE => 'none',
ident_switch::SMTP_AUTH_CUSTOM => 'custom', ident_switch::SMTP_AUTH_CUSTOM => 'custom',
default => "unknown({$authMode})", default => "unknown({$authMode})",
}; };
ident_switch::debug_log("SMTP: iid={$iid}, host={$args['smtp_host']}, user={$args['smtp_user']}, auth={$authLabel}"); 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. * Handle managesieve_connect hook: configure Sieve settings for the active account.
* *
* Loads Sieve host, port, and credentials from the database * Loads Sieve host, port, and credentials from the database
* for the currently selected identity. * for the currently selected identity.
* *
* @param array $args Hook arguments containing Sieve connection parameters. * @param array $args Hook arguments containing Sieve connection parameters.
* @return array Modified hook arguments with updated Sieve settings. * @return array Modified hook arguments with updated Sieve settings.
*/ */
public function configure_managesieve(array $args): array public function configure_managesieve(array $args): array
{ {
$iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null; $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
if (!is_numeric($iid) || (int)$iid === -1) { if (!is_numeric($iid) || (int)$iid === -1) {
return $args; 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 = ?'; $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); $q = $rc->db->query($sql, $iid, $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (is_array($r)) { if (is_array($r)) {
// If this is an alias, follow parent_id to get the parent's Sieve config // If this is an alias, follow parent_id to get the parent's Sieve config
if (!empty($r['parent_id'])) { if (!empty($r['parent_id'])) {
ident_switch::debug_log("Sieve: identity {$iid} is alias, following parent_id={$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 = ?'; $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); $q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (!is_array($r)) { if (!is_array($r)) {
ident_switch::debug_log("Sieve: parent account not found, using default config"); ident_switch::debug_log("Sieve: parent account not found, using default config");
return $args; return $args;
} }
$iid = $r['iid']; $iid = $r['iid'];
} }
if (empty($r['sieve_host'])) { if (empty($r['sieve_host'])) {
return $args; return $args;
} }
$r['username'] = ident_switch::resolve_username($iid, $r['username']); $r['username'] = ident_switch::resolve_username($iid, $r['username']);
$sieveHost = $r['sieve_host']; $sieveHost = $r['sieve_host'];
$sievePort = $r['sieve_port'] ?: 4190; $sievePort = $r['sieve_port'] ?: 4190;
$args['host'] = $sieveHost . ':' . $sievePort; $args['host'] = $sieveHost . ':' . $sievePort;
$authMode = (int)$r['sieve_auth']; $authMode = (int)$r['sieve_auth'];
if ($authMode === ident_switch::SIEVE_AUTH_CUSTOM) { if ($authMode === ident_switch::SIEVE_AUTH_CUSTOM) {
$args['user'] = $r['sieve_username'] ?: ''; $args['user'] = $r['sieve_username'] ?: '';
$args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : ''; $args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : '';
} elseif ($authMode === ident_switch::SIEVE_AUTH_IMAP) { } elseif ($authMode === ident_switch::SIEVE_AUTH_IMAP) {
$args['user'] = $r['username']; $args['user'] = $r['username'];
$args['password'] = $rc->decrypt($r['password']) ?: ''; $args['password'] = $rc->decrypt($r['password']) ?: '';
} else { } else {
$args['user'] = ''; $args['user'] = '';
$args['password'] = ''; $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. * Handle preferences_list hook: customize special folders form for remote accounts.
* *
* When viewing folder preferences while impersonating, shows the remote account's * When viewing folder preferences while impersonating, shows the remote account's
* special folder assignments instead of the default ones. * special folder assignments instead of the default ones.
* *
* @param array $args Hook arguments containing 'section' and 'blocks' with form data. * @param array $args Hook arguments containing 'section' and 'blocks' with form data.
* @return array Modified hook arguments with updated folder selections. * @return array Modified hook arguments with updated folder selections.
*/ */
public function get_special_folders_form(array $args): array public function get_special_folders_form(array $args): array
{ {
$rc = rcmail::get_instance(); $rc = rcmail::get_instance();
if ($args['section'] === 'folders' if (
&& strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0 $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('')"; $no_override = array_flip((array)$rc->config->get('dont_override'));
$select = $rc->folder_selector([ $onchange = "if ($(this).val() == 'INBOX') $(this).val('')";
'noselection' => '---', $select = $rc->folder_selector([
'realnames' => true, 'noselection' => '---',
'maxlength' => 30, 'realnames' => true,
'folder_filter' => 'mail', 'maxlength' => 30,
'folder_rights' => 'w', 'folder_filter' => 'mail',
]); 'folder_rights' => 'w',
]);
$sql = 'SELECT label FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; $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); $q = $rc->db->query($sql, $_SESSION['iid' . ident_switch::MY_POSTFIX], $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
$args['blocks']['main']['name'] .= ' (' . ($r['label'] ? rcube::Q($rc->gettext('server')) . ': ' . rcube::Q($r['label']) : 'remote') . ')'; $args['blocks']['main']['name'] .= ' (' . ($r['label'] ? rcube::Q($rc->gettext('server')) . ': ' . rcube::Q($r['label']) : 'remote') . ')';
foreach (rcube_storage::$folder_types as $type) { foreach (rcube_storage::$folder_types as $type) {
if (isset($no_override[$type . '_mbox'])) { if (isset($no_override[$type . '_mbox'])) {
continue; continue;
} }
$defaultKey = $type . '_mbox_default' . ident_switch::MY_POSTFIX; $defaultKey = $type . '_mbox_default' . ident_switch::MY_POSTFIX;
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
$selected = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey] ?? ''; $selected = $_SESSION[$otherKey] ?? $_SESSION[$defaultKey] ?? '';
$attr = ['id' => '_' . $type . '_mbox', 'name' => '_' . $type . '_mbox', 'onchange' => $onchange]; $attr = ['id' => '_' . $type . '_mbox', 'name' => '_' . $type . '_mbox', 'onchange' => $onchange];
$args['blocks']['main']['options'][$type . '_mbox']['content'] = $select->show($selected, $attr); $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. * Handle preferences_save hook: persist special folder assignments for remote accounts.
* *
* Saves folder preferences to the plugin's database table instead of the default * Saves folder preferences to the plugin's database table instead of the default
* Roundcube preferences when impersonating a remote account. * Roundcube preferences when impersonating a remote account.
* *
* @param array $args Hook arguments containing 'section' and 'prefs' with folder data. * @param array $args Hook arguments containing 'section' and 'prefs' with folder data.
* @return array Modified hook arguments, with 'abort' set to prevent default save. * @return array Modified hook arguments, with 'abort' set to prevent default save.
*/ */
public function save_special_folders(array $args): array public function save_special_folders(array $args): array
{ {
$rc = rcmail::get_instance(); $rc = rcmail::get_instance();
if ($args['section'] === 'folders' if (
&& strcasecmp($rc->user->data['username'], $_SESSION['username']) !== 0 $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); $sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
$r = $rc->db->fetch_assoc($q); $q = $rc->db->query($sql, $_SESSION['iid' . ident_switch::MY_POSTFIX], $rc->user->ID);
if ($r) { $r = $rc->db->fetch_assoc($q);
$sql = 'UPDATE ' . if ($r) {
$rc->db->table_name(ident_switch::TABLE) . $sql = 'UPDATE ' .
' SET drafts_mbox = ?, sent_mbox = ?, junk_mbox = ?, trash_mbox = ?' . $rc->db->table_name(ident_switch::TABLE) .
' WHERE id = ?'; ' SET drafts_mbox = ?, sent_mbox = ?, junk_mbox = ?, trash_mbox = ?' .
' WHERE id = ?';
$rc->db->query( $rc->db->query(
$sql, $sql,
$args['prefs']['drafts_mbox'], $args['prefs']['drafts_mbox'],
$args['prefs']['sent_mbox'], $args['prefs']['sent_mbox'],
$args['prefs']['junk_mbox'], $args['prefs']['junk_mbox'],
$args['prefs']['trash_mbox'], $args['prefs']['trash_mbox'],
$r['id'] $r['id']
); );
// Abort to prevent RC from saving prefs to default storage // Abort to prevent RC from saving prefs to default storage
$args['abort'] = true; $args['abort'] = true;
$args['result'] = true; $args['result'] = true;
foreach (rcube_storage::$folder_types as $type) { foreach (rcube_storage::$folder_types as $type) {
if (!empty($args['prefs'][$type . '_mbox'])) { if (!empty($args['prefs'][$type . '_mbox'])) {
$otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX; $otherKey = $type . '_mbox' . ident_switch::MY_POSTFIX;
$_SESSION[$otherKey] = $args['prefs'][$type . '_mbox']; $_SESSION[$otherKey] = $args['prefs'][$type . '_mbox'];
} }
} }
return $args; return $args;
} }
$args['abort'] = true; $args['abort'] = true;
$args['result'] = false; $args['result'] = false;
return $args; return $args;
} }
foreach (rcube_storage::$folder_types as $type) { foreach (rcube_storage::$folder_types as $type) {
if (!empty($args['prefs'][$type . '_mbox'])) { if (!empty($args['prefs'][$type . '_mbox'])) {
$key = $type . '_mbox_default' . ident_switch::MY_POSTFIX; $key = $type . '_mbox_default' . ident_switch::MY_POSTFIX;
$_SESSION[$key] = $args['prefs'][$type . '_mbox']; $_SESSION[$key] = $args['prefs'][$type . '_mbox'];
} }
} }
return $args; return $args;
} }
/** /**
* Reset the baseline for a target account so delta display resets to 0. * Reset the baseline for a target account so delta display resets to 0.
* *
* For primary account (identId=-1), iid is 0. * For primary account (identId=-1), iid is 0.
* For secondary accounts, look up iid from the ident_switch table. * 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 integer|null $iid Known iid (0 for primary), or null to look up.
* @param rcmail $rc Roundcube instance. * @param rcmail $rc Roundcube instance.
* @param mixed $identId The ident_switch.id value for secondary accounts. * @param mixed $identId The ident_switch.id value for secondary accounts.
*/ */
private function reset_baseline(?int $iid, rcmail $rc, mixed $identId): void private function reset_baseline(?int $iid, rcmail $rc, mixed $identId): void
{ {
if ($iid === null) { if ($iid === null) {
// Look up iid from ident_switch table for secondary account // 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 = ?'; $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); $q = $rc->db->query($sql, $identId, $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (!$r) { if (!$r) {
return; return;
} }
$iid = (int)$r['iid']; $iid = (int)$r['iid'];
} }
$counts = $_SESSION['ident_switch_counts'] ?? []; $counts = $_SESSION['ident_switch_counts'] ?? [];
if (isset($counts[$iid])) { if (isset($counts[$iid])) {
unset($counts[$iid]['baseline']); unset($counts[$iid]['baseline']);
$_SESSION['ident_switch_counts'] = $counts; $_SESSION['ident_switch_counts'] = $counts;
} }
} }
} }