# 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;
