| SYSTEM INFORMATION | |
|---|---|
| OS | Ubuntu 20.04.6 LTS |
| Webmin | 2.660 |
| Virtualmin | 8.2.0.gpl-1 |
| Mail system | Postfix (mail_system=0) |
modify-user silently drops all but one local forwarding destination
Summary
Any virtualmin modify-user call on a mailbox that forwards to more than one local
mailbox discards all but the last of those destinations — permanently, with no error and
no warning.
This happens on every modify-user invocation for such a user, including ones that have
nothing to do with mail routing (--pass, --real-name, --quota). The alias block in
modify-user.pl is entered unconditionally whenever the user has an email address, and it
rewrites the user’s forwarding list from a model that can only hold one local destination.
The equivalent operation in the web UI is not affected — it passes an argument the CLI
does not (details below).
Steps to reproduce
-
Create a domain
example.comwith three mailboxes:alice,bobandarchive. -
Create a fourth mailbox
sales@example.com, and set it to forward to both
alice@example.comandarchive@example.comas local destinations (Edit Users →
Sales → Forward to other addresses, using the local mailbox names so Virtualmin
records them as local delivery rather than as external forwards). -
Confirm the routing is in place:
# virtualmin list-users --domain example.com --user sales --simple-aliases --multiline -
Now change something unrelated to mail routing:
# virtualmin modify-user --domain example.com --user sales --real-name "Sales Desk" -
Re-read the user’s forwarding.
Expected
sales@example.com still forwards to both alice@example.com and archive@example.com.
The command changed only the real name.
Actual
sales@example.com now forwards to archive@example.com only. alice@example.com has
been removed from the alias. The command exited 0 and printed no warning.
Mail addressed to sales@ stops reaching alice@ from that moment on. Because the change
is silent and the trigger is an unrelated field, there is nothing to connect the missing
mail to the command that caused it.
Root cause
simple-lib.pl, get_simple_alias() — local delivery destinations (atype == 7) are
stored in a scalar, so each one overwrites the previous:
elsif ($atype == 7) {
# Local delivery
$simple->{'local'} = $aval;
push(@{$simple->{'local-all'}}, $aval)
if ($allow_merge &&
$mail_system == 0 &&
$aval =~ /\@/ &&
getpwnam(&unescape_user($aval)));
}
The full set is preserved in local-all, but only when the caller passes the third
argument, allow-merging-local.
save_simple_alias() then rebuilds $alias->{'to'} from that model and emits the single
scalar:
if ($simple->{'local'} && ...) {
push(@v, "\\".$escuser);
}
modify-user.pl:534 calls get_simple_alias with two arguments, so $allow_merge is
false and local-all is never populated:
$simple = @{$user->{'to'}} ? &get_simple_alias($d, $user)
: { 'tome' => 1 };
…and reaches &save_simple_alias($d, $user, $simple) unconditionally — the enclosing
condition is only if (!$user->{'noalias'} && ($user->{'email'} || $user->{'noprimary'})),
which is true for any ordinary mailbox regardless of which flags were passed.
By contrast edit_user.cgi:373 and save_user.cgi:465,640 all pass the third argument:
$simple = &get_simple_alias($d, $user, 1);
which is why the web UI round-trips these users correctly and the CLI does not.
Impact
On one production mail server, 6 of 304 mailboxes hold more than one local destination and
would lose a recipient on any modify-user call. They are ordinary staff mailboxes
forwarding a copy to a shared archive mailbox — a common enough pattern that we expect
this is not unique to our fleet.
The severity comes from the trigger: a routine password change made through an automated
tool or a self-service panel would silently break a third party’s mail delivery, with no
signal to the operator, the mailbox owner, or the recipient who stops receiving mail.
Suggested fix
Pass the merge flag in modify-user.pl, matching the CGI callers:
$simple = @{$user->{'to'}} ? &get_simple_alias($d, $user, 1)
: { 'tome' => 1 };
Failing that, modify-user should refuse to proceed when get_simple_alias would collapse
multiple local destinations, rather than rewriting the alias and exiting 0.
Related read-side issue
list-users.pl prints Deliver to user and Forward, but has no output for
$simple->{'local'}. A mailbox whose only routing is local delivery to another mailbox
therefore reports:
Deliver to user: No
with no Forward lines at all — indistinguishable from a mailbox that discards its mail.
On the same server, 17 mailboxes report this way while delivering normally. Any tool built
on list-users output will misreport them.