1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-06-13 01:50:34 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] d5524689b4 Improve authentication failure detection with word boundaries in regex
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
2025-12-15 08:16:49 +00:00
copilot-swe-agent[bot] 4e12ba99a0 Fix sync-jobs with legacy-encoded passwords by implementing UTF-8/Latin-1 retry logic
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
2025-12-15 08:15:52 +00:00
copilot-swe-agent[bot] 9b1540697f Initial plan 2025-12-15 08:11:12 +00:00
9 changed files with 76 additions and 209 deletions
-19
View File
@@ -246,25 +246,6 @@ while true; do
done
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
done
# Fetch alias domains where target domain has MTA-STS enabled
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
if [[ $? -eq 0 ]]; then
while read alias_domain; do
if [[ -z "${alias_domain}" ]]; then
# ignore empty lines
continue
fi
# Only add mta-sts subdomain for alias domains
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "mta-sts.${alias_domain}"; then
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
fi
fi
done <<< "${SQL_ALIAS_DOMAINS}"
fi
fi
fi
if check_domain ${MAILCOW_HOSTNAME}; then
+58 -32
View File
@@ -117,51 +117,77 @@ while ($row = $sth->fetchrow_arrayref()) {
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
my $template = $run_dir . '/imapsync.XXXXXXX';
my $passfile1 = File::Temp->new(TEMPLATE => $template);
my $passfile2 = File::Temp->new(TEMPLATE => $template);
binmode( $passfile1, ":utf8" );
print $passfile1 "$password1\n";
print $passfile2 trim($master_pass) . "\n";
my @custom_params_a = qqw($custom_params);
my $custom_params_ref = \@custom_params_a;
my $generated_cmds = [ "/usr/local/bin/imapsync",
"--tmpdir", "/tmp",
"--nofoldersizes",
"--addheader",
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
($exclude eq "" ? () : ("--exclude", $exclude)),
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
($maxage eq "0" ? () : ('--maxage', $maxage)),
($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
($subscribeall ne "1" ? () : ('--subscribeall')),
($delete1 ne "1" ? () : ('--delete')),
($delete2 ne "1" ? () : ('--delete2')),
($automap ne "1" ? () : ('--automap')),
($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')),
(!defined($enc1) ? () : ($enc1)),
"--host1", $host1,
"--user1", $user1,
"--passfile1", $passfile1->filename,
"--port1", $port1,
"--host2", "localhost",
"--user2", $user2 . '*' . trim($master_user),
"--passfile2", $passfile2->filename,
($dry eq "1" ? ('--dry') : ()),
'--no-modulesversion',
'--noreleasecheck'];
# Helper function to run imapsync with a specific encoding for password1
my $run_imapsync = sub {
my ($encoding) = @_;
my $passfile1 = File::Temp->new(TEMPLATE => $template);
if ($encoding eq 'utf8') {
binmode( $passfile1, ":utf8" );
} elsif ($encoding eq 'latin1') {
binmode( $passfile1, ":encoding(iso-8859-1)" );
}
print $passfile1 "$password1\n";
$passfile1->flush();
my $generated_cmds = [ "/usr/local/bin/imapsync",
"--tmpdir", "/tmp",
"--nofoldersizes",
"--addheader",
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
($exclude eq "" ? () : ("--exclude", $exclude)),
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
($maxage eq "0" ? () : ('--maxage', $maxage)),
($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
($subscribeall ne "1" ? () : ('--subscribeall')),
($delete1 ne "1" ? () : ('--delete')),
($delete2 ne "1" ? () : ('--delete2')),
($automap ne "1" ? () : ('--automap')),
($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')),
(!defined($enc1) ? () : ($enc1)),
"--host1", $host1,
"--user1", $user1,
"--passfile1", $passfile1->filename,
"--port1", $port1,
"--host2", "localhost",
"--user2", $user2 . '*' . trim($master_user),
"--passfile2", $passfile2->filename,
($dry eq "1" ? ('--dry') : ()),
'--no-modulesversion',
'--noreleasecheck'];
my $stdout;
run [@$generated_cmds, @$custom_params_ref], '&>', \$stdout;
return $stdout;
};
try {
$is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?");
$is_running->bind_param( 1, ${id} );
$is_running->execute();
run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout;
# First attempt with UTF-8 encoding (default behavior)
my $stdout = $run_imapsync->('utf8');
# Check if authentication failed
my $auth_failed = ($stdout =~ /\b(LOGIN failed|authentication failed|AUTHENTICATIONFAILED)\b/i);
# If authentication failed with UTF-8, retry with Latin-1 encoding for legacy passwords
if ($auth_failed) {
$stdout = $run_imapsync->('latin1');
}
# check exit code and status
($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/);
+1 -10
View File
@@ -129,16 +129,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
);
}
// Check if domain is an alias domain and get target domain's MTA-STS
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
$mta_sts_domain = $domain;
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
// This is an alias domain, check target domain for MTA-STS
$mta_sts_domain = $alias_domain_details['target_domain'];
}
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
$mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) {
$records[] = array(
+15 -37
View File
@@ -664,18 +664,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
dkim('add', array('key_size' => $_data['key_size'], 'dkim_selector' => $_data['dkim_selector'], 'domains' => $domain));
}
}
// Create MTA-STS settings from template if enabled
if (!empty($DOMAIN_DEFAULT_ATTRIBUTES['mta_sts']) && $DOMAIN_DEFAULT_ATTRIBUTES['mta_sts'] == 1) {
$mta_sts_data = array(
'domain' => $domain,
'version' => $DOMAIN_DEFAULT_ATTRIBUTES['mta_sts_version'],
'mode' => $DOMAIN_DEFAULT_ATTRIBUTES['mta_sts_mode'],
'max_age' => $DOMAIN_DEFAULT_ATTRIBUTES['mta_sts_max_age'],
'mx' => $DOMAIN_DEFAULT_ATTRIBUTES['mta_sts_mx'],
'active' => 1
);
mailbox('add', 'mta_sts', $mta_sts_data);
}
if (!empty($restart_sogo)) {
$restart_response = json_decode(docker('post', 'sogo-mailcow', 'restart'), true);
if ($restart_response['type'] == "success") {
@@ -1660,11 +1648,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['relay_unknown_only'] = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : 0;
$attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim";
$attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : 2048;
$attr['mta_sts'] = (isset($_data['mta_sts'])) ? intval($_data['mta_sts']) : 0;
$attr['mta_sts_version'] = (isset($_data['mta_sts_version'])) ? $_data['mta_sts_version'] : 'stsv1';
$attr['mta_sts_mode'] = (isset($_data['mta_sts_mode'])) ? $_data['mta_sts_mode'] : 'enforce';
$attr['mta_sts_max_age'] = (isset($_data['mta_sts_max_age'])) ? intval($_data['mta_sts_max_age']) : 604800;
$attr['mta_sts_mx'] = (isset($_data['mta_sts_mx'])) ? $_data['mta_sts_mx'] : '';
// save template
$stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`)
@@ -3001,26 +2984,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
// check attributes
$attr = array();
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : (isset($is_now['attributes']['tags']) ? $is_now['attributes']['tags'] : array());
$attr['max_num_aliases_for_domain'] = (isset($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : (isset($is_now['attributes']['max_num_aliases_for_domain']) ? $is_now['attributes']['max_num_aliases_for_domain'] : 0);
$attr['max_num_mboxes_for_domain'] = (isset($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : (isset($is_now['attributes']['max_num_mboxes_for_domain']) ? $is_now['attributes']['max_num_mboxes_for_domain'] : 0);
$attr['def_quota_for_mbox'] = (isset($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : (isset($is_now['attributes']['def_quota_for_mbox']) ? $is_now['attributes']['def_quota_for_mbox'] : 0);
$attr['max_quota_for_mbox'] = (isset($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : (isset($is_now['attributes']['max_quota_for_mbox']) ? $is_now['attributes']['max_quota_for_mbox'] : 0);
$attr['max_quota_for_domain'] = (isset($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : (isset($is_now['attributes']['max_quota_for_domain']) ? $is_now['attributes']['max_quota_for_domain'] : 0);
$attr['rl_frame'] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : (isset($is_now['attributes']['rl_frame']) ? $is_now['attributes']['rl_frame'] : "s");
$attr['rl_value'] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : (isset($is_now['attributes']['rl_value']) ? $is_now['attributes']['rl_value'] : "");
$attr['active'] = isset($_data['active']) ? intval($_data['active']) : (isset($is_now['attributes']['active']) ? $is_now['attributes']['active'] : 1);
$attr['gal'] = (isset($_data['gal'])) ? intval($_data['gal']) : (isset($is_now['attributes']['gal']) ? $is_now['attributes']['gal'] : 1);
$attr['backupmx'] = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : (isset($is_now['attributes']['backupmx']) ? $is_now['attributes']['backupmx'] : 0);
$attr['relay_all_recipients'] = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : (isset($is_now['attributes']['relay_all_recipients']) ? $is_now['attributes']['relay_all_recipients'] : 0);
$attr['relay_unknown_only'] = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : (isset($is_now['attributes']['relay_unknown_only']) ? $is_now['attributes']['relay_unknown_only'] : 0);
$attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : (isset($is_now['attributes']['dkim_selector']) ? $is_now['attributes']['dkim_selector'] : "dkim");
$attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : (isset($is_now['attributes']['key_size']) ? $is_now['attributes']['key_size'] : 2048);
$attr['mta_sts'] = (isset($_data['mta_sts'])) ? intval($_data['mta_sts']) : (isset($is_now['attributes']['mta_sts']) ? $is_now['attributes']['mta_sts'] : 0);
$attr['mta_sts_version'] = (isset($_data['mta_sts_version'])) ? $_data['mta_sts_version'] : (isset($is_now['attributes']['mta_sts_version']) ? $is_now['attributes']['mta_sts_version'] : 'stsv1');
$attr['mta_sts_mode'] = (isset($_data['mta_sts_mode'])) ? $_data['mta_sts_mode'] : (isset($is_now['attributes']['mta_sts_mode']) ? $is_now['attributes']['mta_sts_mode'] : 'enforce');
$attr['mta_sts_max_age'] = (isset($_data['mta_sts_max_age'])) ? intval($_data['mta_sts_max_age']) : (isset($is_now['attributes']['mta_sts_max_age']) ? $is_now['attributes']['mta_sts_max_age'] : 604800);
$attr['mta_sts_mx'] = (isset($_data['mta_sts_mx'])) ? $_data['mta_sts_mx'] : (isset($is_now['attributes']['mta_sts_mx']) ? $is_now['attributes']['mta_sts_mx'] : '');
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array();
$attr['max_num_aliases_for_domain'] = (isset($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 0;
$attr['max_num_mboxes_for_domain'] = (isset($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 0;
$attr['def_quota_for_mbox'] = (isset($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 0;
$attr['max_quota_for_mbox'] = (isset($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 0;
$attr['max_quota_for_domain'] = (isset($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 0;
$attr['rl_frame'] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
$attr['rl_value'] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
$attr['active'] = isset($_data['active']) ? intval($_data['active']) : 1;
$attr['gal'] = (isset($_data['gal'])) ? intval($_data['gal']) : 1;
$attr['backupmx'] = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : 0;
$attr['relay_all_recipients'] = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : 0;
$attr['relay_unknown_only'] = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : 0;
$attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim";
$attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : 2048;
// update template
$stmt = $pdo->prepare("UPDATE `templates`
-2
View File
@@ -723,8 +723,6 @@
"mta_sts_mx": "MX server",
"mta_sts_mx_info": "Allows sending only to explicitly listed mail server hostnames; the sending MTA checks if the DNS MX hostname matches the policy list, and only allows delivery with a valid TLS certificate (guards against MITM).",
"mta_sts_mx_notice": "Multiple MX servers can be specified (separated by commas).",
"mta_sts_enable": "Enable MTA-STS",
"mta_sts_template_info": "When enabled, MTA-STS will be automatically configured for all domains created with this template.",
"multiple_bookings": "Multiple bookings",
"none_inherit": "None / Inherit",
"nexthop": "Next hop",
+1 -24
View File
@@ -7,30 +7,7 @@ if (!isset($_SERVER['HTTP_HOST']) || strpos($_SERVER['HTTP_HOST'], 'mta-sts.') !
}
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
$domain = idn_to_ascii(strtolower(str_replace('mta-sts.', '', $host)), 0, INTL_IDNA_VARIANT_UTS46);
// Validate domain or return 404 on error
if ($domain === false || empty($domain)) {
http_response_code(404);
exit;
}
// Check if domain is an alias domain and resolve to target domain
try {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$alias_row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($alias_row !== false && !empty($alias_row['target_domain'])) {
// This is an alias domain, use the target domain for MTA-STS lookup
$domain = $alias_row['target_domain'];
}
} catch (PDOException $e) {
// On database error, return 404
http_response_code(404);
exit;
}
$domain = str_replace('mta-sts.', '', $host);
$mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) == 0 ||
@@ -10,7 +10,6 @@
<input type="hidden" value="0" name="gal">
<input type="hidden" value="0" name="relay_all_recipients">
<input type="hidden" value="0" name="relay_unknown_only">
<input type="hidden" value="0" name="mta_sts">
{% if mailcow_cc_role == 'admin' %}
<div class="row mb-4">
@@ -125,47 +124,6 @@
</div>
</div>
<hr>
<div class="row mb-4">
<label class="control-label col-sm-2">{{ lang.edit.mta_sts }}</label>
<div class="col-sm-10">
<div class="form-check mb-3">
<label><input type="checkbox" class="form-check-input" value="1" name="mta_sts"{% if template.attributes.mta_sts == '1' %} checked{% endif %}> {{ lang.edit.mta_sts_enable }}</label>
<p><small class="text-muted">{{ lang.edit.mta_sts_template_info|raw }}</small></p>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3" for="mta_sts_version">{{ lang.edit.mta_sts_version }}</label>
<div class="col-sm-9">
<select data-style="btn btn-light" class="form-control" name="mta_sts_version">
<option value="stsv1"{% if template.attributes.mta_sts_version == 'stsv1' %} selected{% endif %}>STSv1</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3" for="mta_sts_mode">{{ lang.edit.mta_sts_mode }}</label>
<div class="col-sm-9">
<select data-style="btn btn-light" class="form-control" name="mta_sts_mode">
<option value="enforce"{% if template.attributes.mta_sts_mode == 'enforce' %} selected{% endif %}>enforce</option>
<option value="testing"{% if template.attributes.mta_sts_mode == 'testing' %} selected{% endif %}>testing</option>
<option value="none"{% if template.attributes.mta_sts_mode == 'none' %} selected{% endif %}>none</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3" for="mta_sts_max_age">{{ lang.edit.mta_sts_max_age }}</label>
<div class="col-sm-9">
<input type="number" class="form-control" name="mta_sts_max_age" value="{{ template.attributes.mta_sts_max_age|default('604800') }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3" for="mta_sts_mx">{{ lang.edit.mta_sts_mx }}</label>
<div class="col-sm-9">
<textarea autocorrect="off" autocapitalize="none" class="form-control" rows="3" name="mta_sts_mx">{{ template.attributes.mta_sts_mx }}</textarea>
<small class="text-muted">{{ lang.edit.mta_sts_mx_notice|raw }}</small>
</div>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="editdomain_template" data-item="{{ template.id }}" data-api-url='edit/domain/template' data-api-attr='{}' href="#">{{ lang.admin.save }}</button>
-42
View File
@@ -688,48 +688,6 @@
</div>
</div>
<hr>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end">{{ lang.edit.mta_sts }}</label>
<div class="col-sm-10">
<div class="form-check mb-3">
<input type="hidden" value="0" name="mta_sts">
<label><input type="checkbox" class="form-check-input" value="1" name="mta_sts"> {{ lang.edit.mta_sts_enable }}</label>
<p><small class="text-muted">{{ lang.edit.mta_sts_template_info|raw }}</small></p>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3 text-sm-end" for="mta_sts_version">{{ lang.edit.mta_sts_version }}</label>
<div class="col-sm-9">
<select data-style="btn btn-light" class="form-control" name="mta_sts_version">
<option value="stsv1" selected>STSv1</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3 text-sm-end" for="mta_sts_mode">{{ lang.edit.mta_sts_mode }}</label>
<div class="col-sm-9">
<select data-style="btn btn-light" class="form-control" name="mta_sts_mode">
<option value="enforce" selected>enforce</option>
<option value="testing">testing</option>
<option value="none">none</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3 text-sm-end" for="mta_sts_max_age">{{ lang.edit.mta_sts_max_age }}</label>
<div class="col-sm-9">
<input type="number" class="form-control" name="mta_sts_max_age" value="604800">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-3 text-sm-end" for="mta_sts_mx">{{ lang.edit.mta_sts_mx }}</label>
<div class="col-sm-9">
<textarea autocorrect="off" autocapitalize="none" class="form-control" rows="3" name="mta_sts_mx"></textarea>
<small class="text-muted">{{ lang.edit.mta_sts_mx_notice|raw }}</small>
</div>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="add_item" data-id="adddomain_template" data-item="{{ domain }}" data-api-url='add/domain/template' data-api-attr='{}' href="#">{{ lang.admin.add }}</button>
+1 -1
View File
@@ -465,7 +465,7 @@ services:
condition: service_started
unbound-mailcow:
condition: service_healthy
image: ghcr.io/mailcow/acme:1.95
image: ghcr.io/mailcow/acme:1.94
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment: