Files
webmin/mailboxes/mailboxes-lib.pl
Ilia Ross c68d03b211 Fix stale mailbox entries after deleted or moved
Refresh stale Maildir and sorted mailbox indexes when messages disappear, avoid rendering missing messages, and keep IMAP sort indexes in sync with mailbox count changes.
2026-05-29 21:12:58 +02:00

1296 lines
34 KiB
Perl
Executable File

# mailboxes-lib.pl
# Common functions for reading user mailboxes
BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
do "$module_root_directory/boxes-lib.pl";
do "$module_root_directory/folders-lib.pl";
%access = &get_module_acl();
$config{'perpage'} ||= 20;
$config{'column_count'} ||= 4;
$no_log_file_changes = 1; # Turn off file change logging for this module
@mail_system_modules = (
[ undef, 4, \&check_qmail_ldap, \&test_qmail_ldap ],
[ undef, 5, \&check_vpopmail ],
[ "qmailadmin", 2 ],
[ "postfix", 0 ],
[ "sendmail", 1 ],
[undef, 6, \&check_exim ]
);
@ignore_users_list = &split_quoted_string($config{'ignore_users'});
# Always detect the mail system if not set
if ($config{'mail_system'} == 3) {
$config{'mail_system'} = &detect_mail_system();
&save_module_config() if ($config{'mail_system'} != 3);
}
# send_mail_program(from, &dests)
# Returns the command for injecting email, based on the mail system in use
sub send_mail_program
{
my ($from, $dests) = @_;
my $qdests = join(" ", map { quotemeta($_) } @$dests);
if ($config{'mail_system'} == 1 || $config{'mail_system'} == 0) {
# Use sendmail executable, or postfix wrapper
local %sconfig = &foreign_check("sendmail") &&
$config{'mail_system'} == 1 ?
&foreign_config("sendmail") : ( );
local $cmd = &has_command($sconfig{'sendmail_path'} || "sendmail");
return "$cmd -f".quotemeta($_[0])." ".$qdests if ($cmd);
}
elsif ($config{'mail_system'} == 2) {
# Use qmail injector
local %qconfig = &foreign_check("qmailadmin") ?
&foreign_config("qmailadmin") : ( );
local $cmd = ($qconfig{'qmail_dir'} || "/var/qmail").
"/bin/qmail-inject";
return $cmd if (-x $cmd);
}
else {
# Fallback - use sendmail command
local $cmd = &has_command("sendmail");
return "$cmd -f".quotemeta($_[0])." ".$qdests if ($cmd);
}
return undef;
}
# list_folders()
# Returns a list of all mailboxes for all users
sub list_folders
{
local (@rv, $uinfo);
foreach $uinfo (&list_mail_users()) {
push(@rv, &list_user_folders(@$uinfo));
}
return @rv;
}
# list_user_folders(user, [other info])
# Returns a list of folders for mailboxes belonging to some user
sub list_user_folders
{
if ($_[0] =~ /^\//) {
# A path .. return a folder just for it
return ( { 'name' => $_[0],
'file' => $_[0],
'type' => &folder_type($_[0]),
'mode' => 1,
'index' => 0 } );
}
else {
# Getting folders for a user
local @uinfo = @_ > 1 ? @_ : &get_mail_user($_[0]);
return ( ) if (!@uinfo);
local ($dir, $style, $mailbox, $maildir) = &get_mail_style();
local @rv;
# Check for user-specified mail store
if ($uinfo[10]) {
push(@rv, { 'name' => $uinfo[10],
'file' => $uinfo[10],
'type' => &folder_type($uinfo[10]),
'mode' => 0,
'index' => scalar(@rv) } );
}
# Check for /var/mail/USERNAME file or directory
if ($dir) {
$dir =~ s/\/$//; # Trailing / means maildir format.
# Postfix sometimes does this.
local $mf = &mail_file_style($uinfo[0], $dir, $style);
push(@rv, { 'type' => -d $mf ? 1 : 0,
'name' => $mf,
'file' => $mf,
'user' => $uinfo[0],
'index' => scalar(@rv) });
}
# Check for file in home dir
if ($mailbox) {
local $mf = "$uinfo[7]/$mailbox";
if (-r $mf || !@rv) {
push(@rv, { 'type' => 0,
'name' => "~$uinfo[0]/$mailbox",
'file' => $mf,
'user' => $uinfo[0],
'index' => scalar(@rv) });
}
}
# Check for directory in home dir
if ($maildir) {
local $mf = "$uinfo[7]/$maildir";
if (-d $mf || !@rv) {
push(@rv, { 'type' => 1,
'name' => "~$uinfo[0]/$maildir/",
'file' => $mf,
'user' => $uinfo[0],
'index' => scalar(@rv) });
}
}
# Add any ~/mail files
if ($config{'mail_usermin'}) {
local $folders_dir = "$uinfo[7]/$config{'mail_usermin'}";
foreach $p (&recursive_files($folders_dir, 1)) {
local $f = $p;
$f =~ s/^\Q$folders_dir\E\///;
push(@rv, { 'file' => $p,
'name' => "~$uinfo[0]/$config{'mail_usermin'}/$f",
'type' => &folder_type($p),
'mode' => 0,
'sent' => $f eq "sentmail",
'index' => scalar(@rv) } );
# Work out if this is a spam folder
if (lc($f) eq "spam" || lc($f) eq ".spam") {
# Has spam in the name
$rv[$#rv]->{'spam'} = 1;
}
elsif (&foreign_check("virtual-server")) {
# Check if Virtualmin is defaulting to it
local %vconfig = &foreign_config(
"virtual-server");
local $sf = $vconfig{'spam_delivery'};
if ($sf) {
$sf =~ s/\$HOME/$uinfo[7]/g;
$sf =~ s/\~/$uinfo[7]/g;
if ($sf !~ /^\//) {
$sf = $uinfo[7]."/".$sf;
}
$sf =~ s/\/$//;
if ($p eq $sf) {
$rv[$#rv]->{'spam'} = 1;
}
}
}
}
}
# Add any Usermin external mail files
if ($config{'mailbox_user'}) {
local %userconfig;
&read_file_cached("$uinfo[7]/$config{'mailbox_user'}/config",
\%userconfig);
local $o;
foreach $o (split(/\t+/, $userconfig{'mailboxes'})) {
$o =~ /\/([^\/]+)$/ || next;
push(@rv, { 'name' => $o,
'file' => $o,
'type' => &folder_type($o),
'mode' => 1,
'index' => scalar(@rv) } );
}
}
foreach my $f (@rv) {
$f->{'user'} = $_[0];
}
return @rv;
}
}
sub list_user_folders_sorted
{
return &list_user_folders(@_);
}
# get_mail_style()
# Returns a list containing the mail base directory, directory style,
# mail file in home dir, and maildir in home dir
sub get_mail_style
{
if (!scalar(@mail_style_cache)) {
if ($config{'auto'}) {
# Based on mail server
if ($config{'mail_system'} == 1) {
# Can get paths from Sendmail module config
local %sconfig = &foreign_config("sendmail");
if ($sconfig{'mail_dir'}) {
# File under /var/mail
return ($sconfig{'mail_dir'},
$sconfig{'mail_style'}, undef, undef);
}
elsif ($sconfig{'mail_type'}) {
# Maildir under home directory
return (undef, $sconfig{'mail_style'},
undef, $sconfig{'mail_file'});
}
else {
# mbox under home directory
return (undef, $sconfig{'mail_style'},
$sconfig{'mail_file'}, undef);
}
}
elsif ($config{'mail_system'} == 0) {
# Need to query Postfix module for paths
&foreign_require("postfix", "postfix-lib.pl");
local @s = &postfix::postfix_mail_system();
if ($s[0] == 0) {
return ($s[1], 0, undef, undef);
}
elsif ($s[0] == 1) {
return (undef, 0, $s[1], undef);
}
elsif ($s[0] == 2) {
return (undef, 0, undef, $s[1]);
}
}
elsif ($config{'mail_system'} == 2 ||
$config{'mail_system'} == 4) {
# Need to check qmail module config for paths
local %qconfig = &foreign_config("qmailadmin");
if ($qconfig{'mail_system'} == 1) {
return (undef, 0, undef,
$qconfig{'mail_dir_qmail'});
}
elsif ($qconfig{'mail_dir'}) {
return ($qconfig{'mail_dir'}, $qconfig{'mail_style'}, undef, undef);
}
else {
return (undef, $qconfig{'mail_style'}, $qconfig{'mail_file'}, undef);
}
}
elsif ($config{'mail_system'} == 5) {
# vpopmail always uses ~/Maildir
return ( undef, 0, undef, "Maildir" );
}
elsif ($config{'mail_system'} == 6) {
# localmail using smail or exim
return ( "/var/spool/mail", 0, undef, undef );
}
else {
# No mail server set yet!
return (undef, undef, undef, undef);
}
}
else {
# Use config settings
@mail_style_cache = ($config{'mail_dir'}, $config{'mail_style'},
$config{'mail_file'}, $config{'mail_sub'});
}
}
return @mail_style_cache;
}
# can_user(username, [other details])
sub can_user
{
if (!&is_user($_[0])) {
# For external files, check if the file is under an allowed
# directory, or owned by an allowed user.
local @st = stat($_[0]);
local @uinfo = &get_mail_uid($st[4]);
return 1 if (@uinfo && &can_user(@uinfo));
local $dir = &allowed_directory();
return defined($dir) && &is_under_directory($dir, $_[0]);
}
local @u = @_ > 1 ? @_ : &get_mail_user($_[0]);
return 1 if ($_[0] && $access{'sent'} eq $_[0]);
return 1 if ($access{'mmode'} == 1);
return 0 if (!@u);
return 0 if ($_[0] =~ /\.\./);
return 0 if ($access{'mmode'} == 0);
local $u;
if ($access{'mmode'} == 2) {
# Is user in list of users?
foreach $u (split(/\s+/, $access{'musers'})) {
return 1 if ($u eq $_[0]);
}
return 0;
}
elsif ($access{'mmode'} == 4) {
# Is user the current Webmin user?
return 1 if ($_[0] eq $remote_user);
}
elsif ($access{'mmode'} == 5) {
# Is the user's gid in the list of groups?
local $gid;
foreach $gid (split(/\s+/, $access{'musers'})) {
return 1 if ($u[3] == $gid);
if ($access{'msec'}) {
# Check user's secondary groups too
local @ginfo = getgrgid($gid);
local @m = split(/\s+/, $ginfo[3]);
return 1 if (&indexof($_[0], @m) >= 0);
}
}
}
elsif ($access{'mmode'} == 3) {
# Is the user not in the list of denied users
foreach $u (split(/\s+/, $access{'musers'})) {
return 0 if ($u eq $_[0]);
}
return 1;
}
elsif ($access{'mmode'} == 6) {
# Does the user match a regexp?
return ($_[0] =~ /^$access{'musers'}$/);
}
elsif ($access{'mmode'} == 7) {
# Is the user's UID within the allowed range?
return (!$access{'musers'} || $u[2] >= $access{'musers'}) &&
(!$access{'musers2'} || $u[2] <= $access{'musers2'});
}
return 0; # can't happen!
}
# movecopy_user_select(number, folders, folder, form-no)
# Returns HTML for entering a username to copy mail to
sub movecopy_user_select
{
local $rv;
$rv .= "<input type=submit name=move$_[0] value=\"$text{'mail_move'}\">";
$rv .= "<input type=submit name=copy$_[0] value=\"$text{'mail_copy'}\">";
$rv .= &ui_user_textbox("mfolder$_[0]", undef, $_[3]);
return $rv;
}
# need_delete_warn(&folder)
sub need_delete_warn
{
return 1 if ($config{'delete_warn'} eq 'y');
return 0 if ($config{'delete_warn'} eq 'n');
local $mf;
return $_[0]->{'type'} == 0 &&
($mf = &folder_file($_[0])) &&
&disk_usage_kb($mf)*1024 > $config{'delete_warn'};
}
# detect_mail_system()
# Works out which mail server is installed
sub detect_mail_system
{
foreach my $m (@mail_system_modules) {
return $m->[1] if (&check_mail_system($m));
}
return 3;
}
# check_mail_system(&mailsystem)
sub check_mail_system
{
if ($_[0]->[0]) {
# Just check module
return &foreign_installed($_[0]->[0]);
}
else {
# Call function
local $func = $_[0]->[2];
return &$func($_[0]);
}
}
# test_mail_system([&mailsystem])
# Returns an error message if the mail system is invalid
sub test_mail_system
{
local $ms;
if (!$ms) {
($ms) = grep { $_->[1] == $config{'mail_system'} } @mail_system_modules;
}
if ($ms->[3]) {
local $func = $ms->[3];
return &$func();
}
return undef;
}
# check_qmail_ldap()
# Make sure Qmail with LDAP extensions is installed
sub check_qmail_ldap
{
return 0 if (&foreign_installed("qmailadmin", 1) != 2);
local %qconfig = &foreign_config("qmailadmin");
return 0 if (!-r "$qconfig{'qmail_dir'}/control/ldapserver");
return 1;
}
# check_vpopmail()
# Make sure Qmail with VPopMail extensions is installed
sub check_vpopmail
{
return 0 if (&foreign_installed("qmailadmin", 1) != 2);
return -x "$config{'vpopmail_dir'}/bin/vadddomain";
}
# check_exim()
# Make sure Exim is installed
sub check_exim
{
return &has_command('exim');
}
# test_qmail_ldap()
# Returns undef the Qmail+LDAP database can be contacted OK, or an error message
sub test_qmail_ldap
{
$config{'ldap_host'} || return $text{'ldap_ehost'};
$config{'ldap_port'} =~ /^\d+$/ || return $text{'ldap_eport'};
$config{'ldap_login'} || return $text{'ldap_euser'};
$config{'ldap_base'} || return $text{'ldap_ebase'};
local $err = &connect_qmail_ldap(1);
return ref($err) ? undef : $err;
}
# show_users_table(&users-list, [only-with-mail])
# Outputs HTML for a table of users, with the appropriate sorting and mode
sub show_users_table
{
my @users = @{$_[0]};
my ($u, %size, %incount, %sentcount, %foldercount);
if ($config{'sort_mode'} == 2 || $config{'show_size'} > 0 || $_[1] ||
$config{'show_count'} || $config{'show_sent'}) {
# Need to check folders
foreach $u (@users) {
next if ($config{'ignore_users_enabled'} == 1 &&
&indexof($u->[0], @ignore_users_list) >= 0);
local @folders = &list_user_folders(@$u);
$foldercount{$u->[0]} = scalar(@folders);
if ($config{'sort_mode'} == 2 ||
$config{'show_size'} > 0 || $_[1]) {
# Compute size of folders
$size{$u->[0]} = $config{'size_mode'} ?
&folder_size(@folders) :
&folder_size($folders[0]);
}
if ($config{'show_count'}) {
# Get number of mails in inbox
$incount{$u->[0]} = &mailbox_folder_size($folders[0]);
}
if ($config{'show_sent'}) {
# Count number of messages in sent mail
local ($sent) = grep { $_->{'sent'} } @folders;
$sentcount{$u->[0]} = &mailbox_folder_size($sent)
if ($sent);
}
}
}
# Sort by chosen mode
if ($config{'sort_mode'} == 2) {
@users = sort { $size{$b->[0]} <=> $size{$a->[0]} } @users;
}
elsif ($config{'sort_mode'} == 1) {
@users = sort { lc($a->[0]) cmp lc($b->[0]) } @users;
}
local @allusers = @users;
if ($_[1]) {
# Limit to those with mail
@users = grep { $size{$_->[0]} } @users;
}
# Show table of users
if (!@allusers) {
print "<b>$text{'index_nousers'}</b><p>\n";
}
elsif (!@users) {
print "<b>$text{'index_nousersmail'}</b><p>\n";
}
elsif ($config{'show_size'} == 2) {
# Show full user details
local %uconfig = &foreign_config("useradmin");
local @ccols;
push(@ccols, $text{'find_incount'}) if ($config{'show_count'});
push(@ccols, $text{'find_sentcount'}) if ($config{'show_sent'});
push(@ccols, $text{'find_fcount'}) if (%foldercount);
print &ui_columns_start( [ $text{'find_user'}, $text{'find_real'},
$text{'find_group'},
$text{'find_size'}, @ccols ], 100);
foreach $u (@users) {
local $g = getgrgid($u->[3]);
next if ($config{'ignore_users_enabled'} == 1 &&
&indexof($u->[0], @ignore_users_list) >= 0);
$u->[6] =~ s/,.*$// if ($uconfig{'extra_real'} ||
$u->[6] =~ /,$/);
local $home = $u->[7];
if (length($home) > 30) {
$home = "...".substr($home, -30);
}
local @ccols;
if ($config{'show_count'}) {
push(@ccols, int($incount{$u->[0]}))
}
if ($config{'show_sent'}) {
push(@ccols, int($sentcount{$u->[0]}))
}
if (%foldercount) {
push(@ccols, int($foldercount{$u->[0]}))
}
print &ui_columns_row(
[ &ui_link("list_mail.cgi?user=$u->[0]","$u->[0]"),
$u->[6], $g,
$size{$u->[0]} == 0 ? $text{'index_empty'} :
&nice_size($size{$u->[0]}),
@ccols ],
[ undef, undef, undef, undef, "nowrap" ]);
}
print &ui_columns_end();
}
else {
# Just showing username (and maybe size)
my @grid;
foreach $u (@users) {
next if ($config{'ignore_users_enabled'} == 1 &&
&indexof($u->[0], @ignore_users_list) >= 0);
my $g = "<a href='list_mail.cgi?user=".&urlize($u->[0])."'>";
$g .= &html_escape($u->[0]);
if ($config{'show_size'} == 1) {
local @folders = &list_user_folders(@$u);
local $total = &folder_size(@folders);
if ($size{$u->[0]} > 0) {
$g .= $config{'show_size_below'} ? '<br>' : ' ';
$g .= "(";
if (%foldercount) {
$g .= &text('find_in',
&nice_size($size{$u->[0]}),
$foldercount{$u->[0]});
}
else {
$g .= &nice_size($size{$u->[0]});
}
$g .= ")";
}
}
$g .= "</a>";
push(@grid, $g);
}
my $w = int(100/$config{'column_count'});
my @tds = map { "width=".int($w)."%" } (1..$config{'column_count'});
print &ui_grid_table(\@grid, $config{'column_count'}, 100, \@tds, undef,
$text{'index_header'});
}
}
# switch_to_user(user)
# Switch to the Unix user that files are accessed as.
sub switch_to_user
{
if (!defined($old_uid)) {
local @uinfo = &get_mail_user($_[0]);
$old_uid = $>;
$old_gid = $);
$) = "$uinfo[3] $uinfo[3]";
$> = $uinfo[2];
}
}
sub switch_user_back
{
if (defined($old_uid)) {
$> = $old_uid;
$) = $old_gid;
$old_uid = $old_gid = undef;
}
}
sub folder_link
{
return &ui_link("list_mail.cgi?user=$_[0]&folder=$_[1]->{'index'}",$text{'mail_return2'});
}
# get_from_address()
# Returns the address to use when sending email from a script
sub get_from_address
{
my $host = &get_from_domain();
my $rv;
if ($config{'webmin_from'} =~ /\@/) {
$rv = $config{'webmin_from'};
}
elsif (!$config{'webmin_from'}) {
$rv = "webmin-noreply\@$host";
}
else {
$rv = "$config{'webmin_from'}\@$host";
}
if ($config{'webmin_from_name'}) {
$rv = "\"$config{'webmin_from_name'}\" <$rv>";
}
return $rv;
}
# get_from_domain()
# Returns the default domain for From: addresses
sub get_from_domain
{
return $config{'from_dom'} || &get_display_hostname();
}
# get_user_from_address(&uinfo)
# Returns the default From: address for mail sent from some user's mailbox
sub get_user_from_address
{
local $uinfo = $_[0];
if ($config{'from_addr'}) {
return $config{'from_addr'};
}
elsif ($uinfo->[11]) {
return $uinfo->[11];
}
elsif ($config{'from_virtualmin'} && &foreign_check("virtual-server")) {
# Does Virtualmin manage this user?
&foreign_require("virtual-server", "virtual-server-lib.pl");
local $d;
foreach $d (&virtual_server::list_domains()) {
local @users = &virtual_server::list_domain_users($d, 0, 0, 1);
local $u;
foreach $u (@users) {
if ($u->{'user'} eq $uinfo->[0] && $u->{'email'}) {
# Found him!
return $u->{'email'};
}
}
}
}
if ($uinfo->[0] =~ /\@/) {
return $uinfo->[0];
}
else {
return $uinfo->[0].'@'.&get_from_domain();
}
}
# check_modification(&folder)
# Display an error message if a folder has been modified since the time
# in $in{'mod'}
sub check_modification
{
local $newmod = &modification_time($_[0]);
if ($in{'mod'} && $in{'mod'} != $newmod && $config{'check_mod'}) {
# Changed!
&error(&text('emodified', "list_mail.cgi?user=$in{'user'}&folder=$_[0]->{'index'}"));
}
}
# spam_report_cmd(user)
# Returns a command for reporting spam, or undef if none
sub spam_report_cmd
{
local ($user) = @_;
local %sconfig = &foreign_config("spam");
local $cmd;
if ($config{'spam_report'} eq 'sa_learn') {
$cmd = &has_command($sconfig{'sa_learn'}) ? "$sconfig{'sa_learn'} --spam --mbox" : undef;
}
elsif ($config{'spam_report'} eq 'spamassassin') {
$cmd = &has_command($sconfig{'spamassassin'}) ? "$sconfig{'spamassassin'} --r" : undef;
}
else {
$cmd = &has_command($sconfig{'sa_learn'}) ?
"$sconfig{'sa_learn'} --spam --mbox" :
&has_command($sconfig{'spamassassin'}) ?
"$sconfig{'spamassassin'} --r" : undef;
}
return $user eq "root" ? $cmd :
$cmd ? &command_as_user($user, 0, $cmd) : undef;
}
# ham_report_cmd()
# Returns a command for reporting ham, or undef if none
sub ham_report_cmd
{
local %sconfig = &foreign_config("spam");
return &has_command($sconfig{'sa_learn'}) ? "$sconfig{'sa_learn'} --ham --mbox" : undef;
}
# allowed_directory()
# Returns base directory for mail files, or undef if not allowed
sub allowed_directory
{
if ($access{'dir'}) {
return $access{'dir'};
}
else {
return $access{'mmode'} == 1 ? "/" : undef;
}
}
# is_user(user)
sub is_user
{
return $_[0] !~ /^\//;
}
# list_mail_users([max], [filterfunc])
# Returns getpw* style structures for all users who can receive mail. Those with
# duplicate info are skipped.
sub list_mail_users
{
local ($max, $filter) = @_;
local @rv;
if ($config{'mail_system'} < 3 or $config{'mail_system'} == 6 ) {
# Postfix, Sendmail, smail, exim and Qmail all use Unix users
local %found;
local %hfound;
local ($dir, $style, $mailbox, $maildir) = &get_mail_style();
setpwent();
while(my (@uinfo) = getpwent()) {
if ($found{$uinfo[0]}++) {
# done this username already
next;
}
if (!$dir && $hfound{$uinfo[7]}++) {
# done this home directory (and thus inbox) already
next;
}
if ($dir && $uinfo[0] =~ /\@/) {
# If this is a user@domain username, mark user-domain
# as done already, to avoid showing dupes
local $noat = $uinfo[0];
$noat =~ s/\@/-/g;
$found{$noat}++;
}
next if ($filter && !&$filter(@uinfo));
push(@rv, \@uinfo);
last if ($max && @rv > $max);
}
endpwent();
}
elsif ($config{'mail_system'} == 4) {
# Qmail+LDAP uses an LDAP db
local $ldap = &connect_qmail_ldap();
local $rv = $ldap->search(base => $config{'ldap_base'},
filter => "(objectClass=qmailUser)");
&error($rv->error) if ($rv->code);
local $u;
foreach $u ($rv->all_entries) {
local @uinfo = &qmail_dn_to_user($u);
next if (!$uinfo[10]); # alias only
next if ($filter && !&$filter(@uinfo));
push(@rv, \@uinfo);
last if ($max && @rv > $max);
}
$ldap->unbind();
}
elsif ($config{'mail_system'} == 5) {
# Get vpopmail user list for all domains
opendir(DOMS, "$config{'vpopmail_dir'}/domains");
foreach my $d (readdir(DOMS)) {
next if ($d =~ /^\./);
local @uinfos = &parse_vpopmail_users("-D $d", $d);
if ($filter) {
@uinfos = grep { &$filter(@$_) } @uinfos;
}
push(@rv, @uinfos);
last if ($max && @rv > $max);
}
closedir(DOMS);
}
return @rv;
}
# parse_vpopmail_users(command, domain)
sub parse_vpopmail_users
{
local %attr_map = ( "passwd" => 1,
"gecos" => 5,
"dir" => 7 );
local (@rv, $user);
open(UINFO, "$config{'vpopmail_dir'}/bin/vuserinfo $_[0] |");
while(<UINFO>) {
s/\r|\n//g;
if (/^([^:]+):\s+(.*)$/) {
local ($attr, $value) = ($1, $2);
if ($attr eq "name") {
# Start of a new user
$user = [ "$value\@$_[1]" ];
$user->[11] = $user->[0];
push(@rv, $user);
}
local $amapped = $attr_map{$attr};
$user->[$amapped] = $value if ($amapped);
}
}
close(UINFO);
return @rv;
}
# get_mail_user(name)
# Looks up a user by name
sub get_mail_user
{
if ($config{'mail_system'} < 3 || $config{'mail_system'} == 6) {
# Just find Unix user for Sendmail, Postfix, Qmail and Exim
return getpwnam($_[0]);
}
elsif ($config{'mail_system'} == 4) {
# Lookup in LDAP DB
local $ldap = &connect_qmail_ldap();
local $rv = $ldap->search(base => $config{'ldap_base'},
filter => "(&(objectClass=qmailUser)(uid=$_[0]))");
&error($rv->error) if ($rv->code);
local ($u) = $rv->all_entries;
if ($u) {
# Found in LDAP
local @uinfo = &qmail_dn_to_user($u);
$ldap->unbind();
return @uinfo;
}
else {
# Fall back to Unix user
return getpwnam($_[0]);
}
}
elsif ($config{'mail_system'} == 5) {
# Find in vpopmail
local ($box, $dom) = split(/\@/, $_[0]);
local @users = &parse_vpopmail_users($_[0], $dom);
return @{$users[0]};
}
}
# get_mail_uid(name)
# Looks up a user by UID
sub get_mail_uid
{
if ($config{'mail_system'} < 3) {
# Just find Unix user
return getpwuid($_[0]);
}
elsif ($config{'mail_system'} == 4) {
# Lookup in LDAP DB
local $ldap = &connect_qmail_ldap();
local $rv = $ldap->search(base => $config{'ldap_base'},
filter => "(&(objectClass=qmailUser)(uidNumber=$_[0]))");
&error($rv->error) if ($rv->code);
local ($u) = $rv->all_entries;
local @uinfo = &qmail_dn_to_user($u);
$ldap->unbind();
return @uinfo;
}
elsif ($config{'mail_system'} == 5) {
# Find in vpopmail
return ( ); # not possible, since UIDs aren't used!
}
}
# connect_qmail_ldap([return-error])
# Connect to the LDAP server used for Qmail. Returns an LDAP handle on success,
# or an error message on failure.
sub connect_qmail_ldap
{
eval "use Net::LDAP";
if ($@) {
local $err = &text('ldap_emod', "<tt>Net::LDAP</tt>");
if ($_[0]) { return $err; }
else { &error($err); }
}
# Connect to server
local $port = $config{'ldap_port'} || 389;
local $ldap = Net::LDAP->new($config{'ldap_host'}, port => $port);
if (!$ldap) {
local $err = &text('ldap_econn',
"<tt>$config{'ldap_host'}</tt>","<tt>$port</tt>");
if ($_[0]) { return $err; }
else { &error($err); }
}
# Start TLS if configured
if ($config{'ldap_tls'}) {
$ldap->start_tls();
}
# Login
local $mesg;
if ($config{'ldap_login'}) {
$mesg = $ldap->bind(dn => $config{'ldap_login'},
password => $config{'ldap_pass'});
}
else {
$mesg = $ldap->bind(anonymous => 1);
}
if (!$mesg || $mesg->code) {
local $err = &text('ldap_elogin', "<tt>$config{'ldap_host'}</tt>",
$dn, $mesg ? $mesg->error : "Unknown error");
if ($_[0]) { return $err; }
else { &error($err); }
}
return $ldap;
}
# qmail_dn_to_user(&dn)
sub qmail_dn_to_user
{
local $mms = &add_ldapmessagestore(
scalar($_[0]->get_value("mailMessageStore")));
if (-d "$mms/Maildir") {
$mms .= "/" if ($mms !~ /\/$/);
$mms .= "Maildir";
}
return ( scalar($_[0]->get_value("uid")),
scalar($_[0]->get_value("userPassword")),
scalar($_[0]->get_value("uidNumber")),
scalar($_[0]->get_value("gidNumber")),
scalar($_[0]->get_value("mailQuotaSize")),
scalar($_[0]->get_value("cn")),
scalar($_[0]->get_value("cn")),
scalar($_[0]->get_value("homeDirectory")),
scalar($_[0]->get_value("loginShell")),
undef,
$mms,
scalar($_[0]->get_value("mail")),
);
}
# add_ldapmessagestore(path)
sub add_ldapmessagestore
{
if (!$_[0]) {
return $_[0];
}
elsif ($_[0] =~ /^\//) {
return $_[0];
}
else {
&foreign_require("qmailadmin", "qmail-lib.pl");
local $pfx = &qmailadmin::get_control_file("ldapmessagestore");
return $pfx."/".$_[0];
}
}
# show_buttons(number, &folders, current-folder, &mail, user, search-mode)
sub show_buttons
{
local ($num, $folders, $folder, $mail, $user, $search) = @_;
local $uuser = &urlize($user);
local $spacer = "&nbsp;\n";
if (@$mail) {
# Delete
print "<input type=submit name=delete value=\"$text{'mail_delete'}\">";
if ($config{'show_delall'} && !$search) {
print "<input type=submit name=deleteall value=\"$text{'mail_deleteall'}\">";
}
print $spacer;
# Mark as
print "<input type=submit name=mark$_[0] value=\"$text{'mail_mark'}\">";
print "<select name=mode$_[0]>\n";
print "<option value=1 checked>$text{'mail_mark1'}</option>\n";
print "<option value=0>$text{'mail_mark0'}</option>\n";
print "<option value=2>$text{'mail_mark2'}</option>\n";
print "</select>";
print $spacer;
if (&is_user($user)) {
# Forward
if ($config{'open_mode'}) {
# Forward messages in a separate window
print "<input type=submit name=forward value=\"$text{'mail_forward'}\" onClick='args = \"user=$uuser&folder=$folder->{'index'}\"; for(i=0; i<form.d.length; i++) { if (form.d[i].checked) { args += \"&mailforward=\"+form.d[i].value; } } window.open(\"reply_mail.cgi?\"+args, \"compose\", \"toolbar=no,menubar=no,scrollbars=yes,width=1024,height=768\"); return false'>";
}
else {
print "<input type=submit name=forward value=\"$text{'mail_forward'}\">";
}
print $spacer;
}
# Move/copy
print &movecopy_user_select($_[0], $folders, $folder, 1);
print $spacer;
# Show spam report buttons
if (!$folder->{'spam'} &&
&foreign_installed("spam") &&
$config{'spam_buttons'} =~ /list/ &&
&spam_report_cmd($user)) {
if (&foreign_available("spam")) {
print "<input type=submit value=\"$text{'mail_black'}\" name=black>";
}
if ($config{'spam_del'}) {
print "<input type=submit value=\"$text{'view_razordel'}\" name=razor>\n";
}
else {
print "<input type=submit value=\"$text{'view_razor'}\" name=razor>";
}
print $spacer;
}
elsif ($folder->{'spam'} &&
&foreign_installed("spam") &&
$config{'spam_buttons'} =~ /list/ &&
&ham_report_cmd($user)) {
if (&foreign_available("spam")) {
print "<input type=submit value=\"$text{'mail_white'}\" name=white>";
}
print "<input type=submit value=\"$text{'view_ham'}\" name=ham>";
print $spacer;
}
}
if ($config{'open_mode'}) {
# Show mass open button
print "<input type=submit name=new value=\"$text{'mail_open'}\" onClick='for(i=0; i<form.d.length; i++) { if (form.d[i].checked) { window.open(\"view_mail.cgi?user=$uuser&folder=$folder->{'index'}&idx=\"+form.d[i].value, \"view\"+i, \"toolbar=no,menubar=no,scrollbars=yes,width=1024,height=768\"); } }
return false'>";
print $spacer;
}
# Compose
if (&is_user($user)) {
if ($config{'open_mode'}) {
# In a separate window
print "<input type=submit name=new value=\"$text{'mail_compose'}\" onClick='window.open(\"reply_mail.cgi?user=$user&new=1\", \"compose\", \"toolbar=no,menubar=no,scrollbars=yes,width=1024,height=768\"); return false'>";
}
else {
print "<input type=submit name=new value=\"$text{'mail_compose'}\">";
}
}
print "<br>\n";
}
# get_signature(user)
# Returns the users signature, if any
sub get_signature
{
local $sf = &get_signature_file($_[0]);
$sf || return undef;
local $sig;
open(SIG, "<".$sf) || return undef;
while(<SIG>) {
$sig .= $_;
}
close(SIG);
return $sig;
}
# get_signature_file(user)
# Returns the full path to the file that should contain the user's signature,
# or undef if none is defined
sub get_signature_file
{
return undef if ($config{'sig_file'} eq '*' || $config{'sig_file'} eq '');
local $sf = $config{'sig_file'};
if ($sf !~ /^\//) {
local @uinfo = getpwnam($_[0]);
$sf = "$uinfo[7]/$sf";
}
return $sf;
}
# view_mail_link(user, &folder, index, from-to-text, [dom])
sub view_mail_link
{
local ($user, $folder, $idx, $txt, $dom) = @_;
local $uuser = &urlize($user);
local $url = "view_mail.cgi?user=$uuser&idx=$idx&folder=$folder->{'index'}".
"&dom=$dom";
if ($config{'open_mode'}) {
return "<a href='' onClick='window.open(\"$url\", \"viewmail\", \"toolbar=no,menubar=no,scrollbars=yes,width=1024,height=768\"); return false'>".
&simplify_from($txt)."</a>";
}
else {
return "<a href='$url'>".&simplify_from($txt)."</a>";
}
}
# mail_page_header(title, headstuff, bodystuff, rightstuff)
sub mail_page_header
{
if ($config{'open_mode'}) {
&popup_header($_[0], $_[1], $_[2]);
}
else {
&ui_print_header(undef, $_[0], "", undef, 0, 0, 0, $_[3], $_[1], $_[2]);
}
}
# mail_page_footer(link, text, ...)
sub mail_page_footer
{
if ($config{'open_mode'}) {
&popup_footer();
}
else {
&ui_print_footer(@_);
}
}
# delete_user_index_files(name)
# Delete all files associated with some user's mail, such as index and maildir
# cache files
sub delete_user_index_files
{
local ($user) = @_;
foreach my $folder (&list_user_folders($user)) {
if ($folder->{'type'} == 0) {
# Remove mbox index
local $ifile = &user_index_file($folder->{'file'});
if (-r $ifile) {
&unlink_file($ifile);
}
else {
&unlink_file(glob("\Q$ifile\E.{dir,pag,db}"));
}
&unlink_file("$ifile.ids");
}
elsif ($folder->{'type'} == 1) {
# Remove Maildir files file
&unlink_file(&get_maildir_cachefile($folder->{'file'}));
}
# Remove sort index
local $ifile = &folder_new_sort_index_file($folder);
if (-r $ifile) {
&unlink_file($ifile);
}
else {
&unlink_file(glob("\Q$ifile\E.{dir,pag,db}"));
}
}
# Remove read file
local $read = &user_read_dbm_file($user);
if (-r $read) {
&unlink_file($read);
}
else {
&unlink_file(glob("\Q$read\E.{dir,pag,db}"));
}
}
# get_mail_read(&folder, &mail)
# Returns the read flag for some message, from the DBM
sub get_mail_read
{
local ($folder, $mail) = @_;
if (!$done_dbmopen_read++) {
&open_dbm_db(\%read, &user_read_dbm_file($folder->{'user'}), 0600);
}
return $read{$mail->{'header'}->{'message-id'}};
}
# set_mail_read(&folder, &mail, read)
# Sets the read flag for some message in the DBM
sub set_mail_read
{
local ($folder, $mail, $read) = @_;
if (!$done_dbmopen_read++) {
&open_dbm_db(\%read, &user_read_dbm_file($folder->{'user'}), 0600);
}
$read{$mail->{'header'}->{'message-id'}} = $read;
}
# show_mail_table(&mails, &folder, formno, [&read])
# Output a full table of messages
sub show_mail_table
{
local @mail = @{$_[0]};
local (undef, $folder, $formno) = @_;
my $showto = !$folder ? 0 :
$folder->{'sent'} || $folder->{'drafts'} ? 1 : 0;
my @tds = ( "nowrap", "nowrap", "nowrap", "nowrap" );
# Show mailbox headers
local @hcols;
if ($folder) {
push(@hcols, "");
splice(@tds, "width=5", 0, 0);
}
push(@hcols, $showto ? $text{'mail_to'} : $text{'mail_from'});
push(@hcols, $config{'show_to'} ? $showto ? ( $text{'mail_from'} ) :
( $text{'mail_to'} ) : ());
push(@hcols, $text{'mail_date'});
push(@hcols, $text{'mail_size'});
push(@hcols, $text{'mail_subject'});
my @links = ( &select_all_link("d", $formno),
&select_invert_link("d", $formno) );
if ($folder) {
print &ui_links_row(\@links);
}
print &ui_columns_start(\@hcols, 100, 0, \@tds);
# Show rows for actual mail messages
my $i = 0;
foreach my $mail (@mail) {
next if (!$mail);
local $idx = $mail->{'idx'};
local $cols = 0;
local @cols;
local @rowtds = @tds;
# From and To columns, with links
local $from = $mail->{'header'}->{$showto ? 'to' : 'from'};
$from = $text{'mail_unknown'} if ($from !~ /\S/);
local $mfolder = $mail->{'folder'} || $folder;
push(@cols, &view_mail_link($in{'user'}, $mfolder, $idx, $from,
$in{'dom'}));
if ($config{'show_to'}) {
push(@cols, &simplify_from(
$mail->{'header'}->{$showto ? 'from' : 'to'}));
}
# Date and size columns
push(@cols, &simplify_date($mail->{'header'}->{'date'}));
push(@cols, &nice_size($mail->{'size'}, 1024));
$rowtds[$#cols] .= " data-sort=".&parse_mail_date($mail->{'header'}->{'date'});
# Subject with icons
local @icons = &message_icons($mail, $mfolder->{'sent'}, $mfolder);
push(@cols, &simplify_subject($mail->{'header'}->{'subject'}).
join("&nbsp;", @icons));
# Flag unread mails
local $hid = $mail->{'header'}->{'message-id'};
if ($_[3] && !$_[3]->{$hid}) {
@cols = map { "<b>$_</b>" } @cols;
}
# Generate the row
if (!$folder) {
print &ui_columns_row(\@cols, \@rowtds);
}
elsif (&editable_mail($mail)) {
print &ui_checked_columns_row(\@cols, \@rowtds, "d", $idx);
}
else {
print &ui_columns_row([ "", @cols ], \@tds);
}
if ($config{'show_body'}) {
# Show part of the body too
&parse_mail($mail);
local $data = &mail_preview($mail);
if ($data) {
print "<tr $cb> <td colspan=",(scalar(@cols)+1),"><tt>",
&html_escape($data),"</tt></td> </tr>\n";
}
}
$i++;
}
print &ui_columns_end();
if ($folder) {
print &ui_links_row(\@links);
}
}
sub user_list_link
{
if ($in{'dom'}) {
return "../virtual-server/list_users.cgi?dom=$in{'dom'}";
}
else {
return "";
}
}
# user_read_dbm_file(user)
# Returns the DBM base filename to track read status for messages to some user
sub user_read_dbm_file
{
my ($user) = @_;
my $rv = "$module_config_directory/$user.read";
if (!glob("\Q$rv\E.*")) {
$rv = "$module_var_directory/$user.read";
}
return $rv;
}
1;