<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;"># folders-lib.pl
# Functions for dealing with mail folders in various formats

$pop3_port = 110;
$imap_port = 143;

@index_fields = ( "subject", "from", "to", "date", "size",
		  "x-spam-status", "message-id" );
$create_cid_count = 0;

# get_folder_cache_directory(&amp;folder)
# Returns a directory used to cache IMAP or POP3 files for some folder
sub get_folder_cache_directory
{
my ($folder) = @_;
if ($user_module_config_directory) {
	return $user_module_config_directory."/".$folder-&gt;{'id'}.".cache";
	}
else {
	my $rv = $module_config_directory."/".$folder-&gt;{'id'}.".cache";
	if (!-d $rv) {
		$rv = $module_var_directory."/".$folder-&gt;{'id'}.".cache";
		}
	return $rv;
	}
}

# mailbox_list_mails(start, end, &amp;folder, [headersonly], [&amp;error])
# Returns an array whose size is that of the entire folder, with messages
# in the specified range filled in.
sub mailbox_list_mails
{
my @mail;
&amp;switch_to_folder_user($_[2]);
if ($_[2]-&gt;{'type'} == 0) {
	# List a single mbox formatted file
	@mail = &amp;list_mails($_[2]-&gt;{'file'}, $_[0], $_[1]);
	}
elsif ($_[2]-&gt;{'type'} == 1) {
	# List a qmail maildir
	local $md = $_[2]-&gt;{'file'};
	@mail = &amp;list_maildir($md, $_[0], $_[1], $_[3]);
	}
elsif ($_[2]-&gt;{'type'} == 2) {
	# Get mail headers/body from a remote POP3 server

	# Login first
	local @rv = &amp;pop3_login($_[2]);
	if ($rv[0] != 1) {
		# Failed to connect or login
		if ($_[4]) {
			@{$_[4]} = @rv;
			return ();
			}
		elsif ($rv[0] == 0) { &amp;error($rv[1]); }
		else { &amp;error(&amp;text('save_elogin', $rv[1])); }
		}
	local $h = $rv[1];
	local @uidl = &amp;pop3_uidl($h);
	local %onserver = map { &amp;safe_uidl($_), 1 } @uidl;

	# Work out what range we want
	local ($start, $end) = &amp;compute_start_end($_[0], $_[1], scalar(@uidl));
	@mail = map { undef } @uidl;

	# For each message in the range, get the headers or body
	local ($i, $f, %cached, %sizeneed);
	local $cd = &amp;get_folder_cache_directory($_[2]);
	if (opendir(CACHE, $cd)) {
		while($f = readdir(CACHE)) {
			if ($f =~ /^(\S+)\.body$/) {
				$cached{$1} = 2;
				}
			elsif ($f =~ /^(\S+)\.headers$/) {
				$cached{$1} = 1;
				}
			}
		closedir(CACHE);
		}
	else {
		mkdir($cd, 0700);
		}
	for($i=$start; $i&lt;=$end; $i++) {
		local $u = &amp;safe_uidl($uidl[$i]);
		if ($cached{$u} == 2 || $cached{$u} == 1 &amp;&amp; $_[3]) {
			# We already have everything that we need
			}
		elsif ($cached{$u} == 1 || !$_[3]) {
			# We need to get the entire mail
			&amp;pop3_command($h, "retr ".($i+1));
			open(CACHE, "&gt;", "$cd/$u.body");
			while(&lt;$h&gt;) {
				s/\r//g;
				last if ($_ eq ".\n");
				print CACHE $_;
				}
			close(CACHE);
			unlink("$cd/$u.headers");
			$cached{$u} = 2;
			}
		else {
			# We just need the headers
			&amp;pop3_command($h, "top ".($i+1)." 0");
			open(CACHE, "&gt;", "$cd/$u.headers");
			while(&lt;$h&gt;) {
				s/\r//g;
				last if ($_ eq ".\n");
				print CACHE $_;
				}
			close(CACHE);
			$cached{$u} = 1;
			}
		local $mail = &amp;read_mail_file($cached{$u} == 2 ?
				"$cd/$u.body" : "$cd/$u.headers");
		if ($cached{$u} == 1) {
			if ($mail-&gt;{'body'} ne "") {
				$mail-&gt;{'size'} = int($mail-&gt;{'body'});
				}
			else {
				$sizeneed{$i} = 1;
				}
			}
		$mail-&gt;{'idx'} = $i;
		$mail-&gt;{'id'} = $uidl[$i];
		$mail[$i] = $mail;
		}

	# Get sizes for mails if needed
	if (%sizeneed) {
		&amp;pop3_command($h, "list");
		while(&lt;$h&gt;) {
			s/\r//g;
			last if ($_ eq ".\n");
			if (/^(\d+)\s+(\d+)/ &amp;&amp; $sizeneed{$1-1}) {
				# Add size to the mail cache
				$mail[$1-1]-&gt;{'size'} = $2;
				local $u = &amp;safe_uidl($uidl[$1-1]);
				open(CACHE, "&gt;&gt;", "$cd/$u.headers");
				print CACHE $2,"\n";
				close(CACHE);
				}
			}
		}

	# Clean up any cached mails that no longer exist on the server
	foreach $f (keys %cached) {
		if (!$onserver{$f}) {
			unlink($cached{$f} == 1 ? "$cd/$f.headers"
						: "$cd/$f.body");
			}
		}
	}
elsif ($_[2]-&gt;{'type'} == 3) {
	# List an MH directory
	local $md = $_[2]-&gt;{'file'};
	@mail = &amp;list_mhdir($md, $_[0], $_[1], $_[3]);
	}
elsif ($_[2]-&gt;{'type'} == 4) {
	# Get headers and possibly bodies from an IMAP server

	# Login and select the specified mailbox
	local @rv = &amp;imap_login($_[2]);
	if ($rv[0] != 1) {
		# Something went wrong
		if ($_[4]) {
			@{$_[4]} = @rv;
			return ();
			}
		elsif ($rv[0] == 0) { &amp;error($rv[1]); }
		elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
		elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
		}
	local $h = $rv[1];
	local $count = $rv[2];
	return () if (!$count);
	$_[2]-&gt;{'lastchange'} = $rv[3] if ($rv[3]);

	# Work out what range we want
	local ($start, $end) = &amp;compute_start_end($_[0], $_[1], $count);
	@mail = map { undef } (0 .. $count-1);

	# Get the headers or body of messages in the specified range
	local @rv;
	if ($_[3]) {
		# Just the headers
		@rv = &amp;imap_command($h,
			sprintf "FETCH %d:%d (RFC822.SIZE UID FLAGS RFC822.HEADER)",
				$start+1, $end+1);
		}
	else {
		# Whole messages
		@rv = &amp;imap_command($h,
			sprintf "FETCH %d:%d (UID FLAGS BODY.PEEK[])", $start+1, $end+1);
		}

	# Parse the headers or whole messages that came back
	local $i;
	for($i=0; $i&lt;@{$rv[1]}; $i++) {
		# Extract the actual mail part
		local $mail = &amp;parse_imap_mail($rv[1]-&gt;[$i]);
		if ($mail) {
			$mail-&gt;{'idx'} = $start+$i;
			$mail[$start+$i] = $mail;
			}
		}
	}
elsif ($_[2]-&gt;{'type'} == 5) {
	# A composite folder, which combined two or more others.

	# Work out exactly how big the total is
	local ($sf, %len, $count);
	foreach $sf (@{$_[2]-&gt;{'subfolders'}}) {
		print DEBUG "working out size of ",&amp;folder_name($sf),"\n";
		$len{$sf} = &amp;mailbox_folder_size($sf);
		$count += $len{$sf};
		}

	# Work out what range we need
	local ($start, $end) = &amp;compute_start_end($_[0], $_[1], $count);

	# Fetch the needed part of each sub-folder
	local $pos = 0;
	foreach $sf (@{$_[2]-&gt;{'subfolders'}}) {
		local ($sfstart, $sfend);
		local $sfn = &amp;folder_name($sf);
		$sfstart = $start - $pos;
		$sfend = $end - $pos;
		$sfstart = $sfstart &lt; 0 ? 0 :
			   $sfstart &gt;= $len{$sf} ? $len{$sf}-1 : $sfstart;
		$sfend = $sfend &lt; 0 ? 0 :
			 $sfend &gt;= $len{$sf} ? $len{$sf}-1 : $sfend;
		print DEBUG "getting mail from $sfstart to $sfend in $sfn\n";
		local @submail =
			&amp;mailbox_list_mails($sfstart, $sfend, $sf, $_[3]);
		local $sm;
		foreach $sm (@submail) {
			if ($sm) {
				# ID is the original folder and ID
				$sm-&gt;{'id'} = $sfn."\t".$sm-&gt;{'id'};
				}
			}
		push(@mail, @submail);
		$pos += $len{$sf};
		}
	}
elsif ($_[2]-&gt;{'type'} == 6) {
	# A virtual folder, which just contains ids of mails in other folders
	local $mems = $folder-&gt;{'members'};
	local ($start, $end) = &amp;compute_start_end($_[0], $_[1], scalar(@$mems));

	# Build a map from sub-folder names to IDs in them
	local (%wantmap, %namemap);
	for(my $i=$start; $i&lt;=$end; $i++) {
		local $sf = $mems-&gt;[$i]-&gt;[0];
		local $sid = $mems-&gt;[$i]-&gt;[1];
		local $sfn = &amp;folder_name($sf);
		$namemap{$sfn} = $sf;
		push(@{$wantmap{$sfn}}, [ $sid, $i ]);
		}

	# For each sub-folder, get the IDs we need, and put them into the
	# return array at the right place
	@mail = map { undef } (0 .. @$mems-1);
	local $changed = 0;
	foreach my $sfn (keys %wantmap) {
		local $sf = $namemap{$sfn};
		local @wantids = map { $_-&gt;[0] } @{$wantmap{$sfn}};
		local @wantidxs = map { $_-&gt;[1] } @{$wantmap{$sfn}};
		local @sfmail = &amp;mailbox_select_mails($sf, \@wantids, $_[3]);
		for(my $i=0; $i&lt;@sfmail; $i++) {
			$mail[$wantidxs[$i]] = $sfmail[$i];
			if ($sfmail[$i]) {
				# Original mail exists .. add to results
				if ($sfmail[$i]-&gt;{'id'} ne $wantids[$i]) {
					# Under new ID now - fix up index
					print DEBUG "wanted ID ",$wantids[$i],
						" got ",$sfmail[$i]-&gt;{'id'},"\n";
					local ($m) = grep {
						$_-&gt;[1] eq $wantids[$i] } @$mems;
					if ($m) {
						$m-&gt;[1] = $sfmail[$i]-&gt;{'id'};
						$changed = 1;
						}
					}
				$sfmail[$i]-&gt;{'idx'} = $wantidxs[$i];
				$sfmail[$i]-&gt;{'id'} =
					$sfn."\t".$sfmail[$i]-&gt;{'id'};
				}
			else {
				# Take out of virtual folder index
				print DEBUG "underlying email $sfn $wantids[$i] is gone!\n";
				$mems = [ grep { $_-&gt;[0] ne $sf ||
					 $_-&gt;[1] ne $wantids[$i] } @$mems ];
				$changed = 1;
				$mail[$wantidxs[$i]] = 'GONE';
				}
			}
		}
	if ($changed) {
		# Need to save virtual folder
		$folder-&gt;{'members'} = $mems;
		&amp;save_folder($folder, $folder);
		}

	# Filter out messages that don't exist anymore
	@mail = grep { $_ ne 'GONE' } @mail;
	}
elsif ($_[2]-&gt;{'type'} == 7) {
	# MBX format folder
	print DEBUG "listing MBX $_[2]-&gt;{'file'}\n";
	@mail = &amp;list_mbxfile($_[2]-&gt;{'file'}, $_[0], $_[1]);
	}
&amp;switch_from_folder_user($_[2]);
return @mail;
}

# mailbox_select_mails(&amp;folder, &amp;ids, headersonly)
# Returns only messages from a folder with unique IDs in the given array
sub mailbox_select_mails
{
local ($folder, $ids, $headersonly) = @_;
my @mail;
&amp;switch_to_folder_user($_[0]);
if ($folder-&gt;{'type'} == 0) {
	# mbox folder
	@mail = &amp;select_mails($folder-&gt;{'file'}, $ids, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 1) {
	# Maildir folder
	@mail = &amp;select_maildir($folder-&gt;{'file'}, $ids, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 3) {
	# MH folder
	@mail = &amp;select_mhdir($folder-&gt;{'file'}, $ids, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 2) {
	# POP folder

	# Login first
	local @rv = &amp;pop3_login($folder);
	if ($rv[0] != 1) {
		# Failed to connect or login
		if ($_[4]) {
			@{$_[4]} = @rv;
			return ();
			}
		elsif ($rv[0] == 0) { &amp;error($rv[1]); }
		else { &amp;error(&amp;text('save_elogin', $rv[1])); }
		}
	local $h = $rv[1];
	local @uidl = &amp;pop3_uidl($h);
	local %uidlmap;		# Map from UIDLs to POP3 indexes
	for(my $i=0; $i&lt;@uidl; $i++) {
		$uidlmap{$uidl[$i]} = $i+1;
		}

	# Work out what we have cached
	local ($i, $f, %cached, %sizeneed);
	local $cd = &amp;get_folder_cache_directory($_[2]);
	if (opendir(CACHE, $cd)) {
		while($f = readdir(CACHE)) {
			if ($f =~ /^(\S+)\.body$/) {
				$cached{$1} = 2;
				}
			elsif ($f =~ /^(\S+)\.headers$/) {
				$cached{$1} = 1;
				}
			}
		closedir(CACHE);
		}
	else {
		mkdir($cd, 0700);
		}

	# For each requested uidl, get the headers or body
	foreach my $i (@$ids) {
		local $u = &amp;safe_uidl($i);
		print DEBUG "need uidl $i -&gt; $uidlmap{$i}\n";
		if ($cached{$u} == 2 || $cached{$u} == 1 &amp;&amp; $headersonly) {
			# We already have everything that we need
			}
		elsif ($cached{$u} == 1 || !$headersonly) {
			# We need to get the entire mail
			&amp;pop3_command($h, "retr ".$uidlmap{$i});
			open(CACHE, "&gt;", "$cd/$u.body");
			while(&lt;$h&gt;) {
				s/\r//g;
				last if ($_ eq ".\n");
				print CACHE $_;
				}
			close(CACHE);
			unlink("$cd/$u.headers");
			$cached{$u} = 2;
			}
		else {
			# We just need the headers
			&amp;pop3_command($h, "top ".$uidlmap{$i}." 0");
			open(CACHE, "&gt;", "$cd/$u.headers");
			while(&lt;$h&gt;) {
				s/\r//g;
				last if ($_ eq ".\n");
				print CACHE $_;
				}
			close(CACHE);
			$cached{$u} = 1;
			}
		local $mail = &amp;read_mail_file($cached{$u} == 2 ?
				"$cd/$u.body" : "$cd/$u.headers");
		if ($cached{$u} == 1) {
			if ($mail-&gt;{'body'} ne "") {
				$mail-&gt;{'size'} = length($mail-&gt;{'body'});
				}
			else {
				$sizeneed{$uidlmap{$i}} = $mail;
				}
			}
		$mail-&gt;{'idx'} = $uidlmap{$i}-1;
		$mail-&gt;{'id'} = $i;
		push(@mail, $mail);
		}

	# Get sizes for mails if needed
	if (%sizeneed) {
		&amp;pop3_command($h, "list");
		while(&lt;$h&gt;) {
			s/\r//g;
			last if ($_ eq ".\n");
			if (/^(\d+)\s+(\d+)/ &amp;&amp; $sizeneed{$1}) {
				# Find mail in results, and set its size
				local ($ns) = $sizeneed{$1};
				$ns-&gt;{'size'} = $2;
				local $u = &amp;safe_uidl($uidl[$1-1]);
				open(CACHE, "&gt;&gt;", "$cd/$u.headers");
				print CACHE $2,"\n";
				close(CACHE);
				}
			}
		}
	}
elsif ($folder-&gt;{'type'} == 4) {
	# IMAP folder

	# Login and select the specified mailbox
	local @irv = &amp;imap_login($folder);
	if ($irv[0] != 1) {
		# Something went wrong
		if ($_[4]) {
			@{$_[4]} = @irv;
			return ();
			}
		elsif ($irv[0] == 0) { &amp;error($irv[1]); }
		elsif ($irv[0] == 3) { &amp;error(&amp;text('save_emailbox', $irv[1]));}
		elsif ($irv[0] == 2) { &amp;error(&amp;text('save_elogin2', $irv[1])); }
		}
	local $h = $irv[1];
	local $count = $irv[2];
	return () if (!$count);
        $folder-&gt;{'lastchange'} = $irv[3] if ($irv[3]);

	# Build map from IDs to original order, as UID FETCH doesn't return
	# mail in the order we asked for!
	local %wantpos;
	for(my $i=0; $i&lt;@$ids; $i++) {
		$wantpos{$ids-&gt;[$i]} = $i;
		}

	# Fetch each mail by ID. This is done in blocks of 1000, to avoid
	# hitting a the IMAP server's max request limit
	@mail = map { undef } @$ids;
	local $wanted = $headersonly ? "(RFC822.SIZE UID FLAGS RFC822.HEADER)"
				     : "(UID FLAGS BODY.PEEK[])";
	if (@$ids) {
		for(my $chunk=0; $chunk&lt;@$ids; $chunk+=1000) {
			local $chunkend = $chunk+999;
			if ($chunkend &gt;= @$ids) { $chunkend = @$ids-1; }
			local @cids = @$ids[$chunk .. $chunkend];
			local @idxrv = &amp;imap_command($h,
				"UID FETCH ".join(",", @cids)." $wanted");
			foreach my $idxrv (@{idxrv-&gt;[1]}) {
				local $mail = &amp;parse_imap_mail($idxrv);
				if ($mail) {
					$mail-&gt;{'idx'} = $mail-&gt;{'imapidx'}-1;
					$mail[$wantpos{$mail-&gt;{'id'}}] = $mail;
					}
				}
			}
		}
	print DEBUG "imap rv = ",scalar(@mail),"\n";
	}
elsif ($folder-&gt;{'type'} == 5 || $folder-&gt;{'type'} == 6) {
	# Virtual or composite folder .. for each ID, work out the folder and
	# build a map from folders to ID lists
	print DEBUG "selecting ",scalar(@$ids)," ids\n";

	# Build a map from sub-folder names to IDs in them
	my $i = 0;
	my %wantmap;
	foreach my $id (@$ids) {
		local ($sfn, $sid) = split(/\t+/, $id, 2);
		push(@{$wantmap{$sfn}}, [ $sid, $i ]);
		$i++;
		}

	# Build map from sub-folder names to IDs
	my (%namemap, @allids, $mems);
	if ($folder-&gt;{'type'} == 6) {
		# For a virtual folder, we need to find all sub-folders
		$mems = $folder-&gt;{'members'};
		foreach my $m (@$mems) {
			local $sfn = &amp;folder_name($m-&gt;[0]);
			$namemap{$sfn} = $m-&gt;[0];
			push(@allids, $sfn."\t".$m-&gt;[1]);
			}
		}
	else {
		# For a composite, they are simply listed
		foreach my $sf (@{$folder-&gt;{'subfolders'}}) {
			local $sfn = &amp;folder_name($sf);
			$namemap{$sfn} = $sf;
			}
		@allids = &amp;mailbox_idlist($folder);
		}

	# For each sub-folder, get the IDs we need, and put them into the
        # return array at the right place
	@mail = map { undef } @$ids;
	foreach my $sfn (keys %wantmap) {
		local $sf = $namemap{$sfn};
		local @wantids = map { $_-&gt;[0] } @{$wantmap{$sfn}};
		local @wantidxs = map { $_-&gt;[1] } @{$wantmap{$sfn}};
		local @sfmail = &amp;mailbox_select_mails($sf, \@wantids,
						      $headersonly);
		for(my $i=0; $i&lt;@sfmail; $i++) {
			$mail[$wantidxs[$i]] = $sfmail[$i];
			if ($sfmail[$i]) {
				# Original mail exists .. add to results
				$sfmail[$i]-&gt;{'id'} =
					$sfn."\t".$sfmail[$i]-&gt;{'id'};
				$sfmail[$i]-&gt;{'idx'} = &amp;indexof(
					$sfmail[$i]-&gt;{'id'}, @allids);
				print DEBUG "looking for ",$sfmail[$i]-&gt;{'id'}," found at ",$sfmail[$i]-&gt;{'idx'},"\n";
				}
			else {
				# Take out of virtual folder index
				print DEBUG "underlying email $sfn $wantids[$i] is gone!\n";
				$mems = [ grep { $_-&gt;[0] ne $sf ||
					 $_-&gt;[1] ne $wantids[$i] } @$mems ];
				$changed = 1;
				}
			}
		}
	if ($changed &amp;&amp; $folder-&gt;{'type'} == 6) {
		# Need to save virtual folder
		$folder-&gt;{'members'} = $mems;
		&amp;save_folder($folder, $folder);
		}
	}
elsif ($folder-&gt;{'type'} == 7) {
	# MBX folder
	@mail = &amp;select_mbxfile($folder-&gt;{'file'}, $ids, $headersonly);
	}
&amp;switch_from_folder_user($_[0]);
return @mail;
}

# mailbox_get_mail(&amp;folder, id, headersonly)
# Convenience function to get a single mail by ID
sub mailbox_get_mail
{
local ($folder, $id, $headersonly) = @_;
local ($mail) = &amp;mailbox_select_mails($folder, [ $id ], $headersonly);
if ($mail) {
	# Find the sort index for this message
	local ($field, $dir) = &amp;get_sort_field($folder);
	if (!$field || !$folder-&gt;{'sortable'}) {
		# No sorting, so sort index is the opposite of real
		$mail-&gt;{'sortidx'} = &amp;mailbox_folder_size($folder, 1) -
				     $mail-&gt;{'idx'} - 1;
		print DEBUG "idx=$mail-&gt;{'idx'} sortidx=$mail-&gt;{'sortidx'} size=",&amp;mailbox_folder_size($folder, 1),"\n";
		}
	else {
		# Need to extract from sort index
		local @sorter = &amp;build_sorted_ids($folder, $field, $dir);
		$mail-&gt;{'sortidx'} = &amp;indexof($id, @sorter);
		}
	}
return $mail;
}

# mailbox_idlist(&amp;folder)
# Returns a list of IDs of messages in some folder
sub mailbox_idlist
{
local ($folder) = @_;
&amp;switch_to_folder_user($_[0]);
my @idlist;
if ($folder-&gt;{'type'} == 0) {
	# mbox, for which IDs are mail positions
	print DEBUG "starting to get IDs from $folder-&gt;{'file'}\n";
	@idlist = &amp;idlist_mails($folder-&gt;{'file'});
	print DEBUG "got ",scalar(@idlist)," ids\n";
	}
elsif ($folder-&gt;{'type'} == 1) {
	# maildir, for which IDs are filenames
	@idlist = &amp;idlist_maildir($folder-&gt;{'file'});
	}
elsif ($folder-&gt;{'type'} == 2) {
	# pop3, for which IDs are uidls
	local @rv = &amp;pop3_login($folder);
	if ($rv[0] != 1) {
		# Failed to connect or login
		if ($rv[0] == 0) { &amp;error($rv[1]); }
		else { &amp;error(&amp;text('save_elogin', $rv[1])); }
		}
	local $h = $rv[1];
	@idlist = &amp;pop3_uidl($h);
	}
elsif ($folder-&gt;{'type'} == 3) {
	# MH directory, for which IDs are file numbers
	@idlist = &amp;idlist_mhdir($folder-&gt;{'file'});
	}
elsif ($folder-&gt;{'type'} == 4) {
	# IMAP, for which IDs are IMAP UIDs
	local @rv = &amp;imap_login($folder);
	if ($rv[0] != 1) {
		# Something went wrong
		if ($rv[0] == 0) { &amp;error($rv[1]); }
		elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
		elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
		}
	local $h = $rv[1];
	local $count = $rv[2];
	return () if (!$count);
        $folder-&gt;{'lastchange'} = $irv[3] if ($irv[3]);

	@rv = &amp;imap_command($h, "FETCH 1:$count UID");
	foreach my $uid (@{$rv[1]}) {
		if ($uid =~ /UID\s+(\d+)/) {
			push(@idlist, $1);
			}
		}
	}
elsif ($folder-&gt;{'type'} == 5) {
	# Composite, IDs come from sub-folders
	foreach my $sf (@{$folder-&gt;{'subfolders'}}) {
		local $sfn = &amp;folder_name($sf);
		push(@idlist, map { $sfn."\t".$_ } &amp;mailbox_idlist($sf));
		}
	}
elsif ($folder-&gt;{'type'} == 6) {
	# Virtual, IDs come from sub-folders (where they exist)
	my (%wantmap, %namemap);
	foreach my $m (@{$folder-&gt;{'members'}}) {
		local $sf = $m-&gt;[0];
		local $sid = $m-&gt;[1];
		local $sfn = &amp;folder_name($sf);
		push(@{$wantmap{$sfn}}, $sid);
		$namemap{$sfn} = $sf;
		}
	foreach my $sfn (keys %wantmap) {
		local %wantids = map { $_, 1 } @{$wantmap{$sfn}};
		local $sf = $namemap{$sfn};
		foreach my $sfid (&amp;mailbox_idlist($sf)) {
			if ($wantids{$sfid}) {
				push(@idlist, $sfn."\t".$sfid);
				}
			}
		}
	}
&amp;switch_from_folder_user($_[0]);
return @idlist;
}

# compute_start_end(start, end, count)
# Given start and end indexes (which may be negative or undef), returns the
# real mail file indexes.
sub compute_start_end
{
local ($start, $end, $count) = @_;
if (!defined($start)) {
	return (0, $count-1);
	}
elsif ($end &lt; 0) {
	local $rstart = $count+$_[1]-1;
	local $rend = $count+$_[0]-1;
	$rstart = $rstart &lt; 0 ? 0 : $rstart;
	$rend = $count - 1 if ($rend &gt;= $count);
	return ($rstart, $rend);
	}
else {
	local $rend = $_[1];
	$rend = $count - 1 if ($rend &gt;= $count);
	return ($start, $rend);
	}
}

# mailbox_list_mails_sorted(start, end, &amp;folder, [headeronly], [&amp;error],
#			    [sort-field, sort-dir])
# Returns messages in a folder within the given range, but sorted by the
# given field and condition.
sub mailbox_list_mails_sorted
{
local ($start, $end, $folder, $headersonly, $error, $field, $dir) = @_;
print DEBUG "mailbox_list_mails_sorted from $start to $end\n";
if (!$field) {
	# Default to current ordering
	($field, $dir) = &amp;get_sort_field($folder);
	}
if (!$field || !$folder-&gt;{'sortable'}) {
	# No sorting .. just return newest first
	local @rv = reverse(&amp;mailbox_list_mails(
		-$start, -$end-1, $folder, $headersonly, $error));
	local $i = 0;
	foreach my $m (@rv) {
		$m-&gt;{'sortidx'} = $i++;
		}
	return @rv;
	}

# For IMAP, login first so that the lastchange can be found
if ($folder-&gt;{'type'} == 4 &amp;&amp; !$folder-&gt;{'lastchange'}) {
	&amp;mailbox_select_mails($folder, [ ], 1);
	}

# Get a sorted list of IDs, and then find the real emails within the range
local @sorter = &amp;build_sorted_ids($folder, $field, $dir);
($start, $end) = &amp;compute_start_end($start, $end, scalar(@sorter));
print DEBUG "for ",&amp;folder_name($folder)," sorter = ",scalar(@sorter),"\n";
print DEBUG "start = $start end = $end\n";
local @rv = map { undef } (0 .. scalar(@sorter)-1);
local @wantids = map { $sorter[$_] } ($start .. $end);
print DEBUG "wantids = ",scalar(@wantids),"\n";
local @mails = &amp;mailbox_select_mails($folder, \@wantids, $headersonly);
for(my $i=0; $i&lt;@mails; $i++) {
	$rv[$start+$i] = $mails[$i];
	print DEBUG "setting $start+$i to ",$mails[$i]," id ",$wantids[$i],"\n";
	$mails[$i]-&gt;{'sortidx'} = $start+$i;
	}
print DEBUG "rv = ",scalar(@rv),"\n";
return @rv;
}

# build_sorted_ids(&amp;folder, field, dir)
# Returns a list of message IDs in some folder, sorted on some field
sub build_sorted_ids
{
local ($folder, $field, $dir) = @_;

# Delete old sort indexes
&amp;delete_old_sort_index($folder);

# Build or update the sort index. This is a file mapping unique IDs and fields
# to sortable values.
local %index;
&amp;build_new_sort_index($folder, $field, \%index);

# Get message indexes, sorted by the field
my @sorter;
while(my ($k, $v) = each %index) {
	if ($k =~ /^(.*)_\Q$field\E$/) {
		push(@sorter, [ $1, lc($v) ]);
		}
	}
if ($field eq "size" || $field eq "date" || $field eq "x-spam-status") {
	# Numeric sort
	@sorter = sort { my $s = $a-&gt;[1] &lt;=&gt; $b-&gt;[1]; $dir ? $s : -$s } @sorter;
	}
else {
	# Alpha sort
	@sorter = sort { my $s = $a-&gt;[1] cmp $b-&gt;[1]; $dir ? $s : -$s } @sorter;
	}
return map { $_-&gt;[0] } @sorter;
}

# delete_old_sort_index(&amp;folder)
# Delete old index DBM files
sub delete_old_sort_index
{
local ($folder) = @_;
local $ifile = &amp;folder_sort_index_file($folder);
$ifile =~ /^(.*)\/([^\/]+)$/;
local ($idir, $iname) = ($1, $2);
opendir(IDIR, $idir);
foreach my $f (readdir(IDIR)) {
	if ($f eq $iname || $f =~ /^\Q$iname\E\.[^\.]+$/) {
		unlink("$idir/$f");
		}
	}
closedir(IDIR);
}

# build_new_sort_index(&amp;folder, field, &amp;index)
# Builds and/or loads the index for sorting a folder on some field. The
# index uses the mail number as the key, and the field value as the value.
sub build_new_sort_index
{
local ($folder, $field, $index) = @_;
return 0 if (!$folder-&gt;{'sortable'});
local $ifile = &amp;folder_new_sort_index_file($folder);

&amp;open_dbm_db($index, $ifile, 0600);
print DEBUG "indexchange=$index-&gt;{'lastchange'} folderchange=$folder-&gt;{'lastchange'}\n";
if ($index-&gt;{'lastchange'} != $folder-&gt;{'lastchange'} ||
    !$folder-&gt;{'lastchange'}) {
	# The mail file has changed .. get IDs and update the index with any
	# that are missing
	local @ids = &amp;mailbox_idlist($folder);

	# Find IDs that are new
	local @newids;
	foreach my $id (@ids) {
		if (!defined($index-&gt;{$id."_size"})) {
			push(@newids, $id);
			}
		}
	local @mails = scalar(@newids) ?
			&amp;mailbox_select_mails($folder, \@newids, 1) : ( );
	foreach my $mail (@mails) {
		foreach my $f (@index_fields) {
			if ($f eq "date") {
				# Convert date to Unix time
				$index-&gt;{$mail-&gt;{'id'}."_date"} =
				  &amp;parse_mail_date($mail-&gt;{'header'}-&gt;{'date'});
				}
			elsif ($f eq "size") {
				# Get mail size
				$index-&gt;{$mail-&gt;{'id'}."_size"} =
					$mail-&gt;{'size'};
				}
			elsif ($f eq "from" || $f eq "to") {
				# From: header .. convert to display version
				$index-&gt;{$mail-&gt;{'id'}."_".$f} =
					&amp;simplify_from($mail-&gt;{'header'}-&gt;{$f});
				}
			elsif ($f eq "subject") {
				# Convert subject to display version
				$index-&gt;{$mail-&gt;{'id'}."_".$f} =
				    &amp;simplify_subject($mail-&gt;{'header'}-&gt;{$f});
				}
			elsif ($f eq "x-spam-status") {
				# Extract spam score
				$index-&gt;{$mail-&gt;{'id'}."_".$f} =
					$mail-&gt;{'header'}-&gt;{$f} =~ /(hits|score)=([0-9\.]+)/ ? $2 : undef;
				}
			else {
				# Just a header
				$index-&gt;{$mail-&gt;{'id'}."_".$f} =
					$mail-&gt;{'header'}-&gt;{$f};
				}
			}
		}
	print DEBUG "added ",scalar(@mails)," messages to index\n";

	# Remove IDs that no longer exist
	local %ids = map { $_, 1 } (@ids, @wantids);
	local $dc = 0;
	local @todelete;
	while(my ($k, $v) = each %$index) {
		if ($k =~ /^(.*)_([^_]+)$/ &amp;&amp; !$ids{$1}) {
			push(@todelete, $k);
			$dc++ if ($2 eq "size");
			}
		}
	foreach my $k (@todelete) {
		delete($index-&gt;{$k});
		}
	print DEBUG "deleted $dc messages from index\n";

	# Record index update time
	$index-&gt;{'lastchange'} = $folder-&gt;{'lastchange'} || time();
	$index-&gt;{'mailcount'} = scalar(@ids);
	print DEBUG "new indexchange=$index-&gt;{'lastchange'}\n";
	}
return 1;
}

# delete_new_sort_index_message(&amp;folder, id)
# Removes a message ID from a sort index
sub delete_new_sort_index_message
{
local ($folder, $id) = @_;
local %index;
&amp;build_new_sort_index($folder, undef, \%index);
foreach my $field (@index_fields) {
	delete($index{$id."_".$field});
	}
dbmclose(%index);
if ($folder-&gt;{'type'} == 5 || $folder-&gt;{'type'} == 6) {
	# Remove from underlying folder's index too
	local ($sfn, $sid) = split(/\t+/, $id, 2);
	local $sf = &amp;find_subfolder($folder, $sfn);
	if ($sf) {
		&amp;delete_new_sort_index_message($sf, $sid);
		}
	}
}

# force_new_index_recheck(&amp;folder)
# Resets the last-updated time on a folder's index, to force a re-check
sub force_new_index_recheck
{
local ($folder) = @_;
local %index;
&amp;build_new_sort_index($folder, undef, \%index);
$index{'lastchange'} = 0;
dbmclose(%index);
}

# delete_new_sort_index(&amp;folder)
# Trashes the sort index for a folder, to force a rebuild
sub delete_new_sort_index
{
local ($folder) = @_;
local $ifile = &amp;folder_new_sort_index_file($folder);

my %index;
&amp;open_dbm_db(\%index, $ifile, 0600);
%index = ( );
}

# folder_sort_index_file(&amp;folder)
# Returns the index file to use for some folder
sub folder_sort_index_file
{
local ($folder) = @_;
return &amp;user_index_file(($folder-&gt;{'file'} || $folder-&gt;{'id'}).".sort");
}

# folder_new_sort_index_file(&amp;folder)
# Returns the new ID-style index file to use for some folder
sub folder_new_sort_index_file
{
local ($folder) = @_;
return &amp;user_index_file(($folder-&gt;{'file'} || $folder-&gt;{'id'}).".byid");
}

# mailbox_search_mail(&amp;fields, andmode, &amp;folder, [&amp;limit], [headersonly])
# Search a mailbox for multiple matching fields
sub mailbox_search_mail
{
local ($fields, $andmode, $folder, $limit, $headersonly) = @_;

# For folders other than IMAP and composite and mbox where we already have
# an index, build a sort index and use that for
# the search, if it is simple enough (Subject, From and To only)
local @idxfields = grep { $_-&gt;[0] eq 'from' || $_-&gt;[0] eq 'to' ||
                          $_-&gt;[0] eq 'subject' } @{$_[0]};
if ($folder-&gt;{'type'} != 4 &amp;&amp;
    $folder-&gt;{'type'} != 5 &amp;&amp;
    $folder-&gt;{'type'} != 6 &amp;&amp;
    ($folder-&gt;{'type'} != 0 || !&amp;has_dbm_index($folder-&gt;{'file'})) &amp;&amp;
    scalar(@idxfields) == scalar(@$fields) &amp;&amp; @idxfields &amp;&amp;
    &amp;get_product_name() eq 'usermin') {
	print DEBUG "using index to search\n";
	local %index;
	&amp;build_new_sort_index($folder, undef, \%index);
	local @rv;

	# Work out which mail IDs match the requested headers
	local %idxmatches = map { ("$_-&gt;[0]/$_-&gt;[1]", [ ]) } @idxfields;
	while(my ($k, $v) = each %index) {
		$k =~ /^(.+)_(\S+)$/ || next;
                local ($ki, $kf) = ($1, $2);
                next if (!$kf || $ki eq '');

		# Check all of the fields to see which ones match
		foreach my $if (@idxfields) {
			local $iff = $if-&gt;[0];
			local ($neg) = ($iff =~ s/^\!//);
			next if ($kf ne $iff);
			local $re = $if-&gt;[2] ? $if-&gt;[1] : "\Q$if-&gt;[1]\E";
			if (!$neg &amp;&amp; $v =~ /$re/i ||
			    $neg &amp;&amp; $v !~ /$re/i) {
				push(@{$idxmatches{"$if-&gt;[0]/$if-&gt;[1]"}}, $ki);
				}
			}
		}
	local @matches;
	if ($_[1]) {
		# Find indexes in all arrays
		local %icount;
		foreach my $if (keys %idxmatches) {
			foreach my $i (@{$idxmatches{$if}}) {
				$icount{$i}++;
				}
			}
		foreach my $i (keys %icount) {
			}
		local $fif = $idxfields[0];
		@matches = grep { $icount{$_} == scalar(@idxfields) }
				@{$idxmatches{"$fif-&gt;[0]/$fif-&gt;[1]"}};
		}
	else {
		# Find indexes in any array
		foreach my $if (keys %idxmatches) {
			push(@matches, @{$idxmatches{$if}});
			}
		@matches = &amp;unique(@matches);
		}
	@matches = sort { $a cmp $b } @matches;
	print DEBUG "matches = ",join(" ", @matches),"\n";

	# Select the actual mails
	return &amp;mailbox_select_mails($_[2], \@matches, $headersonly);
	}

if ($folder-&gt;{'type'} == 0) {
	# Just search an mbox format file (which will use its own special
	# field-level index)
	return &amp;advanced_search_mail($folder-&gt;{'file'}, $fields,
				     $andmode, $limit, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 1) {
	# Search a maildir directory
	return &amp;advanced_search_maildir($folder-&gt;{'file'}, $fields,
				        $andmode, $limit, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 2) {
	# Get all of the mail from the POP3 server and search it
	local ($min, $max);
	if ($limit &amp;&amp; $limit-&gt;{'latest'}) {
		$min = -1;
		$max = -$limit-&gt;{'latest'};
		}
	local @mails = &amp;mailbox_list_mails($min, $max, $folder,
			&amp;indexof('body', &amp;search_fields($fields)) &lt; 0 &amp;&amp;
			$headersonly);
	local @rv = grep { $_ &amp;&amp; &amp;mail_matches($fields, $andmode, $_) } @mails;
	}
elsif ($folder-&gt;{'type'} == 3) {
	# Search an MH directory
	return &amp;advanced_search_mhdir($folder-&gt;{'file'}, $fields,
				      $andmode, $limit, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 4) {
	# Use IMAP's remote search feature
	local @rv = &amp;imap_login($_[2]);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
	local $h = $rv[1];
        $_[2]-&gt;{'lastchange'} = $rv[3] if ($rv[3]);

	# Do the search to get back a list of matching numbers
	local @search;
	foreach $f (@{$_[0]}) {
		local $field = $f-&gt;[0] eq "date" ? "on" :
			       $f-&gt;[0] eq "all" ? "body" : $f-&gt;[0];
		local $neg = ($field =~ s/^\!//);
		local $what = $f-&gt;[1];
		if ($field ne "size") {
			$what = "\"".$what."\""
			}
		$field = "LARGER" if ($field eq "size");
		local $search;
		if ($field =~ /^X-/i) {
			$search = "header ".uc($field)." ".$what."";
			}
		else {
			$search = uc($field)." ".$what."";
			}
		$search = "NOT $search" if ($neg);
		push(@searches, $search);
		}
	local $searches;
	if (@searches == 1) {
		$searches = $searches[0];
		}
	elsif ($_[1]) {
		$searches = join(" ", @searches);
		}
	else {
		$searches = $searches[$#searches];
		for($i=$#searches-1; $i&gt;=0; $i--) {
			$searches = "or $searches[$i] ($searches)";
			}
		}
	@rv = &amp;imap_command($h, "UID SEARCH $searches");
	&amp;error(&amp;text('save_esearch', $rv[3])) if (!$rv[0]);

	# Get back the IDs we want
	local ($srch) = grep { $_ =~ /^\*\s+SEARCH/i } @{$rv[1]};
	local @ids = split(/\s+/, $srch);
	shift(@ids); shift(@ids);	# lose * SEARCH

	# Call the select function to get the mails
	return &amp;mailbox_select_mails($folder, \@ids, $headersonly);
	}
elsif ($folder-&gt;{'type'} == 5) {
	# Search each sub-folder and combine the results - taking any count
	# limits into effect
	local $sf;
	local $pos = 0;
	local @mail;
	local (%start, %len);
	foreach $sf (@{$folder-&gt;{'subfolders'}}) {
		$len{$sf} = &amp;mailbox_folder_size($sf);
		$start{$sf} = $pos;
		$pos += $len{$sf};
		}
	local $limit = $limit ? { %$limit } : undef;
	$limit = undef;
	foreach $sf (reverse(@{$folder-&gt;{'subfolders'}})) {
		local $sfn = &amp;folder_name($sf);
		print DEBUG "searching on sub-folder ",&amp;folder_name($sf),"\n";
		local @submail = &amp;mailbox_search_mail($fields, $andmode, $sf,
					$limit, $headersonly);
		print DEBUG "found ",scalar(@submail),"\n";
		foreach my $sm (@submail) {
			$sm-&gt;{'id'} = $sfn."\t".$sm-&gt;{'id'};
			}
		push(@mail, reverse(@submail));
		if ($limit &amp;&amp; $limit-&gt;{'latest'}) {
			# Adjust latest down by size of this folder
			$limit-&gt;{'latest'} -= $len{$sf};
			last if ($limit-&gt;{'latest'} &lt;= 0);
			}
		}
	return reverse(@mail);
	}
elsif ($folder-&gt;{'type'} == 6) {
	# Just run a search on the sub-mails
	local @rv;
	local ($min, $max);
	if ($limit &amp;&amp; $limit-&gt;{'latest'}) {
		$min = -1;
		$max = -$limit-&gt;{'latest'};
		}
	local $mail;
	local $sfn = &amp;folder_name($sf);
	print DEBUG "searching virtual folder ",&amp;folder_name($folder),"\n";
	foreach $mail (&amp;mailbox_list_mails($min, $max, $folder)) {
		if ($mail &amp;&amp; &amp;mail_matches($fields, $andmode, $mail)) {
			push(@rv, $mail);
			}
		}
	return @rv;
	}
}

# mailbox_delete_mail(&amp;folder, mail, ...)
# Delete multiple messages from some folder
sub mailbox_delete_mail
{
return undef if (&amp;is_readonly_mode());
local $f = shift(@_);
&amp;switch_to_folder_user($f);
if ($userconfig{'delete_mode'} == 1 &amp;&amp; !$f-&gt;{'trash'} &amp;&amp; !$f-&gt;{'spam'} &amp;&amp;
    !$f-&gt;{'notrash'}) {
	# Copy to trash folder first .. if we have one
	local ($trash) = grep { $_-&gt;{'trash'} } &amp;list_folders();
	if ($trash) {
		my $r;
		my $save_read = &amp;get_product_name() eq "usermin";
		foreach my $m (@_) {
			$r = &amp;get_mail_read($f, $m) if ($save_read);
			my $mcopy = { %$m };	  # Because writing changes id
			&amp;write_mail_folder($mcopy, $trash);
			&amp;set_mail_read($trash, $mcopy, $r) if ($save_read);
			}
		}
	}

if ($f-&gt;{'type'} == 0) {
	# Delete from mbox
	&amp;delete_mail($f-&gt;{'file'}, @_);
	}
elsif ($f-&gt;{'type'} == 1) {
	# Delete from Maildir
	&amp;delete_maildir(@_);
	}
elsif ($f-&gt;{'type'} == 2) {
	# Login and delete from the POP3 server
	local @rv = &amp;pop3_login($f);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin', $rv[1])); }
	local $h = $rv[1];
	local @uidl = &amp;pop3_uidl($h);
	local $m;
	local $cd = &amp;get_folder_cache_directory($f);
	foreach $m (@_) {
		local $idx = &amp;indexof($m-&gt;{'id'}, @uidl);
		if ($idx &gt;= 0) {
			&amp;pop3_command($h, "dele ".($idx+1));
			local $u = &amp;safe_uidl($m-&gt;{'id'});
			unlink("$cd/$u.headers", "$cd/$u.body");
			}
		}
	}
elsif ($f-&gt;{'type'} == 3) {
	# Delete from MH dir
	&amp;delete_mhdir(@_);
	}
elsif ($f-&gt;{'type'} == 4) {
	# Delete from the IMAP server
	local @rv = &amp;imap_login($f);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
	local $h = $rv[1];

	local $m;
	foreach $m (@_) {
		@rv = &amp;imap_command($h, "UID STORE ".$m-&gt;{'id'}.
					" +FLAGS (\\Deleted)");
		&amp;error(&amp;text('save_edelete', $rv[3])) if (!$rv[0]);
		}
	@rv = &amp;imap_command($h, "EXPUNGE");
	&amp;error(&amp;text('save_edelete', $rv[3])) if (!$rv[0]);
	}
elsif ($f-&gt;{'type'} == 5 || $f-&gt;{'type'} == 6) {
	# Delete from underlying folder(s), and from virtual index
	foreach my $sm (@_) {
		local ($sfn, $sid) = split(/\t+/, $sm-&gt;{'id'}, 2);
		local $sf = &amp;find_subfolder($f, $sfn);
		$sf || &amp;error("Failed to find sub-folder named $sfn");
		if ($f-&gt;{'type'} == 5 || $f-&gt;{'type'} == 6 &amp;&amp; $f-&gt;{'delete'}) {
			$sm-&gt;{'id'} = $sid;
			&amp;mailbox_delete_mail($sf, $sm);
			$sm-&gt;{'id'} = $sfn."\t".$sm-&gt;{'id'};
			}
		if ($f-&gt;{'type'} == 6) {
			$f-&gt;{'members'} = [
				grep { $_-&gt;[0] ne $sf ||
				       $_-&gt;[1] ne $sid } @{$f-&gt;{'members'}} ];
			}
		}
	if ($f-&gt;{'type'} == 6) {
		# Save new ID list
		&amp;save_folder($f, $f);
		}
	}
&amp;switch_from_folder_user($f);

# Always force a re-check of the index when deleting, as we may not detect
# the change (especially for IMAP, where UIDNEXT may not change). This isn't
# needed for Maildir or MH, as indexing is reliable enough
if ($f-&gt;{'type'} != 1 &amp;&amp; $f-&gt;{'type'} != 3) {
	&amp;force_new_index_recheck($f);
	}
}

# mailbox_empty_folder(&amp;folder)
# Remove the entire contents of a mail folder
sub mailbox_empty_folder
{
return undef if (&amp;is_readonly_mode());
local $f = $_[0];
&amp;switch_to_folder_user($f);
if ($f-&gt;{'type'} == 0) {
	# mbox format mail file
	&amp;empty_mail($f-&gt;{'file'});
	}
elsif ($f-&gt;{'type'} == 1) {
	# qmail format maildir
	&amp;empty_maildir($f-&gt;{'file'});
	}
elsif ($f-&gt;{'type'} == 2) {
	# POP3 server .. delete all messages
	local @rv = &amp;pop3_login($f);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin', $rv[1])); }
	local $h = $rv[1];
	@rv = &amp;pop3_command($h, "stat");
	$rv[1] =~ /^(\d+)/ || return;
	local $count = $1;
	local $i;
	for($i=1; $i&lt;=$count; $i++) {
		&amp;pop3_command($h, "dele ".$i);
		}
	}
elsif ($f-&gt;{'type'} == 3) {
	# mh format maildir
	&amp;empty_mhdir($f-&gt;{'file'});
	}
elsif ($f-&gt;{'type'} == 4) {
	# IMAP server .. delete all messages
	local @rv = &amp;imap_login($f);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
	local $h = $rv[1];
	local $count = $rv[2];
	local $i;
	for($i=1; $i&lt;=$count; $i++) {
		@rv = &amp;imap_command($h, "STORE ".$i.
					" +FLAGS (\\Deleted)");
		&amp;error(&amp;text('save_edelete', $rv[3])) if (!$rv[0]);
		}
	@rv = &amp;imap_command($h, "EXPUNGE");
	&amp;error(&amp;text('save_edelete', $rv[3])) if (!$rv[0]);
	}
elsif ($f-&gt;{'type'} == 5) {
	# Empty each sub-folder
	local $sf;
	foreach $sf (@{$f-&gt;{'subfolders'}}) {
		&amp;mailbox_empty_folder($sf);
		}
	}
elsif ($f-&gt;{'type'} == 6) {
	if ($folder-&gt;{'delete'}) {
		# Delete all underlying messages
		local @dmails = &amp;mailbox_list_mails(undef, undef, $f, 1);
		&amp;mailbox_delete_mail($f, @dmails);
		}
	else {
		# Clear the virtual index
		$f-&gt;{'members'} = [ ];
		&amp;save_folder($f);
		}
	}
&amp;switch_from_folder_user($f);

# Trash the folder index
if ($folder-&gt;{'sortable'}) {
	&amp;delete_new_sort_index($folder);
	}
}

# mailbox_copy_folder(&amp;source, &amp;dest)
# Copy all messages from one folder to another. This is done in an optimized
# way if possible.
sub mailbox_copy_folder
{
local ($src, $dest) = @_;
if ($src-&gt;{'type'} == 0 &amp;&amp; $dest-&gt;{'type'} == 0) {
	# mbox to mbox .. just read and write the files
	&amp;switch_to_folder_user($src);
	&amp;open_as_mail_user(SOURCE, $src-&gt;{'file'});
	&amp;switch_from_folder_user($src);
	&amp;switch_to_folder_user($dest);
	&amp;open_as_mail_user(DEST, "&gt;&gt;$dest-&gt;{'file'}");
	while(read(SOURCE, $buf, 32768) &gt; 0) {
		print DEST $buf;
		}
	close(DEST);
	close(SOURCE);
	&amp;switch_from_folder_user($dest);
	}
elsif ($src-&gt;{'type'} == 1 &amp;&amp; $dest-&gt;{'type'} == 1) {
	# maildir to maildir .. just copy the files
	local @files = &amp;get_maildir_files($src-&gt;{'file'});
	foreach my $f (@files) {
		local $fn = &amp;unique_maildir_filename($dest);
		&amp;copy_source_dest_as_mail_user($f, "$dest-&gt;{'file'}/$fn");
		}
	&amp;mailbox_fix_permissions($dest);
	}
elsif ($src-&gt;{'type'} == 1 &amp;&amp; $dest-&gt;{'type'} == 0) {
	# maildir to mbox .. append all the files
	&amp;switch_to_folder_user($dest);
	&amp;open_as_mail_user(DEST, "&gt;&gt;$dest-&gt;{'file'}");
	&amp;switch_from_folder_user($dest);
	local $fromline = &amp;make_from_line("webmin\@example.com")."\n";
	&amp;switch_to_folder_user($src);
	local @files = &amp;get_maildir_files($src-&gt;{'file'});
	foreach my $f (@files) {
		&amp;open_as_mail_user(SOURCE, $f);
		print DEST $fromline;
		while(read(SOURCE, $buf, 1024) &gt; 0) {
			print DEST $buf;
			}
		close(SOURCE);
		}
	close(DEST);
	&amp;switch_from_folder_user($src);
	}
else {
	# read in all mail and write out, in 100 message blocks
	local $max = &amp;mailbox_folder_size($src);
	for(my $s=0; $s&lt;$max; $s+=100) {
		local $e = $s+99;
		$e = $max-1 if ($e &gt;= $max);
		local @mail = &amp;mailbox_list_mails($s, $e, $src);
		local @want = @mail[$s..$e];
		&amp;mailbox_copy_mail($src, $dest, @want);
		}
	}
}

# mailbox_move_mail(&amp;source, &amp;dest, mail, ...)
# Move mail from one folder to another
sub mailbox_move_mail
{
return undef if (&amp;is_readonly_mode());
local $src = shift(@_);
local $dst = shift(@_);
local $now = time();
local $hn = &amp;get_system_hostname();
local $fix_index;
if (($src-&gt;{'type'} == 1 || $src-&gt;{'type'} == 3) &amp;&amp; $dst-&gt;{'type'} == 1) {
	# Can just move mail files to Maildir names
	if ($src-&gt;{'user'} eq $dst-&gt;{'user'}) {
		&amp;switch_to_folder_user($dst);
		}
	&amp;create_folder_maildir($dst);
	local $dd = $dst-&gt;{'file'};
	foreach my $m (@_) {
		&amp;rename_as_mail_user($m-&gt;{'file'}, "$dd/cur/$now.$$.$hn");
		$now++;
		}
	&amp;mailbox_fix_permissions($dst);
	if ($src-&gt;{'user'} eq $dst-&gt;{'user'}) {
		&amp;switch_from_folder_user($dst);
		}
	$fix_index = 1;
	}
elsif (($src-&gt;{'type'} == 1 || $src-&gt;{'type'} == 3) &amp;&amp; $dst-&gt;{'type'} == 3) {
	# Can move and rename to MH numbering
	if ($src-&gt;{'user'} eq $dst-&gt;{'user'}) {
		&amp;switch_to_folder_user($dst);
		}
	&amp;create_folder_maildir($dst);
	local $dd = $dst-&gt;{'file'};
	local $num = &amp;max_mhdir($dst-&gt;{'file'}) + 1;
	foreach my $m (@_) {
		&amp;rename_as_mail_user($m-&gt;{'file'}, "$dd/$num");
		$num++;
		}
	&amp;mailbox_fix_permissions($dst);
	if ($src-&gt;{'user'} eq $dst-&gt;{'user'}) {
		&amp;switch_from_folder_user($dst);
		}
	$fix_index = 1;
	}
else {
	# Append to new folder file, or create in folder directory
	my @mdel;
	my $r;
	my $save_read = &amp;get_product_name() eq "usermin";
	&amp;switch_to_folder_user($dst);
	&amp;create_folder_maildir($dst);
	foreach my $m (@_) {
		$r = &amp;get_mail_read($src, $m) if ($save_read);
		my $mcopy = { %$m };
		&amp;write_mail_folder($mcopy, $dst);
		&amp;set_mail_read($dst, $mcopy, $r) if ($save_read);
		push(@mdel, $m);
		}
	local $src-&gt;{'notrash'} = 1;	# Prevent saving to trash
	&amp;switch_from_folder_user($dst);
	&amp;mailbox_delete_mail($src, @mdel);
	}
}

# mailbox_fix_permissions(&amp;folder, [&amp;stat])
# Set the ownership on all files in a folder correctly, either based on its
# current stat or a structure passed in.
sub mailbox_fix_permissions
{
local ($f, $st) = @_;
return 0 if ($&lt; != 0);			# Only makes sense when running as root
return 0 if ($main::mail_open_user);	# File ops are already done as the
					# correct user
$st ||= [ stat($f-&gt;{'file'}) ];
if ($f-&gt;{'type'} == 0) {
	# Set perms on a single file
	&amp;set_ownership_permissions($st-&gt;[4], $st-&gt;[5], $st-&gt;[2], $f-&gt;{'file'});
	return 1;
	}
elsif ($f-&gt;{'type'} == 1 || $f-&gt;{'type'} == 3) {
	# Do a whole directory
	&amp;execute_command("chown -R $st-&gt;[4]:$st-&gt;[5] ".
			 quotemeta($dst-&gt;{'file'}));
	return 1;
	}
return 0;
}

# mailbox_move_folder(&amp;source, &amp;dest)
# Moves all mail from one folder to another, possibly converting the type
sub mailbox_move_folder
{
local ($src, $dst) = @_;
return undef if (&amp;is_readonly_mode());
&amp;switch_to_folder_user($dst);
if ($src-&gt;{'type'} == $dst-&gt;{'type'} &amp;&amp; !$src-&gt;{'remote'}) {
	# Can just move the file or dir
	local @st = stat($src-&gt;{'file'});
	&amp;unlink_file($dst-&gt;{'file'});
	&amp;rename_as_mail_user($src-&gt;{'file'}, $dst-&gt;{'file'});
	if (@st) {
		&amp;mailbox_fix_permissions($dst, \@st);
		}
	}
elsif (($src-&gt;{'type'} == 1 || $src-&gt;{'type'} == 3) &amp;&amp; $dst-&gt;{'type'} == 0) {
	# For Maildir or MH to mbox moves, just append files
	local @files = $src-&gt;{'type'} == 1 ? &amp;get_maildir_files($src-&gt;{'file'})
					   : &amp;get_mhdir_files($src-&gt;{'file'});
	&amp;open_as_mail_user(DEST, "&gt;&gt;$dst-&gt;{'file'}");
	local $fromline = &amp;make_from_line("webmin\@example.com");
	foreach my $f (@files) {
		&amp;open_as_mail_user(SOURCE, $f);
		print DEST $fromline;
		while(read(SOURCE, $buf, 32768) &gt; 0) {
			print DEST $buf;
			}
		close(SOURCE);
		&amp;unlink_as_mail_user($f);
		}
	close(DEST);
	}
else {
	# Need to read in and write out. But do it in 1000-message blocks
	local $count = &amp;mailbox_folder_size($src);
	local $step = 1000;
	for(my $start=0; $start&lt;$count; $start+=$step) {
		local $end = $start + $step - 1;
		$end = $count-1 if ($end &gt;= $count);
		local @mails = &amp;mailbox_list_mails($start, $end, $src);
		@mails = @mails[$start..$end];
		&amp;mailbox_copy_mail($src, $dst, @mails);
		}
	&amp;mailbox_empty_folder($src);
	}
&amp;switch_from_folder_user($dst);

# Delete source folder index
if ($src-&gt;{'sortable'}) {
	&amp;delete_new_sort_index($src);
	}
}

# mailbox_copy_mail(&amp;source, &amp;dest, mail, ...)
# Copy mail from one folder to another
sub mailbox_copy_mail
{
return undef if (&amp;is_readonly_mode());
local $src = shift(@_);
local $dst = shift(@_);
local $now = time();
if ($src-&gt;{'type'} == 6 &amp;&amp; $dst-&gt;{'type'} == 6) {
	# Copying from one virtual folder to another, so just copy the
	# reference
	foreach my $m (@_) {
		push(@{$dst-&gt;{'members'}}, [ $m-&gt;{'subfolder'}, $m-&gt;{'subid'},
					     $m-&gt;{'header'}-&gt;{'message-id'} ]);
		}
	}
elsif ($dst-&gt;{'type'} == 6) {
	# Add this mail to the index of the virtual folder
	foreach my $m (@_) {
		push(@{$dst-&gt;{'members'}}, [ $src, $m-&gt;{'idx'},
					     $m-&gt;{'header'}-&gt;{'message-id'} ]);
		}
	&amp;save_folder($dst);
	}
else {
	# Just write to destination folder. The read status is preserved, but
	# only if in Usermin.
	my $r;
	my $save_read = &amp;get_product_name() eq "usermin";
	&amp;switch_to_folder_user($dst);
	&amp;create_folder_maildir($dst);
	foreach my $m (@_) {
		$r = &amp;get_mail_read($src, $m) if ($save_read);
		my $mcopy = { %$m };
		&amp;write_mail_folder($mcopy, $dst);
		&amp;set_mail_read($dst, $mcopy, $r) if ($save_read);
		}
	&amp;switch_from_folder_user($dst);
	}
}

# folder_type(file_or_dir)
# Returns a numeric folder type based on the contents
sub folder_type
{
my ($f) = @_;
if (-d "$f/cur") {
	# Maildir directory
	return 1;
	}
elsif (-d $f) {
	# MH directory
	return 3;
	}
else {
	# Check for MBX format
	open(MBXTEST, "&lt;", $f);
	my $first;
	read(MBXTEST, $first, 5);
	close(MBXTEST);
	return $first eq "*mbx*" ? 7 : 0;
	}
}

# create_folder_maildir(&amp;folder)
# Ensure that a maildir folder has the needed new, cur and tmp directories
sub create_folder_maildir
{
if ($folders_dir) {
	mkdir($folders_dir, 0700);
	}
if ($_[0]-&gt;{'type'} == 1) {
	local $id = $_[0]-&gt;{'file'};
	&amp;mkdir_as_mail_user($id, 0700);
	&amp;mkdir_as_mail_user("$id/cur", 0700);
	&amp;mkdir_as_mail_user("$id/new", 0700);
	&amp;mkdir_as_mail_user("$id/tmp", 0700);
	}
}

# write_mail_folder(&amp;mail, &amp;folder, textonly)
# Writes some mail message to a folder
sub write_mail_folder
{
return undef if (&amp;is_readonly_mode());
&amp;switch_to_folder_user($_[1]);
&amp;create_folder_maildir($_[1]);
local $needid;
if ($_[1]-&gt;{'type'} == 1) {
	# Add to a maildir directory. ID is set by write_maildir to the new
	# relative filename
	local $md = $_[1]-&gt;{'file'};
	&amp;write_maildir($_[0], $md, $_[2]);
	}
elsif ($_[1]-&gt;{'type'} == 3) {
	# Create a new MH file. ID is just the new message number
	local $num = &amp;max_mhdir($_[1]-&gt;{'file'}) + 1;
	local $md = $_[1]-&gt;{'file'};
	local @st = stat($_[1]-&gt;{'file'});
	&amp;send_mail($_[0], "$md/$num", $_[2], 1);
	if ($&lt; == 0) {
		&amp;set_ownership_permissions($st[4], $st[5], undef, "$md/$num");
		}
	$_[0]-&gt;{'id'} = $num;
	}
elsif ($_[1]-&gt;{'type'} == 0) {
	# Just append to the folder file.
	&amp;send_mail($_[0], $_[1]-&gt;{'file'}, $_[2], 1);
	$needid = 1;
	}
elsif ($_[1]-&gt;{'type'} == 4) {
	# Upload to the IMAP server
	local @rv = &amp;imap_login($_[1]);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
	local $h = $rv[1];

	# Create a temp file and use it to create the IMAP command
	local $temp = &amp;transname();
	&amp;send_mail($_[0], $temp, $_[2], 0, "dummy");
	local $text = &amp;read_file_contents($temp);
	unlink($temp);
	$text =~ s/^From.*\r?\n//;	# Not part of IMAP format
	@rv = &amp;imap_command($h, sprintf "APPEND \"%s\" {%d}\r\n%s",
			$_[1]-&gt;{'mailbox'} || "INBOX", length($text), $text);
	&amp;error(&amp;text('save_eappend', $rv[3])) if (!$rv[0]);
	$needid = 1;
	}
elsif ($_[1]-&gt;{'type'} == 5) {
	# Just append to the last subfolder
	local @sf = @{$_[1]-&gt;{'subfolders'}};
	&amp;write_mail_folder($_[0], $sf[$#sf], $_[2]);
	$needid = 1;
	}
elsif ($_[1]-&gt;{'type'} == 6) {
	# Add mail to first sub-folder, and to virtual index
	# XXX not done
	&amp;error("Cannot add mail to virtual folders");
	}
&amp;switch_from_folder_user($_[1]);
if ($needid) {
	# Get the ID of the new mail
	local @idlist = &amp;mailbox_idlist($_[1]);
	print DEBUG "new idlist=",join(" ", @idlist),"\n";
	$_[0]-&gt;{'id'} = $idlist[$#idlist];
	}
}

# mailbox_modify_mail(&amp;oldmail, &amp;newmail, &amp;folder, textonly)
# Replaces some mail message with a new one
sub mailbox_modify_mail
{
local ($oldmail, $mail, $folder, $textonly) = @_;
return undef if (&amp;is_readonly_mode());
&amp;switch_to_folder_user($_[2]);
if ($folder-&gt;{'type'} == 1) {
	# Just replace the existing file
	&amp;modify_maildir($oldmail, $mail, $textonly);
	}
elsif ($folder-&gt;{'type'} == 3) {
	# Just replace the existing file
	&amp;modify_mhdir($oldmail, $mail, $textonly);
	}
elsif ($folder-&gt;{'type'} == 0) {
	# Modify the mail file
	&amp;modify_mail($folder-&gt;{'file'}, $oldmail, $mail, $textonly);
	}
elsif ($folder-&gt;{'type'} == 5 || $folder-&gt;{'type'} == 6) {
	# Modify in the underlying folder
	local ($oldsfn, $oldsid) = split(/\t+/, $oldmail-&gt;{'id'}, 2);
	local ($sfn, $sid) = split(/\t+/, $mail-&gt;{'id'}, 2);
	local $sf = &amp;find_subfolder($folder, $sfn);
	$oldmail-&gt;{'id'} = $oldsid;
	$mail-&gt;{'id'} = $sid;
	&amp;mailbox_modify_mail($oldmail, $mail, $sf, $textonly);
	$oldmail-&gt;{'id'} = $oldsfn."\t".$oldsid;
	$mail-&gt;{'id'} = $sfn."\t".$sid;
	}
else {
	&amp;error("Cannot modify mail in this type of folder!");
	}
&amp;switch_from_folder_user($_[2]);

# Delete the message being modified from its index, to force re-generation
# with new details
$mail-&gt;{'id'} = $oldmail-&gt;{'id'};	# Assume that it will replace the old
if ($folder-&gt;{'sortable'}) {
	&amp;delete_new_sort_index_message($folder, $mail-&gt;{'id'});
	}
}

# mailbox_folder_size(&amp;folder, [estimate])
# Returns the number of messages in some folder
sub mailbox_folder_size
{
local ($f, $est) = @_;
&amp;switch_to_folder_user($f);
local $rv;
if ($f-&gt;{'type'} == 0) {
	# A mbox formatted file
	$rv = &amp;count_mail($f-&gt;{'file'});
	}
elsif ($f-&gt;{'type'} == 1) {
	# A qmail maildir
	$rv = &amp;count_maildir($f-&gt;{'file'});
	}
elsif ($f-&gt;{'type'} == 2) {
	# A POP3 server
	local @rv = &amp;pop3_login($f);
	if ($rv[0] != 1) {
		if ($rv[0] == 0) { &amp;error($rv[1]); }
		else { &amp;error(&amp;text('save_elogin', $rv[1])); }
		}
	local @st = &amp;pop3_command($rv[1], "stat");
	if ($st[0] == 1) {
		local ($count, $size) = split(/\s+/, $st[1]);
		return $count;
		}
	else {
		&amp;error($st[1]);
		}
	}
elsif ($f-&gt;{'type'} == 3) {
	# An MH directory
	$rv = &amp;count_mhdir($f-&gt;{'file'});
	}
elsif ($f-&gt;{'type'} == 4) {
	# An IMAP server
	local @rv = &amp;imap_login($f);
	if ($rv[0] != 1) {
		if ($rv[0] == 0) { &amp;error($rv[1]); }
		elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
		elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
		}
        $f-&gt;{'lastchange'} = $rv[3];
	$rv = $rv[2];
	}
elsif ($f-&gt;{'type'} == 5) {
	# A composite folder - the size is just that of the sub-folders
	$rv = 0;
	foreach my $sf (@{$f-&gt;{'subfolders'}}) {
		$rv += &amp;mailbox_folder_size($sf);
		}
	}
elsif ($f-&gt;{'type'} == 6 &amp;&amp; !$est) {
	# A virtual folder .. we need to exclude messages that no longer
	# exist in the parent folders
	$rv = 0;
	foreach my $msg (@{$f-&gt;{'members'}}) {
		if (&amp;mailbox_get_mail($msg-&gt;[0], $msg-&gt;[1])) {
			$rv++;
			}
		}
	}
elsif ($f-&gt;{'type'} == 6 &amp;&amp; $est) {
	# A virtual folder .. but we can just use the last member count
	$rv = scalar(@{$f-&gt;{'members'}});
	}
&amp;switch_from_folder_user($f);
return $rv;
}

# mailbox_folder_unread(&amp;folder)
# Returns the total messages in some folder, the number unread and the number
# flagged as special.
sub mailbox_folder_unread
{
local ($folder) = @_;
if ($folder-&gt;{'type'} == 4) {
	# For IMAP, the server knows
	local @rv = &amp;imap_login($folder);
	if ($rv[0] != 1) {
		return ( );
		}
	local @data = ( $rv[2] );
	local $h = $rv[1];
	foreach my $s ("UNSEEN", "FLAGGED") {
		@rv = &amp;imap_command($h, "SEARCH ".$s);
		local ($srch) = grep { $_ =~ /^\*\s+SEARCH/i } @{$rv[1]};
		local @ids = split(/\s+/, $srch);
		shift(@ids); shift(@ids);	# lose * SEARCH
		push(@data, scalar(@ids));
		}
	return @data;
	}
elsif ($folder-&gt;{'type'} == 5) {
	# Composite folder - counts are sums of sub-folders
	local @data;
	foreach my $sf (@{$folder-&gt;{'subfolders'}}) {
		local @sfdata = &amp;mailbox_folder_unread($sf);
		if (scalar(@sfdata)) {
			$data[0] += $sfdata[0];
			$data[1] += $sfdata[1];
			$data[2] += $sfdata[2];
			}
		}
	return @data;
	}
else {
	# For all other folders, just check individual messages
	# XXX faster for maildir?
	local @data = ( 0, 0, 0 );
	local @mails;
	eval {
		$main::error_must_die = 1;
		@mails = &amp;mailbox_list_mails(undef, undef, $folder, 1);
		};
	return ( ) if ($@);
	foreach my $m (@mails) {
		local $rf = &amp;get_mail_read($folder, $m);
		if ($rf == 2) {
			$data[2]++;
			}
		elsif ($rf == 0) {
			$data[1]++;
			}
		$data[0]++;
		}
	return @data;
	}
}

# mailbox_set_read_flags(&amp;folder, &amp;mail, read, special, replied)
# Updates the status flags on some message
sub mailbox_set_read_flag
{
local ($folder, $mail, $read, $special, $replied) = @_;
if ($folder-&gt;{'type'} == 4) {
	# Set flags on IMAP server
	local @rv = &amp;imap_login($folder);
	if ($rv[0] == 0) { &amp;error($rv[1]); }
	elsif ($rv[0] == 3) { &amp;error(&amp;text('save_emailbox', $rv[1])); }
	elsif ($rv[0] == 2) { &amp;error(&amp;text('save_elogin2', $rv[1])); }
	local $h = $rv[1];
	foreach my $f ([ $read, "\\Seen" ],
		       [ $special, "\\Flagged" ],
		       [ $replied, "\\Answered" ]) {
		print DEBUG "setting '$f-&gt;[0]' '$f-&gt;[1]' for $mail-&gt;{'id'}\n";
		next if (!defined($f-&gt;[0]));
		local $pm = $f-&gt;[0] ? "+" : "-";
		@rv = &amp;imap_command($h, "UID STORE ".$mail-&gt;{'id'}.
					" ".$pm."FLAGS (".$f-&gt;[1].")");
		&amp;error(&amp;text('save_eflag', $rv[3])) if (!$rv[0]);
		}
	}
elsif ($folder-&gt;{'type'} == 1) {
	# Add flag to special characters at end of filename
	my $file = $mail-&gt;{'file'} || $mail-&gt;{'id'};
	my $path;
	if (!$mail-&gt;{'file'}) {
		$path = "$folder-&gt;{'file'}/";
		}
	my ($base, %flags);
	if ($file =~ /^(.*):2,([A-Z]*)$/) {
		$base = $1;
		%flags = map { $_, 1 } split(//, $2);
		}
	else {
		$base = $file;
		}
	$flags{'S'} = $read;
	$flags{'F'} = $special;
	$flags{'R'} = $replied if (defined($replied));
	my $newfile = $base.":2,".
			 join("", grep { $flags{$_} } sort(keys %flags));
	if ($newfile ne $file) {
		# Need to rename file
		rename("$path$file", "$path$newfile");
		$newfile =~ s/^(.*)\/((cur|tmp|new)\/.*)$/$2/;
		$mail-&gt;{'id'} = $newfile;
		&amp;flush_maildir_cachefile($folder-&gt;{'file'});
		}
	}
else {
	&amp;error("Read flags cannot be set on folders of type $folder-&gt;{'type'}");
	}

# Update the mail object too
$mail-&gt;{'read'} = $read if (defined($read));
$mail-&gt;{'special'} = $special if (defined($special));
$mail-&gt;{'replied'} = $replied if (defined($replied));
}

# pop3_login(&amp;folder)
# Logs into a POP3 server and returns a status (1=ok, 0=connect failed,
# 2=login failed) and handle or error message
sub pop3_login
{
local $h = $pop3_login_handle{$_[0]-&gt;{'id'}};
return (1, $h) if ($h);
$h = "POP3".time().++$pop3_login_count;
local $error;
&amp;open_socket($_[0]-&gt;{'server'}, $_[0]-&gt;{'port'} || 110, $h, \$error);
print DEBUG "pop3 open_socket to $_[0]-&gt;{'server'} : $error\n";
return (0, $error) if ($error);
local $os = select($h); $| = 1; select($os);
local @rv = &amp;pop3_command($h);
return (0, $rv[1]) if (!$rv[0]);
local $user = $_[0]-&gt;{'user'} eq '*' ? $remote_user : $_[0]-&gt;{'user'};
@rv = &amp;pop3_command($h, "user $user");
return (2, $rv[1]) if (!$rv[0]);
@rv = &amp;pop3_command($h, "pass $_[0]-&gt;{'pass'}");
return (2, $rv[1]) if (!$rv[0]);
return (1, $pop3_login_handle{$_[0]-&gt;{'id'}} = $h);
}

# pop3_command(handle, command)
# Executes a command and returns the status (1 or 0 for OK or ERR) and message
sub pop3_command
{
local ($h, $c) = @_;
print $h "$c\r\n" if ($c);
local $rv = &lt;$h&gt;;
$rv =~ s/\r|\n//g;
print DEBUG "pop3 $c -&gt; $rv\n";
return !$rv ? ( 0, "Connection closed" ) :
       $rv =~ /^\+OK\s*(.*)/ ? ( 1, $1 ) :
       $rv =~ /^\-ERR\s*(.*)/ ? ( 0, $1 ) : ( 0, $rv );
}

# pop3_logout(handle, doquit)
sub pop3_logout
{
local @rv = $_[1] ? &amp;pop3_command($_[0], "quit") : (1, undef);
local $f;
foreach $f (keys %pop3_login_handle) {
	delete($pop3_login_handle{$f}) if ($pop3_login_handle{$f} eq $_[0]);
	}
close($_[0]);
return @rv;
}

# pop3_uidl(handle)
# Returns the uidl list
sub pop3_uidl
{
local @rv;
local $h = $_[0];
local @urv = &amp;pop3_command($h, "uidl");
if (!$urv[0] &amp;&amp; $urv[1] =~ /not\s+implemented/i) {
	# UIDL is not available?! Use numeric list instead
	&amp;pop3_command($h, "list");
	while(&lt;$h&gt;) {
		s/\r//g;
		last if ($_ eq ".\n");
		if (/^(\d+)\s+(\d+)/) {
			push(@rv, "size$2");
			}
		}
	}
elsif (!$urv[0]) {
	&amp;error("uidl failed! $urv[1]") if (!$urv[0]);
	}
else {
	# Can get normal UIDL list
	while(&lt;$h&gt;) {
		s/\r//g;
		last if ($_ eq ".\n");
		if (/^(\d+)\s+(\S+)/) {
			push(@rv, $2);
			}
		}
	}
return @rv;
}

# pop3_logout_all()
# Properly closes all open POP3 and IMAP sessions
sub pop3_logout_all
{
local $f;
foreach $f (keys %pop3_login_handle) {
	&amp;pop3_logout($pop3_login_handle{$f}, 1);
	}
foreach $f (keys %imap_login_handle) {
	&amp;imap_logout($imap_login_handle{$f}, 1);
	}
}

# imap_login(&amp;folder)
# Logs into a POP3 server, selects a mailbox and returns a status
# (1=ok, 0=connect failed, 2=login failed, 3=mailbox error), a handle or error
# message, the number of messages in the mailbox, the next UID, the number
# unread, and the number special.
sub imap_login
{
local ($folder) = @_;
local $key = join("/", $folder-&gt;{'server'}, $folder-&gt;{'port'},
		       $folder-&gt;{'user'});
local $h = $imap_login_handle{$key};
local @rv;
if (!$h) {
	# Need to open socket
	$h = "IMAP".time().++$imap_login_count;
	local $error;
	print DEBUG "Connecting to IMAP server $folder-&gt;{'server'}:$folder-&gt;{'port'}\n";
	&amp;open_socket($folder-&gt;{'server'}, $folder-&gt;{'port'} || $imap_port,
		     $h, \$error);
	print DEBUG "IMAP error=$error\n" if ($error);
	return (0, $error) if ($error);
	local $os = select($h); $| = 1; select($os);

	# Login normally
	@rv = &amp;imap_command($h);
	return (0, $rv[3]) if (!$rv[0]);
	local $user = $folder-&gt;{'user'} eq '*' ? $remote_user
					       : $folder-&gt;{'user'};
	local $pass = $folder-&gt;{'pass'};
	$pass =~ s/\\/\\\\/g;
	$pass =~ s/"/\\"/g;
	@rv = &amp;imap_command($h,"login \"$user\" \"$pass\"");
	return (2, $rv[3]) if (!$rv[0]);

	$imap_login_handle{$key} = $h;
	}

# Select the right folder (if one was given)
@rv = &amp;imap_command($h, "select \"".($folder-&gt;{'mailbox'} || "INBOX")."\"");
return (3, $rv[3]) if (!$rv[0]);
local $count = $rv[2] =~ /\*\s+(\d+)\s+EXISTS/i ? $1 : undef;
local $uidnext = $rv[2] =~ /UIDNEXT\s+(\d+)/ ? $1 : undef;
return (1, $h, $count, $uidnext);
}

# imap_command(handle, command)
# Executes an IMAP command and returns 1 for success or 0 for failure, and
# a reference to an array of results (some of which may be multiline), and
# all of the results joined together, and the stuff after OK/BAD
sub imap_command
{
local ($h, $c) = @_;
if (!$h) {
	local $err = "Invalid IMAP handle";
	return (0, [ $err ], $err, $err);
	}
local @rv;

# Send the command, and read lines until a non-* one is found
local $id = $$."-".$imap_command_count++;
local ($first, $rest) = split(/\r?\n/, $c, 2);
if ($rest) {
	# Multi-line - send first line, then wait for continuation, then rest
	print $h "$id $first\r\n";
	print DEBUG "imap command $id $first\n";
	local $l = &lt;$h&gt;;
	print DEBUG "imap line $l";
	if ($l =~ /^\+/) {
		print $h $rest."\r\n";
		}
	else {
		local $err = "Server did not ask for continuation : $l";
		return (0, [ $err ], $err, $err);
		}
	}
elsif ($c) {
	print $h "$id $c\r\n";
	print DEBUG "imap command $id $c\n";
	}
while(1) {
	local $l = &lt;$h&gt;;
	print DEBUG "imap line $l";
	last if (!$l);
	if ($l =~ /^(\*|\+)/) {
		# Another response, and possibly the only one if no command
		# was sent.
		push(@rv, $l);
		last if (!$c);
		if ($l =~ /\{(\d+)\}\s*$/) {
			# Start of multi-line text .. read the specified size
			local $size = $1;
			local $got;
			local $err = "Error reading email";
			while($got &lt; $size) {
				local $buf;
				local $r = read($h, $buf, $size-$got);
				return (0, [ $err ], $err, $err) if ($r &lt;= 0);
				$rv[$#rv] .= $buf;
				$got += $r;
				}
			}
		}
	elsif ($l =~ /^(\S+)\s+/ &amp;&amp; $1 eq $id) {
		# End of responses
		push(@rv, $l);
		last;
		}
	else {
		# Part of last response
		if (!@rv) {
			local $err = "Got unknown line $l";
			return (0, [ $err ], $err, $err);
			}
		$rv[$#rv] .= $l;
		}
	}
local $j = join("", @rv);
print DEBUG "imap response $j\n";
local $lline = $rv[$#rv];
if ($lline =~ /^(\S+)\s+OK\s*(.*)/) {
	# Looks like the command worked
	return (1, \@rv, $j, $2);
	}
else {
	# Command failed!
	return (0, \@rv, $j, $lline =~ /^(\S+)\s+(\S+)\s*(.*)/ ? $3 : $lline);
	}
}

# imap_logout(handle, doquit)
sub imap_logout
{
local @rv = $_[1] ? &amp;imap_command($_[0], "close") : (1, undef);
local $f;
foreach $f (keys %imap_login_handle) {
	delete($imap_login_handle{$f}) if ($imap_login_handle{$f} eq $_[0]);
	}
close($_[0]);
return @rv;
}

# lock_folder(&amp;folder)
sub lock_folder
{
return if ($_[0]-&gt;{'remote'} || $_[0]-&gt;{'type'} == 5 || $_[0]-&gt;{'type'} == 6);
local $f = $_[0]-&gt;{'file'} ? $_[0]-&gt;{'file'} :
	   $_[0]-&gt;{'type'} == 0 ? &amp;user_mail_file($remote_user) :
				  $qmail_maildir;
if (&amp;lock_file($f)) {
	$_[0]-&gt;{'lock'} = $f;
	}
else {
	# Cannot lock if in /var/mail
	local $ff = $f;
	$ff =~ s/\//_/g;
	$ff = "/tmp/$ff";
	$_[0]-&gt;{'lock'} = $ff;
	&amp;lock_file($ff);
	}

# Also, check for a .filename.pop3 file
if ($config{'pop_locks'} &amp;&amp; $f =~ /^(\S+)\/([^\/]+)$/) {
	local $poplf = "$1/.$2.pop";
	local $count = 0;
	while(-r $poplf) {
		sleep(1);
		if ($count++ &gt; 5*60) {
			# Give up after 5 minutes
			&amp;error(&amp;text('epop3lock_tries', "&lt;tt&gt;$f&lt;/tt&gt;", 5));
			}
		}
	}
}

# unlock_folder(&amp;folder)
sub unlock_folder
{
return if ($_[0]-&gt;{'remote'});
&amp;unlock_file($_[0]-&gt;{'lock'});
}

# folder_file(&amp;folder)
# Returns the full path to the file or directory containing the folder's mail,
# or undef if not appropriate (such as for POP3)
sub folder_file
{
return $_[0]-&gt;{'remote'} ? undef : $_[0]-&gt;{'file'};
}

# parse_imap_mail(response)
# Parses a response from the IMAP server into a standard mail structure
sub parse_imap_mail
{
local ($imap) = @_;

# Extract the actual mail part
local $mail = { };
local $realsize;
if ($imap =~ /RFC822.SIZE\s+(\d+)/) {
	$realsize = $1;
	}
if ($imap =~ /UID\s+(\d+)/) {
	$mail-&gt;{'id'} = $1;
	}
if ($imap =~ /FLAGS\s+\(([^\)]+)\)/ ||
    $imap =~ /FLAGS\s+(\S+)/) {
	# Got read flags .. use them
	local @flags = split(/\s+/, $1);
	$mail-&gt;{'read'} = &amp;indexoflc("\\Seen", @flags) &gt;= 0 ? 1 : 0;
	$mail-&gt;{'special'} = &amp;indexoflc("\\Flagged", @flags) &gt;= 0 ? 1 : 0;
	$mail-&gt;{'replied'} = &amp;indexoflc("\\Answered", @flags) &gt;= 0 ? 1 : 0;
	$mail-&gt;{'deleted'} = &amp;indexoflc("\\Deleted", @flags) &gt;= 0 ? 1 : 0;
	}
$imap =~ s/^\*\s+(\d+)\s+FETCH.*\{(\d+)\}\r?\n// || return undef;
$mail-&gt;{'imapidx'} = $1;
local $size = $2;
local @lines = split(/\n/, substr($imap, 0, $size));

# Parse the headers
local $lnum = 0;
local @headers;
while(1) {
	local $line = $lines[$lnum++];
	$mail-&gt;{'size'} += length($line);
	$line =~ s/\r//g;
	last if ($line eq '');
	if ($line =~ /^(\S+):\s*(.*)/) {
		push(@headers, [ $1, $2 ]);
		}
	elsif ($line =~ /^(\s+.*)/) {
		$headers[$#headers]-&gt;[1] .= $1
			unless($#headers &lt; 0);
		}
	}
$mail-&gt;{'headers'} = \@headers;
foreach $h (@headers) {
	$mail-&gt;{'header'}-&gt;{lc($h-&gt;[0])} = $h-&gt;[1];
	}

# Parse the body
while($lnum &lt; @lines) {
	$mail-&gt;{'size'} += length($lines[$lnum]+1);
	$mail-&gt;{'body'} .= $lines[$lnum]."\n";
	$lnum++;
	}
$mail-&gt;{'size'} = $realsize if ($realsize);
return $mail;
}

# find_body(&amp;mail, mode)
# Returns the plain text body, html body and the one to use
sub find_body
{
local ($a, $body, $textbody, $htmlbody);
foreach $a (@{$_[0]-&gt;{'attach'}}) {
	next if ($a-&gt;{'header'}-&gt;{'content-disposition'} =~ /^attachment/i);
	if ($a-&gt;{'type'} =~ /^text\/plain/i || $a-&gt;{'type'} eq 'text') {
		$textbody = $a if (!$textbody &amp;&amp; $a-&gt;{'data'} =~ /\S/);
		}
	elsif ($a-&gt;{'type'} =~ /^text\/html/i) {
		$htmlbody = $a if (!$htmlbody &amp;&amp; $a-&gt;{'data'} =~ /\S/);
		}
	}
if ($_[1] == 0) {
	$body = $textbody;
	}
elsif ($_[1] == 1) {
	$body = $textbody || $htmlbody;
	}
elsif ($_[1] == 2) {
	$body = $htmlbody || $textbody;
	}
elsif ($_[1] == 3) {
	# Convert HTML to text if needed
	if ($textbody) {
		$body = $textbody;
		}
	elsif ($htmlbody) {
		local $text = &amp;html_to_text($htmlbody-&gt;{'data'});
		$body = $textbody =
			{ 'data' =&gt; $text };
		}
	}
return ($textbody, $htmlbody, $body);
}

# safe_html(html)
# Converts HTML to a form safe for inclusion in a page
sub safe_html
{
local $html = $_[0];
local $bodystuff;
if ($html =~ s/^[\000-\377]*?&lt;BODY([^&gt;]*)&gt;//i) {
	$bodystuff = $1;
	}
$html =~ s/&lt;\/BODY&gt;[\000-\377]*$//i;
$html =~ s/&lt;base[^&gt;]*&gt;//i;
$html = &amp;filter_javascript($html);
$html = &amp;safe_urls($html);
$bodystuff = &amp;safe_html($bodystuff) if ($bodystuff);
return wantarray ? ($html, $bodystuff) : $html;
}

# head_html(html)
# Returns HTML in the &lt;head&gt; section of a document
sub head_html
{
local $html = $_[0];
return undef if ($html !~ /&lt;HEAD[^&gt;]*&gt;/i || $html !~ /&lt;\/HEAD[^&gt;]*&gt;/i);
$html =~ s/^[\000-\377]*&lt;HEAD[^&gt;]*&gt;//gi || &amp;error("Failed to filter &lt;pre&gt;".&amp;html_escape($html)."&lt;/pre&gt;");
$html =~ s/&lt;\/HEAD[^&gt;]*&gt;[\000-\377]*//gi || &amp;error("Failed to filter &lt;pre&gt;".&amp;html_escape($html)."&lt;/pre&gt;");
$html =~ s/&lt;base[^&gt;]*&gt;//i;
return &amp;filter_javascript($html);
}

# safe_urls(html)
# Replaces dangerous-looking URLs in HTML
sub safe_urls
{
local $html = $_[0];
$html =~ s/((src|href|background)\s*=\s*)([^ '"&gt;]+)()/&amp;safe_url($1, $3, $4)/gei;
$html =~ s/((src|href|background)\s*=\s*')([^']+)(')/&amp;safe_url($1, $3, $4)/gei;
$html =~ s/((src|href|background)\s*=\s*")([^"]+)(")/&amp;safe_url($1, $3, $4)/gei;
return $html;
}

# safe_url(before, url, after)
sub safe_url
{
local ($before, $url, $after) = @_;
if ($url =~ /^#/) {
	# Relative link - harmless
	return $before.$url.$after;
	}
elsif ($url =~ /^cid:/i) {
	# Definitely safe (CIDs are harmless)
	return $before.$url.$after;
	}
elsif ($url =~ /^(http:|https:)/) {
	# Possibly safe, unless refers to local
	local ($host, $port, $page, $ssl) = &amp;parse_http_url($url);
	local ($hhost, $hport) = split(/:/, $ENV{'HTTP_HOST'});
	$hport ||= $ENV{'SERVER_PORT'};
	if ($host ne $hhost ||
	    $port != $hport ||
	    $ssl != (uc($ENV{'HTTPS'}) eq 'ON' ? 1 : 0)) {
		return $before.$url.$after;
		}
	else {
		return $before."_unsafe_link_".$after;
		}
	}
elsif ($url =~ /^mailto:([a-z0-9\.\-\_\@\%]+)/i) {
	# A mailto link which is URL-escaped
	return $before."reply_mail.cgi?new=1&amp;to=".
	       &amp;urlize(&amp;un_urlize($1)).$after;
	}
elsif ($url =~ /^mailto:([a-z0-9\.\-\_\@]+)/i) {
	# A mailto link, which we can convert
	return $before."reply_mail.cgi?new=1&amp;to=".&amp;urlize($1).$after;
	}
elsif ($url =~ /\.cgi/) {
	# Relative URL like foo.cgi or /foo.cgi or ../foo.cgi - unsafe!
	return $before."_unsafe_link_".$after;
	}
else {
	# Non-CGI URL .. assume safe
	return $before.$url.$after;
	}
}

# safe_uidl(string)
sub safe_uidl
{
local $rv = $_[0];
$rv =~ s/\/|\./_/g;
return $rv;
}

# html_to_text(html)
# Attempts to convert some HTML to text form
sub html_to_text
{
local ($h2, $lynx);
if (($h2 = &amp;has_command("html2text")) || ($lynx = &amp;has_command("lynx"))) {
	# Can use a commonly available external program
	local $temp = &amp;transname().".html";
	open(TEMP, "&gt;", $temp);
	print TEMP $_[0];
	close(TEMP);
	open(OUT, ($lynx ? "$lynx -dump $temp" : "$h2 $temp")." 2&gt;/dev/null |");
	while(&lt;OUT&gt;) {
		if ($lynx &amp;&amp; $_ =~ /^\s*References\s*$/) {
			# Start of Lynx references output
			$gotrefs++;
			}
		elsif ($lynx &amp;&amp; $gotrefs &amp;&amp;
		       $_ =~ /^\s*(\d+)\.\s+(http|https|ftp|mailto)/) {
			# Skip this URL reference line
			}
		else {
			$text .= $_;
			}
		}
	close(OUT);
	unlink($temp);
	return $text;
	}
else {
	# Do conversion manually :(
	local $html = $_[0];
	$html =~ s/\s+/ /g;
	$html =~ s/&lt;p&gt;/\n\n/gi;
	$html =~ s/&lt;br&gt;/\n/gi;
	$html =~ s/&lt;[^&gt;]+&gt;//g;
	$html = &amp;entities_to_ascii($html);
	return $html;
	}
}

# folder_select(&amp;folders, selected-folder, name, [extra-options], [by-id],
#		[auto-submit])
# Returns HTML for selecting a folder
sub folder_select
{
local ($folders, $folder, $name, $extra, $byid, $auto) = @_;
local @opts;
push(@opts, @$extra) if ($extra);
foreach my $f (@$folders) {
	next if ($f-&gt;{'hide'} &amp;&amp; $f ne $_[1]);
	local $umsg;
	if (&amp;should_show_unread($f)) {
		local ($c, $u) = &amp;mailbox_folder_unread($f);
		if ($u) {
			$umsg = " ($u)";
			}
		}
	push(@opts, [ $byid ? &amp;folder_name($f) : $f-&gt;{'index'},
		      $f-&gt;{'name'}.$umsg ]);
	}
return &amp;ui_select($name, $byid ? &amp;folder_name($folder) : $folder-&gt;{'index'},
		  \@opts, 1, 0, 0, 0, $auto ? "onChange='form.submit()'" : "");
}

# folder_size(&amp;folder, ...)
# Sets the 'size' field of one or more folders, and returns the total
sub folder_size
{
local ($f, $total);
foreach $f (@_) {
	if ($f-&gt;{'type'} == 0 || $f-&gt;{'type'} == 7) {
		# Single mail file - size is easy
		local @st = stat($f-&gt;{'file'});
		$f-&gt;{'size'} = $st[7];
		}
	elsif ($f-&gt;{'type'} == 1) {
		# Maildir folder size is that of all files in it, except
		# sub-folders.
		$f-&gt;{'size'} = 0;
		foreach my $sd ("cur", "new", "tmp") {
			$f-&gt;{'size'} += &amp;recursive_disk_usage(
					$f-&gt;{'file'}."/".$sd, '^\\.');
			}
		}
	elsif ($f-&gt;{'type'} == 3) {
		# MH folder size is that of all mail files
		local $mf;
		$f-&gt;{'size'} = 0;
		opendir(MHDIR, $f-&gt;{'file'});
		while($mf = readdir(MHDIR)) {
			next if ($mf eq "." || $mf eq "..");
			local @st = stat("$f-&gt;{'file'}/$mf");
			$f-&gt;{'size'} += $st[7];
			}
		closedir(MHDIR);
		}
	elsif ($f-&gt;{'type'} == 4) {
		# Get size of IMAP folder
		local ($ok, $h, $count, $uidnext) = &amp;imap_login($f);
		if ($ok) {
			$f-&gt;{'size'} = 0;
			$f-&gt;{'lastchange'} = $uidnext;
			local @rv = &amp;imap_command($h,
				"FETCH 1:$count (RFC822.SIZE)");
			foreach my $r (@{$rv[1]}) {
				if ($r =~ /RFC822.SIZE\s+(\d+)/) {
					$f-&gt;{'size'} += $1;
					}
				}
			}
		}
	elsif ($f-&gt;{'type'} == 5) {
		# Size of a combined folder is the size of all sub-folders
		return &amp;folder_size(@{$f-&gt;{'subfolders'}});
		}
	else {
		# Cannot get size of a POP3 folder
		$f-&gt;{'size'} = undef;
		}
	$total += $f-&gt;{'size'};
	}
return $total;
}

# parse_boolean(string)
# Separates a string into a series of and/or separated values. Returns a
# mode number (0=or, 1=and, 2=both) and a list of words
sub parse_boolean
{
local @rv;
local $str = $_[0];
local $mode = -1;
local $lastandor = 0;
while($str =~ /^\s*"([^"]*)"(.*)$/ ||
      $str =~ /^\s*"([^"]*)"(.*)$/ ||
      $str =~ /^\s*(\S+)(.*)$/) {
	local $word = $1;
	$str = $2;
	if (lc($word) eq "and") {
		if ($mode &lt; 0) { $mode = 1; }
		elsif ($mode != 1) { $mode = 2; }
		$lastandor = 1;
		}
	elsif (lc($word) eq "or") {
		if ($mode &lt; 0) { $mode = 0; }
		elsif ($mode != 0) { $mode = 2; }
		$lastandor = 1;
		}
	else {
		if (!$lastandor &amp;&amp; @rv) {
			$rv[$#rv] .= " ".$word;
			}
		else {
			push(@rv, $word);
			}
		$lastandor = 0;
		}
	}
$mode = 0 if ($mode &lt; 0);
return ($mode, \@rv);
}

# recursive_files(dir, treat-dirs-as-folders)
sub recursive_files
{
local ($f, @rv);
opendir(DIR, $_[0]);
local @files = readdir(DIR);
closedir(DIR);
foreach $f (@files) {
	next if ($f eq "." || $f eq ".." || $f =~ /\.lock$/i ||
		 $f eq "cur" || $f eq "tmp" || $f eq "new" ||
		 $f =~ /^\.imap/i || $f eq ".customflags" ||
		 $f eq "dovecot-uidlist" || $f =~ /^courierimap/ ||
		 $f eq "maildirfolder" || $f eq "maildirsize" ||
		 $f eq "maildircache" || $f eq ".subscriptions" ||
                 $f eq ".usermin-maildircache" || $f =~ /^dovecot\.index/ ||
		 $f =~ /^dovecot-uidvalidity/ || $f eq "subscriptions" ||
		 $f =~ /\.webmintmp\.\d+$/ || $f eq "dovecot-keywords" ||
		 $f =~ /^dovecot\.mailbox/);
	local $p = "$_[0]/$f";
	local $added = 0;
	if ($_[1] || !-d $p || -d "$p/cur") {
		push(@rv, $p);
		$added = 1;
		}
	# If this directory wasn't a folder (or it it in Maildir format),
	# search it too.
	if (-d "$p/cur" || !$added) {
		push(@rv, &amp;recursive_files($p));
		}
	}
return @rv;
}

# editable_mail(&amp;mail)
# Returns 0 if some mail message should not be editable (ie. internal folder)
sub editable_mail
{
return $_[0]-&gt;{'header'}-&gt;{'subject'} !~ /DON'T DELETE THIS MESSAGE.*FOLDER INTERNAL DATA/;
}

# fix_cids(html, &amp;attachments, url-prefix)
# Replaces HTML like img src=cid:XXX with img src=detach.cgi?whatever
sub fix_cids
{
local $rv = $_[0];

# Fix images referring to CIDs
$rv =~ s/(src="|href="|background=")cid:([^"]+)(")/$1.&amp;fix_cid($2,$_[1],$_[2]).$3/gei;
$rv =~ s/(src='|href='|background=')cid:([^']+)(')/$1.&amp;fix_cid($2,$_[1],$_[2]).$3/gei;
$rv =~ s/(src=|href=|background=)cid:([^\s&gt;]+)()/$1.&amp;fix_cid($2,$_[1],$_[2]).$3/gei;

# Fix images whose URL is actually in an attachment
$rv =~ s/(src="|href="|background=")([^"]+)(")/$1.&amp;fix_contentlocation($2,$_[1],$_[2]).$3/gei;
$rv =~ s/(src='|href='|background=')([^']+)(')/$1.&amp;fix_contentlocation($2,$_[1],$_[2]).$3/gei;
$rv =~ s/(src=|href=|background=)([^\s&gt;]+)()/$1.&amp;fix_contentlocation($2,$_[1],$_[2]).$3/gei;
return $rv;
}

# fix_cid(cid, &amp;attachments, url-prefix)
sub fix_cid
{
local ($cont) = grep { $_-&gt;{'header'}-&gt;{'content-id'} eq $_[0] ||
		       $_-&gt;{'header'}-&gt;{'content-id'} eq "&lt;$_[0]&gt;" } @{$_[1]};
if ($cont) {
	return "$_[2]&amp;attach=$cont-&gt;{'idx'}";
	}
else {
	return "cid:$_[0]";
	}
}

# fix_contentlocation(url, &amp;attachments, url-prefix)
sub fix_contentlocation
{
local ($cont) = grep { $_-&gt;{'header'}-&gt;{'content-location'} eq $_[0] ||
	       $_-&gt;{'header'}-&gt;{'content-location'} eq "&lt;$_[0]&gt;" } @{$_[1]};
if ($cont) {
	return "$_[2]&amp;attach=$cont-&gt;{'idx'}";
	}
else {
	return $_[0];
	}
}

# create_cids(html, &amp;results-map)
# Replaces all image references in the body like &lt;img src=detach.cgi?...&gt; with
# cid: tags, stores in the results map pointers from the index to the CID.
sub create_cids
{
local ($html, $cidmap) = @_;
$html =~ s/(src="|href="|background=")detach.cgi\?([^"]+)(")/$1.&amp;create_cid($2,$cidmap).$3/gei;
$html =~ s/(src='|href='|background=')detach.cgi\?([^']+)(')/$1.&amp;create_cid($2,$cidmap).$3/gei;
$html =~ s/(src=|href=|background=)detach.cgi\?([^\s&gt;]+)()/$1.&amp;create_cid($2,$cidmap).$3/gei;
return $html;
}

sub create_cid
{
local ($args, $cidmap) = @_;
if ($args =~ /attach=(\d+)/) {
	$create_cid_count++;
	$cidmap-&gt;{$1} = time().$$.$create_cid_count;
	return "cid:".$cidmap-&gt;{$1};
	}
else {
	# No attachment ID!
	return "";
	}
}

# disable_html_images(html, disable?, &amp;urls)
# Turn off some or all images in HTML email. Mode 0=Do nothing, 1=Offsite only,
# 2=All images. Returns the URL of images found in &amp;urls
sub disable_html_images
{
local ($html, $dis, $urls) = @_;
local $newhtml;
while($html =~ /^([\000-\377]*?)(&lt;\s*img[^&gt;]*src=('[^']*'|"[^"]*"|\S+)[^&gt;]*&gt;)([\000-\377]*)/i) {
	local ($before, $allimg, $img, $after) = ($1, $2, $3, $4);
	$img =~ s/^'(.*)'$/$1/ || $img =~ s/^"(.*)"$/$1/;
	push(@$urls, $img) if ($urls);
	if ($dis == 0) {
		# Don't harm image
		$newhtml .= $before.$allimg;
		}
	elsif ($dis == 1) {
		# Don't touch unless offsite
		if ($img =~ /^(http|https|ftp):/) {
			$newhtml .= $before;
			}
		else {
			$newhtml .= $before.$allimg;
			}
		}
	elsif ($dis == 2) {
		# Always remove image
		$newhtml .= $before;
		}
	$html = $after;
	}
$newhtml .= $html;
return $newhtml;
}

# remove_body_attachments(&amp;mail, &amp;attach)
# Returns attachments except for those that make up the message body, and those
# that have sub-attachments.
sub remove_body_attachments
{
local ($mail, $attach) = @_;
local ($textbody, $htmlbody) = &amp;find_body($mail);
return grep { $_ ne $htmlbody &amp;&amp; $_ ne $textbody &amp;&amp; !$_-&gt;{'attach'} &amp;&amp;
	      $_-&gt;{'type'} ne 'message/delivery-status' } @$attach;
}

# remove_cid_attachments(&amp;mail, &amp;attach)
# Returns attachments except for those that are used for inline images in the
# HTML body.
sub remove_cid_attachments
{
local ($mail, $attach) = @_;
local ($textbody, $htmlbody) = &amp;find_body($mail);
local @rv;
foreach my $a (@$attach) {
	my $cid = $a-&gt;{'header'}-&gt;{'content-id'};
	$cid =~ s/^&lt;(.*)&gt;$/$1/g;
	my $cl = $a-&gt;{'header'}-&gt;{'content-location'};
	$cl =~ s/^&lt;(.*)&gt;$/$1/g;
	local $inline;
	if ($cid &amp;&amp; $htmlbody-&gt;{'data'} =~ /cid:\Q$cid\E|cid:"\Q$cid\E"|cid:'\Q$cid\E'/) {
		# CID-based attachment
		$inline = 1;
		}
	elsif ($cl &amp;&amp; $htmlbody-&gt;{'data'} =~ /\Q$cl\E/) {
		# Content-location based attachment
		$inline = 1;
		}
	if (!$inline) {
		push(@rv, $a);
		}
	}
return @rv;
}

# quoted_message(&amp;mail, quote-mode, sig, 0=any,1=text,2=html, sig-at-top?)
# Returns the quoted text, html-flag and body attachment
sub quoted_message
{
local ($mail, $qu, $sig, $bodymode, $sigtop) = @_;
local $mode = $bodymode == 1 ? 1 :
	      $bodymode == 2 ? 2 :
	      %userconfig ? $userconfig{'view_html'} :
			    $config{'view_html'};
local ($plainbody, $htmlbody) = &amp;find_body($mail, $mode);
local ($quote, $html_edit, $body);
local $cfg = %userconfig ? \%userconfig : \%config;
local @writers = &amp;split_addresses($mail-&gt;{'header'}-&gt;{'from'});
local $writer;
if ($writers[0]-&gt;[1]) {
	$writer = &amp;decode_mimewords($writers[0]-&gt;[1])." &lt;".
		  &amp;decode_mimewords($writers[0]-&gt;[0])."&gt; wrote ..";
	}
else {
	$writer = &amp;decode_mimewords($writers[0]-&gt;[0])." wrote ..";
	}
local $tm;
if ($cfg-&gt;{'reply_date'} &amp;&amp;
    ($tm = &amp;parse_mail_date($_[0]-&gt;{'header'}-&gt;{'date'}))) {
	local $tmstr = &amp;make_date($tm);
	$writer = "On $tmstr $writer";
	}
local $qm = %userconfig ? $userconfig{'html_quote'} : $config{'html_quote'};
if (($cfg-&gt;{'html_edit'} == 2 ||
     $cfg-&gt;{'html_edit'} == 1 &amp;&amp; $htmlbody) &amp;&amp;
     $bodymode != 1) {
	# Create quoted body HTML
	if ($htmlbody) {
		$body = $htmlbody;
		$sig =~ s/\n/&lt;br&gt;\n/g;
		if ($qu &amp;&amp; $qm == 0) {
			# Quoted HTML as cite
			$quote = &amp;html_escape($writer)."\n".
				 "&lt;blockquote type=cite&gt;\n".
				 &amp;safe_html($htmlbody-&gt;{'data'}).
				 "&lt;/blockquote&gt;";
			if ($sigtop) {
				$quote = $sig."&lt;br&gt;\n".$quote;
				}
			else {
				$quote = $quote.$sig."&lt;br&gt;\n";
				}
			}
		elsif ($qu &amp;&amp; $qm == 1) {
			# Quoted HTML below line
			$quote = "&lt;br&gt;$sig&lt;hr&gt;".
			         &amp;html_escape($writer)."&lt;br&gt;\n".
				 &amp;safe_html($htmlbody-&gt;{'data'});
			}
		else {
			# Un-quoted HTML
			$quote = &amp;safe_html($htmlbody-&gt;{'data'});
			if ($sigtop) {
				$quote = $sig."&lt;br&gt;\n".$quote;
				}
			else {
				$quote = $quote.$sig."&lt;br&gt;\n";
				}
			}
		}
	elsif ($plainbody) {
		$body = $plainbody;
		local $pd = $plainbody-&gt;{'data'};
		$pd =~ s/^\s+//g;
		$pd =~ s/\s+$//g;
		if ($qu &amp;&amp; $qm == 0) {
			# Quoted plain text as HTML as cite
			$quote = &amp;html_escape($writer)."\n".
				 "&lt;blockquote type=cite&gt;\n".
				 "&lt;pre&gt;$pd&lt;/pre&gt;".
				 "&lt;/blockquote&gt;";
			if ($sigtop) {
				$quote = $sig."&lt;br&gt;\n".$quote;
				}
			else {
				$quote = $quote.$sig."&lt;br&gt;\n";
				}
			}
		elsif ($qu &amp;&amp; $qm == 1) {
			# Quoted plain text as HTML below line
			$quote = "&lt;br&gt;$sig&lt;hr&gt;".
				 &amp;html_escape($writer)."&lt;br&gt;\n".
				 "&lt;pre&gt;$pd&lt;/pre&gt;&lt;br&gt;\n";
			}
		else {
			# Un-quoted plain text as HTML
			$quote = "&lt;pre&gt;$pd&lt;/pre&gt;";
			if ($sigtop) {
				$quote = $sig."&lt;br&gt;\n".$quote;
				}
			else {
				$quote = $quote.$sig."&lt;br&gt;\n";
				}
			}
		}
	$html_edit = 1;
	}
else {
	# Create quoted body text
	if ($plainbody) {
		$body = $plainbody;
		$quote = $plainbody-&gt;{'data'};
		}
	elsif ($htmlbody) {
		$body = $htmlbody;
		$quote = &amp;html_to_text($htmlbody-&gt;{'data'});
		}
	if ($quote &amp;&amp; $qu) {
		$quote = join("", map { "&gt; $_\n" }
			&amp;wrap_lines($quote, 78));
		}
	$quote = $writer."\n".$quote if ($quote &amp;&amp; $qu);
	if ($sig &amp;&amp; $sigtop) {
		$quote = $sig."\n".$quote;
		}
	elsif ($sig &amp;&amp; !$sigtop) {
		$quote = $quote.$sig."\n";
		}
	}
return ($quote, $html_edit, $body);
}

# modification_time(&amp;folder)
# Returns the unix time on which this folder was last modified, or 0 if unknown
sub modification_time
{
if ($_[0]-&gt;{'type'} == 0) {
	# Modification time of file
	local @st = stat($_[0]-&gt;{'file'});
	return $st[9];
	}
elsif ($_[0]-&gt;{'type'} == 1) {
	# Greatest modification time of cur/new directory
	local @stcur = stat("$_[0]-&gt;{'file'}/cur");
	local @stnew = stat("$_[0]-&gt;{'file'}/new");
	return $stcur[9] &gt; $stnew[9] ? $stcur[9] : $stnew[9];
	}
elsif ($_[0]-&gt;{'type'} == 2 || $_[0]-&gt;{'type'} == 4) {
	# Cannot know for POP3 or IMAP folders
	return 0;
	}
elsif ($_[0]-&gt;{'type'} == 3) {
	# Modification time of MH folder
	local @st = stat($_[0]-&gt;{'file'});
	return $st[9];
	}
else {
	# Huh?
	return 0;
	}
}

# requires_delivery_notification(&amp;mail)
sub requires_delivery_notification
{
return $_[0]-&gt;{'header'}-&gt;{'disposition-notification-to'} ||
       $_[0]-&gt;{'header'}-&gt;{'read-reciept-to'};
}

# send_delivery_notification(&amp;mail, [from-addr], manual)
# Send an email containing delivery status information
sub send_delivery_notification
{
local ($mail, $from) = @_;
$from ||= $mail-&gt;{'header'}-&gt;{'to'};
local $host = &amp;get_display_hostname();
local $to = &amp;requires_delivery_notification($mail);
local $product = &amp;get_product_name();
$product = ucfirst($product);
local $version = &amp;get_webmin_version();
local ($taddr) = &amp;split_addresses($mail-&gt;{'header'}-&gt;{'to'});
local $disp = $manual ? "manual-action/MDN-sent-manually"
		      : "automatic-action/MDN-sent-automatically";
local $dsn = &lt;&lt;EOF;
Reporting-UA: $host; $product $version
Original-Recipient: rfc822;$taddr-&gt;[0]
Final-Recipient: rfc822;$taddr-&gt;[0]
Original-Message-ID: $mail-&gt;{'header'}-&gt;{'message-id'}
Disposition: $disp; displayed
EOF
local $dmail = {
	'headers' =&gt;
	   [ [ 'From' =&gt; $from ],
	     [ 'To' =&gt; $to ],
	     [ 'Subject' =&gt; 'Delivery notification' ],
	     [ 'Content-type' =&gt; 'multipart/report; report-type=disposition-notification' ],
	     [ 'Content-Transfer-Encoding' =&gt; '7bit' ] ],
	'attach' =&gt; [
	   { 'headers' =&gt; [ [ 'Content-type' =&gt; 'text/plain' ] ],
	     'data' =&gt; "This is a delivery status notification for the email sent to:\n$mail-&gt;{'header'}-&gt;{'to'}\non the date:\n$mail-&gt;{'header'}-&gt;{'date'}\nwith the subject:\n$mail-&gt;{'header'}-&gt;{'subject'}\n" },
	   { 'headers' =&gt; [ [ 'Content-type' =&gt;
				'message/disposition-notification' ],
			    [ 'Content-Transfer-Encoding' =&gt; '7bit' ] ],
	     'data' =&gt; $dsn }
		] };
eval { local $main::errors_must_die = 1; &amp;send_mail($dmail); };
return $to;
}

# find_subfolder(&amp;folder, name)
# Returns the sub-folder with some name
sub find_subfolder
{
local ($folder, $sfn) = @_;
if ($folder-&gt;{'type'} == 5) {
	# Composite
	foreach my $sf (@{$folder-&gt;{'subfolders'}}) {
		return $sf if (&amp;folder_name($sf) eq $sfn);
		}
	}
elsif ($folder-&gt;{'type'} == 6) {
	# Virtual
	foreach my $m (@{$folder-&gt;{'members'}}) {
		return $m-&gt;[0] if (&amp;folder_name($m-&gt;[0]) eq $sfn);
		}
	}
return undef;
}

# find_named_folder(name, &amp;folders, [&amp;cache])
# Finds a folder by ID, filename, server name or displayed name
sub find_named_folder
{
local ($name, $folders, $cache) = @_;
local $rv;
if ($cache &amp;&amp; exists($cache-&gt;{$name})) {
	# In cache
	$rv = $cache-&gt;{$name};
	}
else {
	# Need to lookup
	($rv) = grep { &amp;folder_name($_) eq $name } @$folders if (!$rv);
	($rv) = grep { my $escfile = $_-&gt;{'file'};
		       $escfile =~ s/\s/_/g;
		       $escfile eq $name ||
		       $_-&gt;{'file'} eq $name ||
		       $_-&gt;{'server'} eq $name } @$folders if (!$rv);
	($rv) = grep { my $escname = $_-&gt;{'name'};
		       $escname =~ s/\s/_/g;
		       $escname eq $name ||
		       $_-&gt;{'name'} eq $name } @$folders if (!$rv);
	$cache-&gt;{$name} = $rv if ($cache);
	}
return $rv;
}

# folder_name(&amp;folder)
# Returns a unique identifier for a folder, based on it's filename or ID
sub folder_name
{
my $rv = $_[0]-&gt;{'id'} ||
         $_[0]-&gt;{'file'} ||
         $_[0]-&gt;{'server'} ||
         $_[0]-&gt;{'name'};
$rv =~ s/\s/_/g;
return $rv;
}

# set_folder_lastmodified(&amp;folders)
# Sets the last-modified time and sortable flag on all given folders
sub set_folder_lastmodified
{
local ($folders) = @_;
foreach my $folder (@$folders) {
	if ($folder-&gt;{'type'} == 0 || $folder-&gt;{'type'} == 3) {
		# For an mbox or MH folder, the last modified date is just that
		# of the file or directory itself
		local @st = stat($folder-&gt;{'file'});
		$folder-&gt;{'lastchange'} = $st[9];
		$folder-&gt;{'sortable'} = 1;
		}
	elsif ($folder-&gt;{'type'} == 1) {
		# For a Maildir folder, the date is that of the newest
		# sub-directory (cur, tmp or new)
		$folder-&gt;{'lastchange'} = 0;
		foreach my $sf ("cur", "tmp", "new") {
			local @st = stat("$folder-&gt;{'file'}/$sf");
			$folder-&gt;{'lastchange'} = $st[9]
				if ($st[9] &gt; $folder-&gt;{'lastchange'});
			}
		$folder-&gt;{'sortable'} = 1;
		}
	elsif ($folder-&gt;{'type'} == 5) {
		# For a composite folder, the date is that of the newest
		# sub-folder, OR the folder file itself
		local @st = stat($folder-&gt;{'folderfile'});
		$folder-&gt;{'lastchange'} = $st[9];
		&amp;set_folder_lastmodified($folder-&gt;{'subfolders'});
		foreach my $sf (@{$folder-&gt;{'subfolders'}}) {
			$folder-&gt;{'lastchange'} = $sf-&gt;{'lastchange'}
				if ($sf-&gt;{'lastchange'} &gt;
				    $folder-&gt;{'lastchange'});
			}
		$folder-&gt;{'sortable'} = 1;
		}
	elsif ($folder-&gt;{'type'} == 6) {
		# For a virtual folder, the date is that of the newest
		# sub-folder, OR the folder file itself
		local @st = stat($folder-&gt;{'folderfile'});
		$folder-&gt;{'lastchange'} = $st[9];
		my %done;
		foreach my $m (@{$folder-&gt;{'members'}}) {
			if (!$done{$m-&gt;[0]}++) {
				&amp;set_folder_lastmodified([ $m-&gt;[0] ]);
				$folder-&gt;{'lastchange'} =
					$m-&gt;[0]-&gt;{'lastchange'}
					if ($m-&gt;[0]-&gt;{'lastchange'} &gt;
					    $folder-&gt;{'lastchange'});
				}
			}
		$folder-&gt;{'sortable'} = 1;
		}
	else {
		# For POP3 and IMAP folders, we don't know the last change
		$folder-&gt;{'lastchange'} = undef;
		$folder-&gt;{'sortable'} = 1;
		}
	}
}

# mail_preview(&amp;mail, [characters])
# Returns a short text preview of a message body
sub mail_preview
{
local ($mail, $chars) = @_;
$chars ||= 100;
local ($textbody, $htmlbody, $body) = &amp;find_body($mail, 0);
local $data = $body-&gt;{'data'};
$data =~ s/\r?\n/ /g;
$data = substr($data, 0, $chars);
if ($data =~ /\S/) {
	return $data;
	}
return undef;
}

# open_dbm_db(&amp;hash, file, mode)
# Attempts to open a DBM, first using SDBM_File, and then NDBM_File
sub open_dbm_db
{
local ($hash, $file, $mode) = @_;
eval "use SDBM_File";
dbmopen(%$hash, $file, $mode);
eval { $hash-&gt;{'1111111111'} = 'foo bar' };
if ($@) {
	dbmclose(%$hash);
	eval "use NDBM_File";
	dbmopen(%$hash, $file, $mode);
	}
}

# generate_message_id(from-address)
# Returns a unique ID for a new message
sub generate_message_id
{
local ($fromaddr) = @_;
local ($finfo) = &amp;split_addresses($fromaddr);
local $dom;
if ($finfo &amp;&amp; $finfo-&gt;[0] =~ /\@(\S+)$/) {
	$dom = $1;
	}
else {
	$dom = &amp;get_system_hostname();
	}
return "&lt;".time().".".$$."\@".$dom."&gt;";
}

# type_to_extension(type)
# Returns a good extension for a MIME type
sub type_to_extension
{
local ($type) = @_;
$type =~ s/;.*$//;
local ($mt) = grep { lc($_-&gt;{'type'}) eq lc($type) } &amp;list_mime_types();
if ($mt &amp;&amp; $m-&gt;{'exts'}-&gt;[0]) {
	return $m-&gt;{'exts'}-&gt;[0];
	}
elsif ($type =~ /^text\//) {
	return ".txt";
	}
else {
	my @p = split(/\//, $type);
	return $p[1];
	}
}

# should_show_unread(&amp;folder)
# Returns 1 if we should show unread counts for some folder
sub should_show_unread
{
local ($folder) = @_;
local $su = $userconfig{'show_unread'} || $config{'show_unread'};

# Work out if all sub-folders are IMAP
local $allimap;
if ($su == 2) {
	# Doesn't matter
	}
elsif ($su == 1 &amp;&amp; $config{'mail_system'} == 4) {
	# Totally IMAP mode
	$allimap = 1;
	}
elsif ($su == 1) {
	if ($folder-&gt;{'type'} == 5) {
		$allimap = 1;
		foreach my $sf (@{$folder-&gt;{'subfolders'}}) {
			$allimap = 0 if (!&amp;should_show_unread($sf));
			}
		}
	elsif ($folder-&gt;{'type'} == 6) {
		$allimap = 1;
		foreach my $mem (@{$folder-&gt;{'members'}}) {
			$allimap = 0 if (!&amp;should_show_unread($mem-&gt;[0]));
			}
		}
	}

return $su == 2 ||				# All folders
       ($folder-&gt;{'type'} == 4 ||		# Only IMAP and derived
	$folder-&gt;{'type'} == 5 &amp;&amp; $allimap ||
	$folder-&gt;{'type'} == 6 &amp;&amp; $allimap) &amp;&amp; $su == 1;
}

# mail_has_attachments(&amp;mail|&amp;mails, &amp;folder)
# Returns an array of flags, each being 1 if the message has attachments, 0
# if not. Uses a cache DBM by message ID and fetches the whole mail if needed.
sub mail_has_attachments
{
local ($mails, $folder) = @_;
if (ref($mails) ne 'ARRAY') {
	# Just one
	$mails = [ $mails ];
	}

# Open cache DBM
if (!%hasattach) {
	local $hasattach_file;
	if ($module_info{'usermin'}) {
		$hasattach_file = "$user_module_config_directory/attach";
		}
	else {
		$hasattach_file = "$module_config_directory/attach";
		if (!glob($hasattach_file."*")) {
			$hasattach_file = "$module_var_directory/attach";
			}
		}
	&amp;open_dbm_db(\%hasattach, $hasattach_file, 0600);
	}

# See which mail we already know about
local @rv = map { undef } @$mails;
local @needbody;
for(my $i=0; $i&lt;scalar(@rv); $i++) {
	local $mail = $mails-&gt;[$i];
	local $mid = $mail-&gt;{'header'}-&gt;{'message-id'} ||
		     $mail-&gt;{'id'};
	if ($mid &amp;&amp; defined($hasattach{$mid})) {
		# Already cached .. use it
		$rv[$i] = $hasattach{$mid};
		}
	elsif (!$mail-&gt;{'body'} &amp;&amp; $mail-&gt;{'size'} &gt; 1024*1024) {
		# Message is big .. just assume it has attachments
		$rv[$i] = 1;
		}
	elsif (!$mail-&gt;{'body'}) {
		# Need to get body
		push(@needbody, $i);
		}
	}

# We need to actually fetch some message bodies to check for attachments
if (@needbody) {
	local (@needmail, %oldread);
	foreach my $i (@needbody) {
		push(@needmail, $mails-&gt;[$i]);
		}
	@needmail = &amp;mailbox_select_mails($folder,
		[ map { $_-&gt;{'id'} } @needmail ], 0);
	foreach my $i (@needbody) {
		$mails-&gt;[$i] = shift(@needmail);
		}
	}

# Now we have bodies, check for attachments
for(my $i=0; $i&lt;scalar(@rv); $i++) {
	next if (defined($rv[$i]));
	local $mail = $mails-&gt;[$i];
	if (!$mail) {
		# Couldn't read from server
		$rv[$i] = 0;
		next;
		}
	if (!@{$mail-&gt;{'attach'}}) {
		# Parse out attachments
		&amp;parse_mail($mail, undef, 0);
		}

	# Check for non-text attachments
	$rv[$i] = 0;
	foreach my $a (@{$mail-&gt;{'attach'}}) {
		if ($a-&gt;{'type'} =~ /^text\/(plain|html)/i ||
		    $a-&gt;{'type'} eq 'text') {
			# Text part .. may be an attachment
			if ($a-&gt;{'header'}-&gt;{'content-disposition'} =~
			    /^attachment/i) {
				$rv[$i] = 1;
				}
			}
		elsif ($a-&gt;{'type'} !~ /^multipart\/(mixed|alternative)/) {
			# Non-text .. assume this means we have an attachment
			$rv[$i] = 1;
			}
		}
	}

# Update the cache
for(my $i=0; $i&lt;scalar(@rv); $i++) {
	local $mail = $mails-&gt;[$i];
	local $mid = $mail-&gt;{'header'}-&gt;{'message-id'} ||
		     $mail-&gt;{'id'};
	if ($mid &amp;&amp; !defined($hasattach{$mid})) {
		$hasattach{$mid} = $rv[$i]
		}
	}

return wantarray ? @rv : $rv[0];
}

# show_delivery_status(&amp;dstatus)
# Show the delivery status HTML for some email
sub show_delivery_status
{
local ($dstatus) = @_;
local $ds = &amp;parse_delivery_status($dstatus-&gt;{'data'});
$dtxt = $ds-&gt;{'status'} =~ /^2\./ ? $text{'view_dstatusok'}
				  : $text{'view_dstatus'};
print &amp;ui_table_start($dtxt, "width=100%", 2, [ "width=10% nowrap" ]);
foreach $dsh ('final-recipient', 'diagnostic-code',
	      'remote-mta', 'reporting-mta') {
	if ($ds-&gt;{$dsh}) {
		$ds-&gt;{$dsh} =~ s/^\S+;//;
		print &amp;ui_table_row($text{'view_'.$dsh},
				    &amp;html_escape($ds-&gt;{$dsh}));
		}
	}
print &amp;ui_table_end();
}

# attachments_table(&amp;attach, folder, view-url, detach-url,
#                   [viewmail-url, viewmail-field], [show-checkboxes])
# Prints an HTML table of attachments. Returns a list of those that can be
# server-side detached.
sub attachments_table
{
local ($attach, $folder, $viewurl, $detachurl, $mailurl, $idfield, $cbs) = @_;
local %typemap = map { $_-&gt;{'type'}, $_-&gt;{'desc'} } &amp;list_mime_types();
local $qid = &amp;urlize($id);
local $rv;
local (@files, @actions, @detach, @sizes, @titles, @links);
foreach my $a (@$attach) {
	local $fn;
	local $size = &amp;nice_size(length($a-&gt;{'data'}));
	local $cb;
	if (!$a-&gt;{'type'}) {
		# An actual email
		push(@files, &amp;text('view_sub2', $a-&gt;{'header'}-&gt;{'from'}));
		$fn = "mail.txt";
		$size = &amp;nice_size($a-&gt;{'size'});
		}
	elsif ($a-&gt;{'type'} eq 'message/rfc822') {
		# Attached email
		local $amail = &amp;extract_mail($a-&gt;{'data'});
		if ($amail &amp;&amp; $amail-&gt;{'header'}-&gt;{'from'}) {
			push(@files, &amp;text('view_sub2',
					$amail-&gt;{'header'}-&gt;{'from'}));
			}
		else {
			push(@files, &amp;text('view_sub'));
			}
		$fn = "mail.txt";
		}
	elsif ($a-&gt;{'filename'}) {
		# Known filename
		$fn = &amp;decode_mimewords($a-&gt;{'filename'});
		push(@files, $fn);
		push(@detach, [ $a-&gt;{'idx'}, $fn ]);
		}
	else {
		# No filename
		push(@files, $text{'view_anofile'});
		$fn = "file.".&amp;type_to_extension($a-&gt;{'type'});
		push(@detach, [ $a-&gt;{'idx'}, $fn ]);
		}
	push(@sizes, $size);
	push(@titles, $files[$#files]."&lt;br&gt;".$size);
	if ($a-&gt;{'error'}) {
		$titles[$#titles] .= "&lt;br&gt;&lt;font size=-1&gt;($a-&gt;{'error'})&lt;/font&gt;";
		}
	$fn =~ s/ /_/g;
	$fn =~ s/\#/_/g;
	$fn = &amp;html_escape($fn);
	local @a;
	local $detachfile = $detachurl;
	$detachfile =~ s/\?/\/$fn\?/;
	if (!$a-&gt;{'type'}) {
		# Complete email for viewing
		local $qmid = &amp;urlize($a-&gt;{$idfield});
		push(@links, "$mailurl&amp;$idfield=$qmid&amp;folder=$folder-&gt;{'index'}");
		}
	elsif ($a-&gt;{'type'} eq 'message/rfc822') {
		# Attached sub-email
		push(@links, $viewurl."&amp;sub=$a-&gt;{'idx'}");
		}
	else {
		# Regular attachment
		push(@links, $detachfile."&amp;attach=$a-&gt;{'idx'}");
		}
	push(@a, "&lt;a href='$links[$#links]'&gt;$text{'view_aview'}&lt;/a&gt;");
	push(@a, "&lt;a href='$links[$#links]' target=_blank&gt;$text{'view_aopen'}&lt;/a&gt;");
	if ($a-&gt;{'type'}) {
		push(@a, "&lt;a href='$detachfile&amp;attach=$a-&gt;{'idx'}&amp;save=1'&gt;$text{'view_asave'}&lt;/a&gt;");
		}
	if ($a-&gt;{'type'} eq 'message/rfc822') {
		push(@a, "&lt;a href='$detachfile&amp;attach=$a-&gt;{'idx'}&amp;type=text/plain$subs'&gt;$text{'view_aplain'}&lt;/a&gt;");
		}
	push(@actions, \@a);
	}
local @tds = ( "width=50%", "width=25%", "width=10%", "width=15% nowrap" );
if ($cbs) {
	unshift(@tds, "width=5");
	}
print &amp;ui_columns_start([
	$cbs ? ( "" ) : ( ),
	$text{'view_afile'},
	$text{'view_atype'},
	$text{'view_asize'},
	$text{'view_aactions'},
	], 100, 0, \@tds);
for(my $i=0; $i&lt;@files; $i++) {
	local $type = $attach[$i]-&gt;{'type'} || "message/rfc822";
	local $typedesc = $typemap{lc($type)} || $type;
	local @cols = (
		"&lt;a href='$links[$i]'&gt;".&amp;html_escape($files[$i])."&lt;/a&gt;",
		$typedesc,
		$sizes[$i],
		&amp;ui_links_row($actions[$i]),
		);
	if ($cbs) {
		print &amp;ui_checked_columns_row(\@cols, \@tds,
					      $cbs, $attach-&gt;[$i]-&gt;{'idx'}, 1);
		}
	else {
		print &amp;ui_columns_row(\@cols, \@tds);
		}
	}
print &amp;ui_columns_end();
return @detach;
}

# message_icons(&amp;mail, showto, &amp;folder)
# Returns a list of icon images for some mail
sub message_icons
{
local ($mail, $showto, $folder) = @_;
local @rv;
if (&amp;mail_has_attachments($mail, $folder)) {
	push(@rv, "&lt;img src=images/attach.gif alt='A'&gt;");
	}
local $p = int($mail-&gt;{'header'}-&gt;{'x-priority'});
if ($p == 1) {
	push(@rv, "&lt;img src=images/p1.gif alt='P1'&gt;");
	}
elsif ($p == 2) {
	push(@rv, "&lt;img src=images/p2.gif alt='P2'&gt;");
	}

# Show icons if special or replied to
local $read = &amp;get_mail_read($folder, $mail);
if ($read&amp;2) {
	push(@rv, "&lt;img src=images/special.gif alt='*'&gt;");
	}
if ($read&amp;4) {
	push(@rv, "&lt;img src=images/replied.gif alt='R'&gt;");
	}

if ($showto &amp;&amp; defined(&amp;open_dsn_hash)) {
	# Show icons if DSNs received
	&amp;open_dsn_hash();
	local $mid = $mail-&gt;{'header'}-&gt;{'message-id'};
	if ($dsnreplies{$mid}) {
		push(@rv, "&lt;img src=images/dsn.gif alt='R'&gt;");
		}
	if ($delreplies{$mid}) {
		local ($bounce) = grep { /^\!/ }
			split(/\s+/, $delreplies{$mid});
		local $img = $bounce ? "red.gif" : "box.gif";
		push(@rv, "&lt;img src=images/$img alt='D'&gt;");
		}
	}
return @rv;
}

# show_mail_printable(&amp;mail, body, textbody, htmlbody)
# Output HTML for printing a message
sub show_mail_printable
{
local ($mail, $body, $textbody, $htmlbody) = @_;

# Display the headers
print &amp;ui_table_start($text{'view_headers'}, "width=100%", 2);
print &amp;ui_table_row($text{'mail_from'},
	&amp;convert_header_for_display($mail-&gt;{'header'}-&gt;{'from'}));
print &amp;ui_table_row($text{'mail_to'},
	&amp;convert_header_for_display($mail-&gt;{'header'}-&gt;{'to'}));
if ($mail-&gt;{'header'}-&gt;{'cc'}) {
	print &amp;ui_table_row($text{'mail_cc'},
		&amp;convert_header_for_display($mail-&gt;{'header'}-&gt;{'cc'}));
	}
print &amp;ui_table_row($text{'mail_date'},
	&amp;convert_header_for_display($mail-&gt;{'header'}-&gt;{'date'}));
print &amp;ui_table_row($text{'mail_subject'},
	&amp;convert_header_for_display(
		$mail-&gt;{'header'}-&gt;{'subject'}));
print &amp;ui_table_end(),"&lt;br&gt;\n";

# Just display the mail body for printing
print &amp;ui_table_start(undef, "width=100%", 2);
if ($body eq $textbody) {
	my $plain;
	foreach my $l (&amp;wrap_lines($body-&gt;{'data'},
				   $config{'wrap_width'} ||
				    $userconfig{'wrap_width'})) {
		$plain .= &amp;eucconv_and_escape($l)."\n";
		}
	print &amp;ui_table_row(undef, "&lt;pre&gt;$plain&lt;/pre&gt;", 2);
	}
elsif ($body eq $htmlbody) {
	print &amp;ui_table_row(undef,
		&amp;safe_html($body-&gt;{'data'}), 2);
	}
print &amp;ui_table_end();
}

# show_attachments_fields(count, server-side)
# Outputs HTML for new attachment fields
sub show_attachments_fields
{
local ($count, $server_attach) = @_;

# Work out if any attachments are supported
my $any_attach = $server_attach || !$main::no_browser_uploads;

if ($any_attach &amp;&amp; &amp;supports_javascript()) {
	# Javascript to increase attachments fields
	print &lt;&lt;EOF;
&lt;script&gt;
function add_attachment()
{
var block = document.getElementById("attachblock");
if (block) {
	var count = 0;
	var first_input = document.forms[0]["attach0"];
	while(document.forms[0]["attach"+count]) { count++; }
	var new_input = document.createElement('input');
	new_input.setAttribute('name', "attach"+count);
	new_input.setAttribute('type', 'file');
	if (first_input) {
		new_input.setAttribute('size',
			first_input.getAttribute('size'));
		new_input.setAttribute('class',
			first_input.getAttribute('class'));
		}
	block.appendChild(new_input);
	var new_br = document.createElement('br');
	block.appendChild(new_br);
	}
return false;
}
function add_ss_attachment()
{
var block = document.getElementById("ssattachblock");
if (block) {
	var count = 0;
	var first_input = document.forms[0]["file0"];
	while(document.forms[0]["file"+count]) { count++; }
	var new_input = document.createElement('input');
	new_input.setAttribute('name', "file"+count);
	if (first_input) {
		new_input.setAttribute('size',
			first_input.getAttribute('size'));
		new_input.setAttribute('class',
			first_input.getAttribute('class'));
		}
	block.appendChild(new_input);
	var new_br = document.createElement('br');
	block.appendChild(new_br);
	}
return false;
}
&lt;/script&gt;
EOF
	}

if ($any_attach) {
	# Show form for attachments (both uploaded and server-side)
	print &amp;ui_table_start($server_attach ? $text{'reply_attach2'}
					     : $text{'reply_attach3'},
			      "width=100%", 2);
	}

# Uploaded attachments
if (!$main::no_browser_uploads) {
	my $atable = "&lt;div&gt;\n";
	for(my $i=0; $i&lt;$count; $i++) {
		$atable .= &amp;ui_upload("attach$i", 80, 0,
				      "style='width:100%'", 1)."&lt;br&gt;";
		}
	$atable .= "&lt;/div&gt; &lt;div id=attachblock&gt;&lt;/div&gt;\n";
	print &amp;ui_hidden("attachcount", int($i)),"\n";
	print &amp;ui_table_row(undef, $atable, 2);
	}
if ($server_attach) {
	my $atable = "&lt;div&gt;\n";
	for(my $i=0; $i&lt;$count; $i++) {
		$atable .= &amp;ui_textbox("file$i", undef, 60, 0, undef,
				       "style='width:95%'").
			   &amp;file_chooser_button("file$i"),"&lt;br&gt;\n";
		}
	$atable .= "&lt;/div&gt; &lt;div id=sattachblock&gt;&lt;/div&gt;\n";
	print &amp;ui_table_row(undef, $atable, 2);
	print &amp;ui_hidden("ssattachcount", int($i)),"\n";
	}

# Links to add more fields
my @addlinks;
if (!$main::no_browser_uploads &amp;&amp; &amp;supports_javascript()) {
	push(@addlinks, "&lt;a href='' onClick='return add_attachment()'&gt;".
		        "$text{'reply_addattach'}&lt;/a&gt;" );
	}
if ($server_attach &amp;&amp; &amp;supports_javascript()) {
	push(@addlinks, "&lt;a href='' onClick='return add_ss_attachment()'&gt;".
			"$text{'reply_addssattach'}&lt;/a&gt;" );
	}
if ($any_attach) {
	print &amp;ui_table_row(undef, &amp;ui_links_row(\@addlinks), 2);
	print &amp;ui_table_end();
	}
}

# inputs_to_hiddens([&amp;in])
# Converts a hash as created by ReadParse into a list of names and values
sub inputs_to_hiddens
{
my $in = $_[0] || \%in;
my @hids;
foreach $i (keys %$in) {
	push(@hids, map { [ $i, $_ ] } split(/\0/, $in-&gt;{$i}));
	}
return @hids;
}

# ui_address_field(name, value, from-mode?, multi-line?)
# Returns HTML for a field for selecting an email address
sub ui_address_field
{
return &amp;theme_ui_address_field(@_) if (defined(&amp;theme_ui_address_field));
local ($name, $value, $from, $multi) = @_;
local @faddrs;
if (defined(&amp;list_addresses)) {
	@faddrs = grep { $_-&gt;[3] } &amp;list_addresses();
	}
local $f = $multi ? &amp;ui_textarea($name, $value, 3, 40, undef, 0,
				 "style='width:95%'")
		  : &amp;ui_textbox($name, $value, 40, 0, undef,
				"style='width:95%'");
if ((!$from || @faddrs) &amp;&amp; defined(&amp;address_button)) {
	$f .= " ".&amp;address_button($name, 0, $from);
	}
return $f;
}

# Returns 1 if spell checking is supported on this system
sub can_spell_check_text
{
return &amp;has_command("ispell");
}

# spell_check_text(text)
# Checks for spelling errors in some text, and returns a list of those found
# as HTML strings
sub spell_check_text
{
local ($plainbody) = @_;
local @errs;
pipe(INr, INw);
pipe(OUTr, OUTw);
select(INw); $| = 1; select(OUTr); $| = 1; select(STDOUT);
if (!fork()) {
	close(INw);
	close(OUTr);
	untie(*STDIN);
	untie(*STDOUT);
	untie(*STDERR);
	open(STDOUT, "&gt;&amp;OUTw");
	open(STDERR, "&gt;/dev/null");
	open(STDIN, "&lt;&amp;INr");
	exec("ispell -a");
	exit;
	}
close(INr);
close(OUTw);
local $indent = "&amp;nbsp;" x 4;
local $SIG{'PIPE'} = 'IGNORE';
local @errs;
foreach $line (split(/\n+/, $plainbody)) {
	next if ($line !~ /\S/);
	print INw $line,"\n";
	local @lerrs;
	while(1) {
		($spell = &lt;OUTr&gt;) =~ s/\r|\n//g;
		last if (!$spell);
		if ($spell =~ /^#\s+(\S+)/) {
			# Totally unknown word
			push(@lerrs, $indent.&amp;text('send_eword',
					"&lt;i&gt;".&amp;html_escape($1)."&lt;/i&gt;"));
			}
		elsif ($spell =~ /^&amp;\s+(\S+)\s+(\d+)\s+(\d+):\s+(.*)/) {
			# Maybe possible word, with options
			push(@lerrs, $indent.&amp;text('send_eword2',
					"&lt;i&gt;".&amp;html_escape($1)."&lt;/i&gt;",
					"&lt;i&gt;".&amp;html_escape($4)."&lt;/i&gt;"));
			}
		elsif ($spell =~ /^\?\s+(\S+)/) {
			# Maybe possible word
			push(@lerrs, $indent.&amp;text('send_eword',
					"&lt;i&gt;".&amp;html_escape($1)."&lt;/i&gt;"));
			}
		}
	if (@lerrs) {
		push(@errs, &amp;text('send_eline',
				"&lt;tt&gt;".&amp;html_escape($line)."&lt;/tt&gt;")."&lt;br&gt;".
				join("&lt;br&gt;", @lerrs));
		}
	}
close(INw);
close(OUTr);
return @errs;
}

# get_mail_charset(&amp;mail, &amp;body)
# Returns the character set to use for the HTML page for some email
sub get_mail_charset
{
my ($mail, $body) = @_;
my $ctype;
if ($body) {
	$ctype = $body-&gt;{'header'}-&gt;{'content-type'};
	}
$ctype ||= $mail-&gt;{'header'}-&gt;{'content-type'};
if ($ctype =~ /charset="([a-z0-9\-]+)"/i ||
    $ctype =~ /charset='([a-z0-9\-]+)'/i ||
    $ctype =~ /charset=([a-z0-9\-]+)/i) {
	$charset = $1;
	}
## Special handling of HTML header charset ($force_charset):
## For japanese text(ISO-2022-JP/EUC=JP/SJIS), the HTML output and
## text contents ($bodycontents) are already converted to EUC,
## so overriding HTML charset to that in the mail header ($charset)
## is generally wrong. (cf. mailbox/boxes-lib.pl:eucconv())
if ( &amp;get_charset() =~ /^EUC/i ) {	# EUC-JP,EUC-KR
	return undef;
	}
else {
	return $charset;
	}
}

# switch_to_folder_user(&amp;folder)
# If a folder has a user, switch the UID and GID used for writes to it
sub switch_to_folder_user
{
my ($folder) = @_;
if ($folder-&gt;{'user'} &amp;&amp; $switch_to_folder_count == 0) {
	&amp;set_mail_open_user($folder-&gt;{'user'});
	}
$switch_to_folder_count++;
}

# switch_from_folder_user(&amp;folder)
# Undoes the change made by switch_to_folder_user
sub switch_from_folder_user
{
my ($folder) = @_;
if ($switch_to_folder_count) {
	$switch_to_folder_count--;
	if ($switch_to_folder_count == 0) {
		&amp;clear_mail_open_user();
		}
	}
else {
	print STDERR "switch_from_folder_user called more often ",
		     "than switch_to_folder_user!\n";
	}
}

1;
</pre></body></html>