diff --git a/ident_switch-form.js b/ident_switch-form.js index 8ad57f6..d5dbf60 100644 --- a/ident_switch-form.js +++ b/ident_switch-form.js @@ -20,7 +20,9 @@ var ident_switch_portDefaults = { }; $(function() { - $("INPUT[name='_ident_switch.form.common.enabled']").change(); + // Apply initial mode visibility + var initialMode = $("SELECT[name='_ident_switch.form.common.mode']").val() || 'primary'; + plugin_switchIdent_mode_onChange(initialMode); plugin_switchIdent_processPreconfig(); // Bind security change handlers @@ -87,6 +89,42 @@ $(function() { } }); +/** + * Handle mode select change: show/hide form sections based on selected mode. + * @param {string} mode - 'primary', 'alias:N', or 'separate'. + */ +function plugin_switchIdent_mode_onChange(mode) { + // Find the fieldsets/legends for each section + var $modeFld = $("SELECT[name='_ident_switch.form.common.mode']"); + var $allFieldsets = $modeFld.closest('form').find('fieldset'); + var $readonlyRow = $("INPUT[name='_ident_switch.form.common.readonly']").parentsUntil('FIELDSET', 'TR, .row'); + + // Separate account fieldsets (label, IMAP, SMTP, Sieve, Notify) + var separateFieldsets = []; + $allFieldsets.each(function() { + var $fs = $(this); + if ($fs.find("INPUT[name='_ident_switch.form.common.label']").length || + $fs.find("INPUT[name='_ident_switch.form.imap.host']").length || + $fs.find("INPUT[name='_ident_switch.form.smtp.host']").length || + $fs.find("INPUT[name='_ident_switch.form.sieve.host']").length || + $fs.find("INPUT[name='_ident_switch.form.notify.check']").length) { + separateFieldsets.push($fs); + } + }); + + if (mode === 'separate') { + // Show all separate account fieldsets + $.each(separateFieldsets, function(_, $fs) { $fs.show(); }); + plugin_switchIdent_processPreconfig(); + } else { + // Hide all separate account fieldsets for primary and alias modes + $.each(separateFieldsets, function(_, $fs) { $fs.hide(); }); + } + + // Always hide readonly row + $readonlyRow.hide(); +} + /** * Handle security dropdown change: update port placeholder and show/hide warning. * @param {string} proto - Protocol name (imap, smtp, sieve). @@ -187,15 +225,6 @@ function plugin_switchIdent_processPreconfig() { } } -function plugin_switchIdent_enabled_onChange(e) { - var $enFld = $("INPUT[name='_ident_switch.form.common.enabled'], INPUT[name='_ident_switch.form.imap.host'], INPUT[name='_ident_switch.form.smtp.host']"); - var $fieldset = $enFld.parents("FIELDSET"); - var isEnabled = $enFld.is(":checked"); - $("INPUT[name!='_ident_switch.form.common.enabled']", $fieldset).prop("disabled", !isEnabled); - $("SELECT", $fieldset).prop("disabled", !isEnabled); - plugin_switchIdent_processPreconfig(); -} - /** * Handle email field change: apply preconfig and manage domain restriction. * @param {string} email - The email address entered by the user. @@ -217,17 +246,22 @@ function plugin_switchIdent_onEmailChange(email) { // Update username placeholder to match current email $("INPUT[name='_ident_switch.form.imap.username']").attr('placeholder', email); - // Show/hide domain warning + // Show/hide domain warning and restrict "separate" option + var $modeSelect = $("SELECT[name='_ident_switch.form.common.mode']"); if (preconfigOnly && !cfg) { var tpl = rcmail.env.ident_switch_warning_tpl || ''; $('#ident-switch-domain-warning').text(tpl.replace('%s', domain)).show(); - $("INPUT[name='_ident_switch.form.common.enabled']").prop('checked', false).prop('disabled', true); - plugin_switchIdent_enabled_onChange(); + // Disable "separate" option but keep alias options available + $modeSelect.find('option[value="separate"]').prop('disabled', true); + if ($modeSelect.val() === 'separate') { + $modeSelect.val('primary'); + plugin_switchIdent_mode_onChange('primary'); + } return; } $('#ident-switch-domain-warning').hide(); - $("INPUT[name='_ident_switch.form.common.enabled']").prop('disabled', false); + $modeSelect.find('option[value="separate"]').prop('disabled', false); // Only auto-fill for identities without an existing DB record if (!rcmail.env.ident_switch_has_record && cfg) { @@ -241,6 +275,9 @@ function plugin_switchIdent_onEmailChange(email) { * @param {string} email - The full email address. */ function plugin_switchIdent_applyJsPreconfig(cfg, email) { + // Pre-fill server fields silently without changing mode. + // The user must explicitly select "Separate account" to see them. + // Apply protocol settings $.each(['imap', 'smtp', 'sieve'], function(_, proto) { if (!cfg[proto]) return; @@ -282,6 +319,6 @@ function plugin_switchIdent_applyJsPreconfig(cfg, email) { } $("INPUT[name='_ident_switch.form.common.readonly']").val(readonlyLevel); - // Re-apply enabled/disabled state (enables fields, then processPreconfig disables readonly ones) - plugin_switchIdent_enabled_onChange(); + // Re-apply preconfig readonly state + plugin_switchIdent_processPreconfig(); } diff --git a/lib/IdentSwitchForm.php b/lib/IdentSwitchForm.php index 3a228fe..1ace377 100644 --- a/lib/IdentSwitchForm.php +++ b/lib/IdentSwitchForm.php @@ -16,6 +16,9 @@ */ class IdentSwitchForm { + /** @var string Sentinel value placed in password fields to indicate an existing password. */ + private const PASSWORD_SENTINEL = '__IDENT_SWITCH_UNCHANGED__'; + private ident_switch $plugin; public function __construct(ident_switch $plugin) @@ -27,9 +30,82 @@ class IdentSwitchForm * 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. + * @return array Form field definitions for mode select, label, and readonly. */ - public function get_common_fields(array &$record): array + public function get_common_fields(array &$record, bool $domainAllowed = true, string $warningHtml = ''): array + { + $prefix = 'ident_switch.form.common.'; + $rc = rcmail::get_instance(); + + // Build mode select: Primary / Alias of X / Separate account + $modeSelect = new html_select([ + 'name' => "_{$prefix}mode", + 'onchange' => 'plugin_switchIdent_mode_onChange(this.value);', + ]); + $modeSelect->add($this->plugin->gettext('form.common.mode.primary'), 'primary'); + + // Add available accounts as alias targets + $accounts = $this->get_available_accounts($rc, $record['identity_id'] ?? null); + foreach ($accounts as $acc) { + $label = $acc['label'] ?: $acc['email']; + $modeSelect->add($label, 'alias:' . $acc['id']); + } + + // Only offer "Separate account" when domain is allowed + if ($domainAllowed) { + $modeSelect->add($this->plugin->gettext('form.common.mode.separate'), 'separate'); + } + + $currentMode = $record["{$prefix}mode"] ?? 'primary'; + $modeHtml = $modeSelect->show($currentMode) + . html::span( + ['class' => 'form-text'], + rcube::Q($this->plugin->gettext('form.common.mode.hint')) + ) + . $warningHtml; + + return [ + $prefix . 'mode' => ['value' => $modeHtml], + $prefix . 'readonly' => ['type' => 'hidden'], + ]; + } + + /** + * Get all separate accounts available as alias targets. + * + * @param rcmail $rc Roundcube instance. + * @param int|null $excludeIid Identity ID to exclude (the identity being edited). + * @return array List of account records with id, label, username, email. + */ + private function get_available_accounts(rcmail $rc, ?int $excludeIid): array + { + $sql = 'SELECT isw.id, isw.label, isw.username, ii.email' + . ' FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' isw' + . ' INNER JOIN ' . $rc->db->table_name('identities') . ' ii ON isw.iid = ii.identity_id' + . ' WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.parent_id IS NULL'; + + $params = [$rc->user->ID, ident_switch::DB_ENABLED]; + + if ($excludeIid !== null) { + $sql .= ' AND isw.iid != ?'; + $params[] = $excludeIid; + } + + $q = $rc->db->query($sql, ...$params); + $accounts = []; + while ($r = $rc->db->fetch_assoc($q)) { + $accounts[] = $r; + } + return $accounts; + } + + /** + * Build the separate account header fields (label). + * + * @param array $record Identity record data. + * @return array Form field definitions for label. + */ + public function get_separate_fields(array &$record): array { $prefix = 'ident_switch.form.common.'; @@ -46,9 +122,7 @@ class IdentSwitchForm ); return [ - $prefix . 'enabled' => ['type' => 'checkbox', 'onchange' => 'plugin_switchIdent_enabled_onChange();'], $prefix . 'label' => ['value' => $labelHtml], - $prefix . 'readonly' => ['type' => 'hidden'], ]; } @@ -66,7 +140,7 @@ class IdentSwitchForm $prefix . 'security' => ['value' => $this->build_security_select($prefix, $record, 'ssl')], $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 993], $prefix . 'username' => ['type' => 'text', 'size' => 64, 'placeholder' => $record['email'] ?? ''], - $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'new-password'], + $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'], $prefix . 'delimiter' => ['value' => $this->build_delimiter_field($prefix, $record)], ]; } @@ -96,7 +170,7 @@ class IdentSwitchForm $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 587], $prefix . 'auth' => ['value' => $authType->show($authVal !== null ? [$authVal] : [])], $prefix . 'username' => ['type' => 'text', 'size' => 64, 'autocomplete' => 'off'], - $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'new-password'], + $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'], ]; } @@ -124,7 +198,7 @@ class IdentSwitchForm $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 4190], $prefix . 'auth' => ['value' => $authType->show($authVal !== null ? [$authVal] : [])], $prefix . 'username' => ['type' => 'text', 'size' => 64, 'autocomplete' => 'off'], - $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'new-password'], + $prefix . 'password' => ['type' => 'password', 'size' => 64, 'autocomplete' => 'off'], ]; } @@ -379,8 +453,13 @@ class IdentSwitchForm } } - // Checkboxes: absent from POST means unchecked - $record['ident_switch.form.common.enabled'] = !empty(self::get_field_value('common', 'enabled', false)); + // Mode select + $modeVal = self::get_field_value('common', 'mode', false); + if ($modeVal !== null) { + $record['ident_switch.form.common.mode'] = $modeVal; + } + + // Checkbox: absent from POST means unchecked $record['ident_switch.form.notify.check'] = !empty(self::get_field_value('notify', 'check', false)); } @@ -503,42 +582,27 @@ class IdentSwitchForm $this->plugin->add_texts('localization'); - // Build info section with description and domain warning $preconfigOnly = $rc->config->get('ident_switch.preconfig_only', false); $domainAllowed = empty($args['record']['email']) || $this->is_domain_allowed($args['record']['email']); - $warningVisible = $preconfigOnly && !$domainAllowed; - - // Extract domain from email for warning message - $email = $args['record']['email'] ?? ''; - $domain = ''; - if (!empty($email) && str_contains($email, '@')) { - $domain = substr($email, strpos($email, '@') + 1); + // Build domain warning HTML (included in mode field) + $warningHtml = ''; + if ($preconfigOnly) { + $email = $args['record']['email'] ?? ''; + $domain = ''; + if (!empty($email) && str_contains($email, '@')) { + $domain = substr($email, strpos($email, '@') + 1); + } + $warningHtml = html::div( + ['class' => 'boxwarning', 'id' => 'ident-switch-domain-warning', + 'style' => $domainAllowed ? 'display:none' : ''], + rcube::Q(sprintf($this->plugin->gettext('form.preconfig_only_warning'), $domain)) + ); } - $infoContent = html::div( - ['class' => 'boxinformation', 'id' => 'ident-switch-info'], - rcube::Q($this->plugin->gettext('form.description')) - ); - $warningContent = html::div( - ['class' => 'boxwarning', 'id' => 'ident-switch-domain-warning', - 'style' => $warningVisible ? '' : 'display:none'], - rcube::Q(sprintf($this->plugin->gettext('form.preconfig_only_warning'), $domain)) - ); - - $args['form']['ident_switch'] = [ - 'name' => $this->plugin->gettext('form.caption'), - 'content' => $infoContent . $warningContent, - ]; - // Pass preconfig data to JS for dynamic form updates $this->pass_preconfig_to_js($rc); - // When preconfig_only is enabled, hide field sections for non-preconfigured domains - if (!$domainAllowed) { - return $args; - } - $row = null; if (isset($args['record']['identity_id'])) { $sql = 'SELECT * FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; @@ -581,8 +645,22 @@ class IdentSwitchForm } } - // Parse flags - $record['ident_switch.form.common.enabled'] = (bool)($row['flags'] & ident_switch::DB_ENABLED); + // Replace encrypted passwords with sentinel (shows dots in form, detectable on save) + foreach (['imap.password', 'smtp.password', 'sieve.password'] as $passKey) { + $fullKey = 'ident_switch.form.' . $passKey; + if (!empty($record[$fullKey])) { + $record[$fullKey] = self::PASSWORD_SENTINEL; + } + } + + // Determine mode: alias (has parent_id) or separate account + if ($row['parent_id'] !== null) { + $record['ident_switch.form.common.mode'] = 'alias:' . $row['parent_id']; + } elseif ($row['flags'] & ident_switch::DB_ENABLED) { + $record['ident_switch.form.common.mode'] = 'separate'; + } else { + $record['ident_switch.form.common.mode'] = 'primary'; + } // Parse host schemes into separate security fields foreach (['imap', 'smtp', 'sieve'] as $proto) { @@ -621,7 +699,11 @@ class IdentSwitchForm $args['form']['ident_switch.common'] = [ 'name' => $this->plugin->gettext('form.common.general'), - 'content' => $this->get_common_fields($record), + 'content' => $this->get_common_fields($record, $domainAllowed, $warningHtml), + ]; + $args['form']['ident_switch.separate'] = [ + 'name' => $this->plugin->gettext('form.caption'), + 'content' => $this->get_separate_fields($record), ]; $args['form']['ident_switch.imap'] = [ 'name' => $this->plugin->gettext('form.imap.caption'), @@ -670,32 +752,46 @@ class IdentSwitchForm return $args; } - // Block save for non-preconfigured domains when preconfig_only is enabled - if (!$this->is_domain_allowed($args['record']['email'])) { + $mode = self::get_field_value('common', 'mode', false) ?? 'primary'; + + // Block separate mode for non-preconfigured domains (alias and primary are always allowed) + if (!$this->is_domain_allowed($args['record']['email']) && $mode === 'separate') { $this->disable($args['id']); return $args; } - if (!self::get_field_value('common', 'enabled', false)) { + if ($mode === 'primary') { $this->disable($args['id']); return $args; } + if (str_starts_with($mode, 'alias:')) { + $parentId = (int)substr($mode, 6); + $this->save_alias($args['id'], $parentId); + return $args; + } + + // mode === 'separate': full validation + save $data = $this->validate(); if (!empty($data['err'])) { $this->plugin->add_texts('localization'); $args['abort'] = true; + $args['result'] = false; $args['message'] = 'ident_switch.err.' . $data['err']; return $args; } $this->apply_readonly_preconfig($data, $args['record']['email']); + // Resolve sentinel passwords to actual passwords for connection testing + $testPass = $this->resolve_password_for_test($rc, $args['id'], $data['imap.pass']); + // Test connections before saving - $connErr = $this->test_connections($data, $args['record']['email'], $data['imap.pass']); + $connErr = $this->test_connections($data, $args['record']['email'], $testPass); if ($connErr) { $this->plugin->add_texts('localization'); $args['abort'] = true; + $args['result'] = false; $args['message'] = 'ident_switch.err.' . $connErr; return $args; } @@ -723,30 +819,48 @@ class IdentSwitchForm return $args; } - // Block creation for non-preconfigured domains when preconfig_only is enabled - if (!$this->is_domain_allowed($args['record']['email'])) { + $mode = self::get_field_value('common', 'mode', false) ?? 'primary'; + + // Block separate mode for non-preconfigured domains (alias and primary are always allowed) + if (!$this->is_domain_allowed($args['record']['email']) && $mode === 'separate') { return $args; } - if (!self::get_field_value('common', 'enabled', false)) { + if ($mode === 'primary') { return $args; } + if (str_starts_with($mode, 'alias:')) { + $parentId = (int)substr($mode, 6); + $_SESSION['createData' . ident_switch::MY_POSTFIX] = [ + 'mode' => 'alias', + 'parent_id' => $parentId, + 'label' => self::get_field_value('common', 'label'), + ]; + return $args; + } + + // mode === 'separate': full validation $data = $this->validate(); if (!empty($data['err'])) { $this->plugin->add_texts('localization'); $args['abort'] = true; + $args['result'] = false; $args['message'] = 'ident_switch.err.' . $data['err']; return $args; } $this->apply_readonly_preconfig($data, $args['record']['email']); + // For new identities, sentinel should not appear, but handle defensively + $testPass = ($data['imap.pass'] === self::PASSWORD_SENTINEL) ? '' : $data['imap.pass']; + // Test connections before saving - $connErr = $this->test_connections($data, $args['record']['email'], $data['imap.pass']); + $connErr = $this->test_connections($data, $args['record']['email'], $testPass); if ($connErr) { $this->plugin->add_texts('localization'); $args['abort'] = true; + $args['result'] = false; $args['message'] = 'ident_switch.err.' . $connErr; return $args; } @@ -779,6 +893,8 @@ class IdentSwitchForm unset($_SESSION['createData' . ident_switch::MY_POSTFIX]); if (!$data || count($data) === 0) { ident_switch::write_log("Object with ident_switch values not found in session for ID = {$args['id']}."); + } elseif (($data['mode'] ?? '') === 'alias') { + $this->save_alias($args['id'], $data['parent_id']); } else { $data['id'] = $args['id']; $this->save($rc, $data); @@ -829,15 +945,12 @@ class IdentSwitchForm $record = ['email' => $email]; $preconfig->apply($record); - // Map preconfig record keys to validated data keys + // Map simple 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.delimiter' => 'imap.delimiter', 'ident_switch.form.imap.username' => 'imap.user', - 'ident_switch.form.smtp.host' => 'smtp.host', 'ident_switch.form.smtp.port' => 'smtp.port', - 'ident_switch.form.sieve.host' => 'sieve.host', 'ident_switch.form.sieve.port' => 'sieve.port', ]; @@ -846,6 +959,44 @@ class IdentSwitchForm $data[$dataKey] = $record[$recordKey]; } } + + // Compose hosts with security scheme (preconfig stores them separately) + foreach (['imap', 'smtp', 'sieve'] as $proto) { + $hostKey = "ident_switch.form.{$proto}.host"; + $secKey = "ident_switch.form.{$proto}.security"; + if (empty($data["{$proto}.host"]) && !empty($record[$hostKey])) { + $security = $record[$secKey] ?? ''; + $data["{$proto}.host"] = self::compose_host_scheme($record[$hostKey], $security); + } + } + } + + /** + * Resolve IMAP password for connection testing. + * + * If the password is the sentinel (unchanged), decrypt from DB. + * Otherwise return the raw password as-is. + * + * @param rcmail $rc Roundcube instance. + * @param int $iid Identity ID. + * @param string|null $pass Raw password from POST. + * @return string Decrypted or raw password for testing. + */ + private function resolve_password_for_test(rcmail $rc, int $iid, ?string $pass): string + { + if ($pass !== self::PASSWORD_SENTINEL) { + return $pass ?? ''; + } + + $sql = 'SELECT password FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $iid, $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + if (!$r || empty($r['password'])) { + return ''; + } + + $decrypted = $rc->decrypt($r['password']); + return ($decrypted !== false) ? $decrypted : ''; } /** @@ -922,8 +1073,8 @@ class IdentSwitchForm return $retVal; } - $retVal['smtp.auth'] = self::get_field_value('smtp', 'auth'); - if (!ctype_digit($retVal['smtp.auth'] ?? '')) { + $retVal['smtp.auth'] = self::get_field_value('smtp', 'auth') ?? (string)ident_switch::SMTP_AUTH_IMAP; + if (!ctype_digit($retVal['smtp.auth'])) { $retVal['err'] = 'auth.num'; return $retVal; } @@ -960,8 +1111,8 @@ class IdentSwitchForm return $retVal; } - $retVal['sieve.auth'] = self::get_field_value('sieve', 'auth'); - if (!ctype_digit($retVal['sieve.auth'] ?? '')) { + $retVal['sieve.auth'] = self::get_field_value('sieve', 'auth') ?? (string)ident_switch::SIEVE_AUTH_IMAP; + if (!ctype_digit($retVal['sieve.auth'])) { $retVal['err'] = 'auth.num'; return $retVal; } @@ -1042,7 +1193,7 @@ class IdentSwitchForm // 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 = ?,' . + ' SET flags = ?, parent_id = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?,' . ' smtp_host = ?, smtp_port = ?, smtp_auth = ?, smtp_username = ?, smtp_password = ?,' . ' sieve_host = ?, sieve_port = ?, sieve_auth = ?, sieve_username = ?, sieve_password = ?,' . ' notify_check = ?, notify_basic = ?, notify_sound = ?, notify_desktop = ?,' . @@ -1052,34 +1203,31 @@ class IdentSwitchForm // 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,' . + '(flags, parent_id, label, imap_host, imap_port, imap_delimiter, username, password,' . ' smtp_host, smtp_port, smtp_auth, smtp_username, smtp_password,' . ' sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password,' . ' notify_check, notify_basic, notify_sound, notify_desktop,' . - ' user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + ' user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; } else { return false; } - // Encrypt IMAP password (compare raw POST value with decrypted DB value) - $existingImapPass = !empty($r['password']) ? $rc->decrypt($r['password']) : null; - if ($data['imap.pass'] === $existingImapPass && $existingImapPass !== false) { - $data['imap.pass'] = $r['password']; + // Handle IMAP password: sentinel means unchanged, empty means clear, else encrypt + if ($data['imap.pass'] === self::PASSWORD_SENTINEL) { + $data['imap.pass'] = $r['password'] ?? null; } else { - $data['imap.pass'] = $rc->encrypt($data['imap.pass']); + $data['imap.pass'] = $data['imap.pass'] ? $rc->encrypt($data['imap.pass']) : null; } - // Encrypt SMTP password - $existingSmtpPass = !empty($r['smtp_password']) ? $rc->decrypt($r['smtp_password']) : null; - if ($data['smtp.pass'] === $existingSmtpPass && $existingSmtpPass !== false) { + // Handle SMTP password + if ($data['smtp.pass'] === self::PASSWORD_SENTINEL) { $data['smtp.pass'] = $r['smtp_password'] ?? null; } else { $data['smtp.pass'] = $data['smtp.pass'] ? $rc->encrypt($data['smtp.pass']) : null; } - // Encrypt Sieve password - $existingSievePass = !empty($r['sieve_password']) ? $rc->decrypt($r['sieve_password']) : null; - if ($data['sieve.pass'] === $existingSievePass && $existingSievePass !== false) { + // Handle Sieve password + if ($data['sieve.pass'] === self::PASSWORD_SENTINEL) { $data['sieve.pass'] = $r['sieve_password'] ?? null; } else { $data['sieve.pass'] = $data['sieve.pass'] ? $rc->encrypt($data['sieve.pass']) : null; @@ -1088,6 +1236,7 @@ class IdentSwitchForm $rc->db->query( $sql, $data['flags'], + null, // parent_id: NULL for separate accounts $data['label'], $data['imap.host'], $data['imap.port'], @@ -1116,6 +1265,54 @@ class IdentSwitchForm return true; } + /** + * Save an alias link between an identity and a parent account. + * + * Creates or updates an ident_switch record with parent_id set and + * all server fields cleared (alias uses parent's config). + * + * @param int $iid Identity ID of the alias. + * @param int $parentId ident_switch.id of the parent account. + */ + private function save_alias(int $iid, int $parentId): void + { + $rc = rcmail::get_instance(); + + // Validate that parent_id exists and belongs to this user + $sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE) + . ' WHERE id = ? AND user_id = ? AND parent_id IS NULL'; + $q = $rc->db->query($sql, $parentId, $rc->user->ID); + if (!$rc->db->fetch_assoc($q)) { + ident_switch::write_log("Alias save: parent account with id={$parentId} not found for user."); + return; + } + + $label = self::get_field_value('common', 'label'); + + // Check if record already exists + $sql = 'SELECT id FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $iid, $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + + if ($r) { + $sql = 'UPDATE ' . $rc->db->table_name(ident_switch::TABLE) + . ' SET flags = ?, parent_id = ?, label = ?,' + . ' imap_host = NULL, imap_port = NULL, imap_delimiter = NULL,' + . ' username = NULL, password = NULL,' + . ' smtp_host = NULL, smtp_port = NULL, smtp_auth = 1, smtp_username = NULL, smtp_password = NULL,' + . ' sieve_host = NULL, sieve_port = NULL, sieve_auth = 1, sieve_username = NULL, sieve_password = NULL,' + . ' notify_check = 0, notify_basic = NULL, notify_sound = NULL, notify_desktop = NULL' + . ' WHERE id = ?'; + $rc->db->query($sql, ident_switch::DB_ENABLED, $parentId, $label, $r['id']); + } else { + $sql = 'INSERT INTO ' . $rc->db->table_name(ident_switch::TABLE) + . ' (flags, parent_id, label, user_id, iid) VALUES (?, ?, ?, ?, ?)'; + $rc->db->query($sql, ident_switch::DB_ENABLED, $parentId, $label, $rc->user->ID, $iid); + } + + ident_switch::write_log("Saved alias link: identity {$iid} → parent account {$parentId}."); + } + /** * Disable the ident_switch flag for a given identity. * diff --git a/localization/de_DE.inc b/localization/de_DE.inc index 1df321e..6a386f1 100644 --- a/localization/de_DE.inc +++ b/localization/de_DE.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'Die Konfiguration als separates Konto // General $labels['form.common.general'] = 'Allgemein'; -// Enabled -$labels['form.common.enabled'] = 'Aktiviert'; +// Account mode +$labels['form.common.mode'] = 'Kontomodus'; +$labels['form.common.mode.primary'] = 'Hauptkonto'; +$labels['form.common.mode.separate'] = 'Separates Konto'; +$labels['form.common.mode.hint'] = 'Verknüpfen Sie diese Identität mit einem bestehenden Konto oder konfigurieren Sie sie als separates Konto mit eigenem Mailserver.'; // Label $labels['form.common.label'] = 'Bezeichnung'; diff --git a/localization/en_US.inc b/localization/en_US.inc index 045fd1b..b8a49ad 100644 --- a/localization/en_US.inc +++ b/localization/en_US.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'Separate account configuration is not // General $labels['form.common.general'] = 'General'; -// Enabled -$labels['form.common.enabled'] = 'Enabled'; +// Account mode +$labels['form.common.mode'] = 'Account mode'; +$labels['form.common.mode.primary'] = 'Primary account'; +$labels['form.common.mode.separate'] = 'Separate account'; +$labels['form.common.mode.hint'] = 'Link this identity to an existing account, or configure it as a separate account with its own mail server.'; // Label $labels['form.common.label'] = 'Label'; diff --git a/localization/fr_FR.inc b/localization/fr_FR.inc index f073a4f..067c130 100644 --- a/localization/fr_FR.inc +++ b/localization/fr_FR.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'La configuration de compte séparé n\ // General $labels['form.common.general'] = 'Général'; -// Enabled -$labels['form.common.enabled'] = 'Activer'; +// Account mode +$labels['form.common.mode'] = 'Mode de compte'; +$labels['form.common.mode.primary'] = 'Compte principal'; +$labels['form.common.mode.separate'] = 'Compte séparé'; +$labels['form.common.mode.hint'] = 'Rattachez cette identité à un compte existant, ou configurez-la comme un compte séparé avec son propre serveur de messagerie.'; // Label $labels['form.common.label'] = 'Nom à afficher'; diff --git a/localization/it_IT.inc b/localization/it_IT.inc index 746c7fb..de6cf69 100644 --- a/localization/it_IT.inc +++ b/localization/it_IT.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'La configurazione come account separat // General $labels['form.common.general'] = 'Generale'; -// Enabled -$labels['form.common.enabled'] = 'Abilita'; +// Account mode +$labels['form.common.mode'] = 'Modalità account'; +$labels['form.common.mode.primary'] = 'Account principale'; +$labels['form.common.mode.separate'] = 'Account separato'; +$labels['form.common.mode.hint'] = 'Collega questa identità a un account esistente o configurala come un account separato con il proprio server di posta.'; // Label $labels['form.common.label'] = 'Nome visualizzato'; diff --git a/localization/nl_NL.inc b/localization/nl_NL.inc index d4732ba..eface7a 100644 --- a/localization/nl_NL.inc +++ b/localization/nl_NL.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'Configuratie als afzonderlijk account // General $labels['form.common.general'] = 'Algemeen'; -// Enabled -$labels['form.common.enabled'] = 'Inschakelen'; +// Account mode +$labels['form.common.mode'] = 'Accountmodus'; +$labels['form.common.mode.primary'] = 'Hoofdaccount'; +$labels['form.common.mode.separate'] = 'Afzonderlijk account'; +$labels['form.common.mode.hint'] = 'Koppel deze identiteit aan een bestaand account of configureer het als een afzonderlijk account met een eigen mailserver.'; // Label $labels['form.common.label'] = 'Label'; diff --git a/localization/ru_RU.inc b/localization/ru_RU.inc index c778140..d2394b5 100644 --- a/localization/ru_RU.inc +++ b/localization/ru_RU.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'Настройка отдельног // General $labels['form.common.general'] = 'Общие'; -// Enabled -$labels['form.common.enabled'] = 'Включено'; +// Account mode +$labels['form.common.mode'] = 'Режим аккаунта'; +$labels['form.common.mode.primary'] = 'Основной аккаунт'; +$labels['form.common.mode.separate'] = 'Отдельный аккаунт'; +$labels['form.common.mode.hint'] = 'Привяжите эту учётную запись к существующему аккаунту или настройте её как отдельный аккаунт с собственным почтовым сервером.'; // Label $labels['form.common.label'] = 'Название'; diff --git a/localization/sl_SI.inc b/localization/sl_SI.inc index b3f7de2..91b4a35 100644 --- a/localization/sl_SI.inc +++ b/localization/sl_SI.inc @@ -12,8 +12,11 @@ $labels['form.preconfig_only_warning'] = 'Konfiguracija ločenega računa za dom // General $labels['form.common.general'] = 'Splošno'; -// Enabled -$labels['form.common.enabled'] = 'Omogočeno'; +// Account mode +$labels['form.common.mode'] = 'Način računa'; +$labels['form.common.mode.primary'] = 'Glavni račun'; +$labels['form.common.mode.separate'] = 'Ločen račun'; +$labels['form.common.mode.hint'] = 'Povežite to identiteto z obstoječim računom ali jo konfigurirajte kot ločen račun z lastnim poštnim strežnikom.'; // Label $labels['form.common.label'] = 'Oznaka';