Add custom auth, connection testing, preconfig_only, and security fixes

Major form improvements:
- Custom SMTP/Sieve credentials (separate username/password per protocol)
- Connection testing on save (IMAP, SMTP, Sieve) with localized errors
- preconfig_only mode to restrict domains to preconfigured entries
- Form POST value preservation on save errors (auth selects, passwords)
- Smart host placeholders (SMTP/Sieve default to IMAP host)

Security and bug:
- Fix password re-encryption bug (was comparing raw vs encrypted values)
- Fix XSS: escape label output in special folders form
- Fix parse_url() return value not checked for false
- Fix decrypt() failures not handled (fallback to empty string)
- Sanitize log output (remove raw POST data from log messages)
- Replace weak == comparisons with strict === (PHP and JS)

SQL changes:
- Consolidate 4 migrations (2026021000-03) into single 2026021000
- Remove now unused notify_sound_url column
- Add smtp_username, smtp_password, sieve_username, sieve_password columns
This commit is contained in:
Laurent Dinclaux
2026-02-10 20:48:49 +11:00
parent 3a8202bd7a
commit fd9836c7ae
25 changed files with 1042 additions and 209 deletions

View File

@@ -43,6 +43,23 @@ $(function() {
});
});
// IMAP host → update SMTP/Sieve host placeholders
$("INPUT[name='_ident_switch.form.imap.host']").on('input change blur', function() {
var imapHost = $(this).val() || 'localhost';
$("INPUT[name='_ident_switch.form.smtp.host']").attr('placeholder', imapHost);
$("INPUT[name='_ident_switch.form.sieve.host']").attr('placeholder', imapHost);
});
// Bind auth change handlers for SMTP and Sieve custom credentials
$.each(['smtp', 'sieve'], function(i, proto) {
var authSel = "SELECT[name='_ident_switch.form." + proto + ".auth']";
$(authSel).on('change', function() {
plugin_switchIdent_onAuthChange(proto, $(this).val());
});
// Apply initial visibility
plugin_switchIdent_onAuthChange(proto, $(authSel).val());
});
// Delimiter mode handler
$("SELECT[name='_ident_switch.form.imap.delimiter_mode']").on('change', function() {
if ($(this).val() === 'manual') {
@@ -52,6 +69,22 @@ $(function() {
$("INPUT[name='_ident_switch.form.imap.delimiter']").val('');
}
});
// Watch email field for dynamic preconfig application
$("INPUT[name='_email']").on('change blur', function() {
plugin_switchIdent_onEmailChange($(this).val());
});
// Set initial placeholders from current field values
var initialEmail = $("INPUT[name='_email']").val();
if (initialEmail) {
$("INPUT[name='_ident_switch.form.common.label']").attr('placeholder', initialEmail);
}
var initialImapHost = $("INPUT[name='_ident_switch.form.imap.host']").val();
if (initialImapHost) {
$("INPUT[name='_ident_switch.form.smtp.host']").attr('placeholder', initialImapHost);
$("INPUT[name='_ident_switch.form.sieve.host']").attr('placeholder', initialImapHost);
}
});
/**
@@ -104,11 +137,29 @@ function plugin_switchIdent_clearIfDefault($field) {
}
}
/**
* Handle auth dropdown change: show/hide custom credential fields.
* @param {string} proto - Protocol name (smtp, sieve).
* @param {string} authVal - Selected auth value.
*/
function plugin_switchIdent_onAuthChange(proto, authVal) {
var isCustom = (authVal === '3'); // SMTP_AUTH_CUSTOM / SIEVE_AUTH_CUSTOM
var userFld = $("INPUT[name='_ident_switch.form." + proto + ".username']");
var passFld = $("INPUT[name='_ident_switch.form." + proto + ".password']");
if (isCustom) {
userFld.parentsUntil("TABLE", "TR").show();
passFld.parentsUntil("TABLE", "TR").show();
} else {
userFld.parentsUntil("TABLE", "TR").hide();
passFld.parentsUntil("TABLE", "TR").hide();
}
}
function plugin_switchIdent_processPreconfig() {
var disFld = $("INPUT[name='_ident_switch.form.common.readonly']");
disFld.parentsUntil("TABLE", "TR").hide();
var disVal = disFld.val();
var disVal = parseInt(disFld.val(), 10) || 0;
if (disVal > 0) {
$("INPUT[name='_ident_switch.form.imap.host']").prop("disabled", true);
$("SELECT[name='_ident_switch.form.imap.security']").prop("disabled", true);
@@ -117,6 +168,9 @@ function plugin_switchIdent_processPreconfig() {
$("INPUT[name='_ident_switch.form.smtp.host']").prop("disabled", true);
$("SELECT[name='_ident_switch.form.smtp.security']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.smtp.port']").prop("disabled", true);
$("SELECT[name='_ident_switch.form.smtp.auth']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.smtp.username']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.smtp.password']").prop("disabled", true);
$("SELECT[name='_ident_switch.form.imap.delimiter_mode']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.imap.delimiter']").prop("disabled", true);
@@ -124,8 +178,11 @@ function plugin_switchIdent_processPreconfig() {
$("INPUT[name='_ident_switch.form.sieve.host']").prop("disabled", true);
$("SELECT[name='_ident_switch.form.sieve.security']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.sieve.port']").prop("disabled", true);
$("SELECT[name='_ident_switch.form.sieve.auth']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.sieve.username']").prop("disabled", true);
$("INPUT[name='_ident_switch.form.sieve.password']").prop("disabled", true);
}
if (2 == disVal) {
if (disVal === 2) {
$("INPUT[name='_ident_switch.form.imap.username']").prop("disabled", true);
}
}
@@ -138,3 +195,93 @@ function plugin_switchIdent_enabled_onChange(e) {
$("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.
*/
function plugin_switchIdent_onEmailChange(email) {
var atPos = email.indexOf('@');
if (atPos < 0) return;
var domain = email.substring(atPos + 1).toLowerCase();
if (!domain) return;
// Update label and username placeholders to match current email
$("INPUT[name='_ident_switch.form.common.label']").attr('placeholder', email);
var preconfig = rcmail.env.ident_switch_preconfig || {};
var preconfigOnly = rcmail.env.ident_switch_preconfig_only || false;
var cfg = preconfig[domain] || preconfig['*'] || null;
// Update username placeholder to match current email
$("INPUT[name='_ident_switch.form.imap.username']").attr('placeholder', email);
// Show/hide domain warning
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();
return;
}
$('#ident-switch-domain-warning').hide();
$("INPUT[name='_ident_switch.form.common.enabled']").prop('disabled', false);
// Only auto-fill for identities without an existing DB record
if (!rcmail.env.ident_switch_has_record && cfg) {
plugin_switchIdent_applyJsPreconfig(cfg, email);
}
}
/**
* Apply preconfig values from JS environment to the form fields.
* @param {object} cfg - Preconfig entry for the matched domain.
* @param {string} email - The full email address.
*/
function plugin_switchIdent_applyJsPreconfig(cfg, email) {
// Apply protocol settings
$.each(['imap', 'smtp', 'sieve'], function(_, proto) {
if (!cfg[proto]) return;
// Set security first (updates port placeholder)
var security = cfg[proto].security || '';
$("SELECT[name='_ident_switch.form." + proto + ".security']").val(security);
plugin_switchIdent_onSecurityChange(proto, security);
// Set host
$("INPUT[name='_ident_switch.form." + proto + ".host']").val(cfg[proto].host || '');
// Set port (empty if matches default, so placeholder shows)
var port = cfg[proto].port;
var defaultPort = ident_switch_portDefaults[proto][security] || '';
$("INPUT[name='_ident_switch.form." + proto + ".port']").val(
(port && port != defaultPort) ? port : ''
);
});
// Apply username from preconfig user mode
if (cfg.user) {
var username = '';
if (cfg.user.toUpperCase() === 'EMAIL') {
username = email;
} else if (cfg.user.toUpperCase() === 'MBOX') {
username = email.split('@')[0];
}
if (username) {
$("INPUT[name='_ident_switch.form.imap.username']").val(username);
}
}
// Apply readonly level
var readonlyLevel = 0;
if (cfg.readonly) {
var hasUser = cfg.user && ['EMAIL', 'MBOX'].indexOf(cfg.user.toUpperCase()) >= 0;
readonlyLevel = hasUser ? 2 : 1;
}
$("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();
}