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