Add alias SMTP/Sieve resolution, debug logging, and exclude aliases from switcher

SMTP and Sieve hooks follow parent_id to resolve alias config from parent
account. Aliases excluded from account switcher dropdown and background
mail checker. Debug logging behind ident_switch.debug config flag.
Fix double ssl:// prefix in switch_account host parsing.
This commit is contained in:
Laurent Dinclaux
2026-02-10 22:28:56 +11:00
parent e4bd92ca63
commit 08673a4399
4 changed files with 75 additions and 13 deletions

View File

@@ -92,3 +92,10 @@ $config['ident_switch.hide_notifier_warning'] = false;
* Default: false (all domains allowed). * Default: false (all domains allowed).
*/ */
$config['ident_switch.preconfig_only'] = false; $config['ident_switch.preconfig_only'] = false;
/*
* Enable debug logging to logs/ident_switch.log.
* Useful for troubleshooting SMTP/Sieve routing and alias resolution.
* Default: false.
*/
$config['ident_switch.debug'] = false;

View File

@@ -190,7 +190,7 @@ class ident_switch extends rcube_plugin
. " FROM" . " FROM"
. " {$rc->db->table_name(self::TABLE)} isw" . " {$rc->db->table_name(self::TABLE)} isw"
. " INNER JOIN {$rc->db->table_name('identities')} ii ON isw.iid=ii.identity_id" . " INNER JOIN {$rc->db->table_name('identities')} ii ON isw.iid=ii.identity_id"
. " WHERE isw.user_id = ? AND isw.flags & ? > 0"; . " WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.parent_id IS NULL";
$qRec = $rc->db->query($sql, $rc->user->data['user_id'], self::DB_ENABLED); $qRec = $rc->db->query($sql, $rc->user->data['user_id'], self::DB_ENABLED);
while ($r = $rc->db->fetch_assoc($qRec)) { while ($r = $rc->db->fetch_assoc($qRec)) {
$accValues[] = $r['id']; $accValues[] = $r['id'];
@@ -419,4 +419,16 @@ class ident_switch extends rcube_plugin
{ {
rcmail::get_instance()->write_log('ident_switch', $txt); rcmail::get_instance()->write_log('ident_switch', $txt);
} }
/**
* Write a debug message (only when ident_switch.debug is enabled).
*
* @param string $txt Log message.
*/
public static function debug_log(string $txt): void
{
if (rcmail::get_instance()->config->get('ident_switch.debug', false)) {
rcmail::get_instance()->write_log('ident_switch', '[DEBUG] ' . $txt);
}
}
} }

View File

@@ -213,7 +213,7 @@ class IdentSwitchChecker
. 'ii.email ' . 'ii.email '
. 'FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' isw ' . 'FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' isw '
. 'INNER JOIN ' . $rc->db->table_name('identities') . ' ii ON isw.iid = ii.identity_id ' . 'INNER JOIN ' . $rc->db->table_name('identities') . ' ii ON isw.iid = ii.identity_id '
. 'WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.notify_check = ?'; . 'WHERE isw.user_id = ? AND isw.flags & ? > 0 AND isw.notify_check = ? AND isw.parent_id IS NULL';
$q = $rc->db->query($sql, $rc->user->ID, ident_switch::DB_ENABLED, ident_switch::NOTIFY_CHECK_ENABLED); $q = $rc->db->query($sql, $rc->user->ID, ident_switch::DB_ENABLED, ident_switch::NOTIFY_CHECK_ENABLED);

View File

@@ -102,12 +102,13 @@ class IdentSwitchSwitcher
$hostLower = strtolower($host); $hostLower = strtolower($host);
if (str_starts_with($hostLower, 'ssl://')) { if (str_starts_with($hostLower, 'ssl://')) {
$ssl = 'ssl'; $ssl = 'ssl';
$host = substr($host, 6);
} elseif (str_starts_with($hostLower, 'tls://')) { } elseif (str_starts_with($hostLower, 'tls://')) {
$ssl = 'tls'; $ssl = 'tls';
$host = substr($host, 6);
} elseif ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS) { } elseif ($r['flags'] & ident_switch::DB_SECURE_IMAP_TLS) {
// Backward compat: old records without scheme in host // Backward compat: old records without scheme in host
$ssl = 'tls'; $ssl = 'tls';
$host = 'tls://' . $host;
} }
$def_port = ($ssl === 'ssl') ? 993 : 143; $def_port = ($ssl === 'ssl') ? 993 : 143;
@@ -154,26 +155,39 @@ class IdentSwitchSwitcher
{ {
$iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null; $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null;
if (!is_numeric($iid) || (int)$iid === -1) { if (!is_numeric($iid) || (int)$iid === -1) {
ident_switch::write_log('no identity switch is selected... trying to find related smtp server from the from header'); ident_switch::debug_log('SMTP: no active switch, resolving from _from header');
$requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST); $requestFrom = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
if (empty($requestFrom)) { if (empty($requestFrom)) {
ident_switch::write_log('no _from post parameter found... falling back to original default config'); ident_switch::debug_log('SMTP: no _from parameter, using default config');
return $args; return $args;
} }
$iid = intval($requestFrom); $iid = intval($requestFrom);
if ($iid === 0) { if ($iid === 0) {
ident_switch::write_log('falling back to original default config as _from post field is not an integer'); ident_switch::debug_log('SMTP: _from is not an integer, using default config');
return $args; return $args;
} }
} }
$rc = rcmail::get_instance(); $rc = rcmail::get_instance();
$sql = 'SELECT smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; $sql = 'SELECT parent_id, smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
$q = $rc->db->query($sql, $iid, $rc->user->ID); $q = $rc->db->query($sql, $iid, $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (is_array($r)) { if (is_array($r)) {
// If this is an alias, follow parent_id to get the parent's SMTP config
if (!empty($r['parent_id'])) {
ident_switch::debug_log("SMTP: identity {$iid} is alias, following parent_id={$r['parent_id']}");
$sql = 'SELECT smtp_host, smtp_port, username, smtp_auth, smtp_username, smtp_password, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
$q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
$r = $rc->db->fetch_assoc($q);
if (!is_array($r)) {
ident_switch::debug_log("SMTP: parent account not found, using default config");
return $args;
}
$iid = $r['iid'];
}
if (!$r['username']) { if (!$r['username']) {
// Load email from identity // Load email from identity
$sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?';
@@ -182,10 +196,11 @@ class IdentSwitchSwitcher
$r['username'] = $rIid['email']; $r['username'] = $rIid['email'];
} }
if ((int)$r['smtp_auth'] === ident_switch::SMTP_AUTH_CUSTOM) { $authMode = (int)$r['smtp_auth'];
if ($authMode === ident_switch::SMTP_AUTH_CUSTOM) {
$args['smtp_user'] = $r['smtp_username'] ?: ''; $args['smtp_user'] = $r['smtp_username'] ?: '';
$args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : ''; $args['smtp_pass'] = $r['smtp_password'] ? ($rc->decrypt($r['smtp_password']) ?: '') : '';
} elseif ((int)$r['smtp_auth'] === ident_switch::SMTP_AUTH_IMAP) { } elseif ($authMode === ident_switch::SMTP_AUTH_IMAP) {
$args['smtp_user'] = $r['username']; $args['smtp_user'] = $r['username'];
$args['smtp_pass'] = $rc->decrypt($r['password']) ?: ''; $args['smtp_pass'] = $rc->decrypt($r['password']) ?: '';
} else { } else {
@@ -197,6 +212,14 @@ class IdentSwitchSwitcher
$smtpHost = $r['smtp_host'] ?: 'localhost'; $smtpHost = $r['smtp_host'] ?: 'localhost';
$smtpPort = $r['smtp_port'] ?: 587; $smtpPort = $r['smtp_port'] ?: 587;
$args['smtp_host'] = $smtpHost . ':' . $smtpPort; $args['smtp_host'] = $smtpHost . ':' . $smtpPort;
$authLabel = match ($authMode) {
ident_switch::SMTP_AUTH_IMAP => 'imap',
ident_switch::SMTP_AUTH_NONE => 'none',
ident_switch::SMTP_AUTH_CUSTOM => 'custom',
default => "unknown({$authMode})",
};
ident_switch::debug_log("SMTP: iid={$iid}, host={$args['smtp_host']}, user={$args['smtp_user']}, auth={$authLabel}");
} }
return $args; return $args;
@@ -220,10 +243,27 @@ class IdentSwitchSwitcher
$rc = rcmail::get_instance(); $rc = rcmail::get_instance();
$sql = 'SELECT sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; $sql = 'SELECT parent_id, sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?';
$q = $rc->db->query($sql, $iid, $rc->user->ID); $q = $rc->db->query($sql, $iid, $rc->user->ID);
$r = $rc->db->fetch_assoc($q); $r = $rc->db->fetch_assoc($q);
if (is_array($r) && !empty($r['sieve_host'])) { if (is_array($r)) {
// If this is an alias, follow parent_id to get the parent's Sieve config
if (!empty($r['parent_id'])) {
ident_switch::debug_log("Sieve: identity {$iid} is alias, following parent_id={$r['parent_id']}");
$sql = 'SELECT sieve_host, sieve_port, sieve_auth, sieve_username, sieve_password, username, password, iid FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE id = ? AND user_id = ?';
$q = $rc->db->query($sql, $r['parent_id'], $rc->user->ID);
$r = $rc->db->fetch_assoc($q);
if (!is_array($r)) {
ident_switch::debug_log("Sieve: parent account not found, using default config");
return $args;
}
$iid = $r['iid'];
}
if (empty($r['sieve_host'])) {
return $args;
}
if (!$r['username']) { if (!$r['username']) {
$sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?';
$q = $rc->db->query($sql, $iid); $q = $rc->db->query($sql, $iid);
@@ -235,16 +275,19 @@ class IdentSwitchSwitcher
$sievePort = $r['sieve_port'] ?: 4190; $sievePort = $r['sieve_port'] ?: 4190;
$args['host'] = $sieveHost . ':' . $sievePort; $args['host'] = $sieveHost . ':' . $sievePort;
if ((int)$r['sieve_auth'] === ident_switch::SIEVE_AUTH_CUSTOM) { $authMode = (int)$r['sieve_auth'];
if ($authMode === ident_switch::SIEVE_AUTH_CUSTOM) {
$args['user'] = $r['sieve_username'] ?: ''; $args['user'] = $r['sieve_username'] ?: '';
$args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : ''; $args['password'] = $r['sieve_password'] ? ($rc->decrypt($r['sieve_password']) ?: '') : '';
} elseif ((int)$r['sieve_auth'] === ident_switch::SIEVE_AUTH_IMAP) { } elseif ($authMode === ident_switch::SIEVE_AUTH_IMAP) {
$args['user'] = $r['username']; $args['user'] = $r['username'];
$args['password'] = $rc->decrypt($r['password']) ?: ''; $args['password'] = $rc->decrypt($r['password']) ?: '';
} else { } else {
$args['user'] = ''; $args['user'] = '';
$args['password'] = ''; $args['password'] = '';
} }
ident_switch::debug_log("Sieve: iid={$iid}, host={$args['host']}, user={$args['user']}");
} }
return $args; return $args;