package openwebmail::storage_maildir;
use strict;

use Fcntl qw(:DEFAULT :flock);

#
# storage_maildir.pl - store mail in maildir format
# 2003/06/19 Laurent Frigault - lfrigault@agneau.org
#

###########################################################
sub make_new_tmp_message_file($$)
{
	use Sys::Hostname;

	my ($dirname,$size) = @_;

	return undef if ! -d $dirname;
	mkdir "$dirname/tmp",0750;

	# try 100 times max
	for my $num (1 .. 100)
	{
		my $handle=do { local *FH };

		# format  timestamp.pid_num.hostname,S=size
		# example 1053802755.P12936Q2.data.galacsys.net,S=1410

		# ugly work arround broken Sys::Hostname::hostname with empty PATH under perl 5.005_03/FreeBSD
		my $SAVEDPATH = $ENV{'PATH'}; 
		$ENV{'PATH'} = '/bin:/usr/bin';
		my $filename=time().'.P'.$$.'Q'.$num.'.'.&hostname().",S=$size";
		$ENV{'PATH'} = $SAVEDPATH;
		($filename =~ /^(.*)$/)
			&& ($filename = $1); ## untaint

		if (!sysopen($handle,"$dirname/tmp/$filename",O_CREAT|O_EXCL|O_RDWR,0640))
		{
			sleep(1);
			next;
		};
		return ($handle,$filename);
	};
	return undef;
}
###########################################################
sub move_tmp_message_to_cur($$)
{
	my ($folderdir,$filename) = @_;

	# just in case they are missing
	mkdir "$folderdir/new",0750;
	mkdir "$folderdir/cur",0750;
	mkdir "$folderdir/tmp",0750;

	if (link("$folderdir/tmp/$filename","$folderdir/cur/$filename"))
	{
		unlink "$folderdir/tmp/$filename";
		return $filename;
	}
	else
	{
		use Sys::Hostname;
		my $size = (stat("$folderdir/tmp/$filename"))[7];
		for my $num (1 .. 100)
		{
			# ugly work arround broken Sys::Hostname::hostname with empty PATH under perl 5.005_03/FreeBSD
			my $SAVEDPATH = $ENV{'PATH'};
			$ENV{'PATH'} = '/bin:/usr/bin';
			my $newfilename=time().'.P'.$$.'Q'.$num.'.'.&hostname().",S=$size";
			($newfilename =~ /^(.*)$/)
				&& ($newfilename = $1); ## untaint
			if (link("$folderdir/tmp/$filename","$folderdir/cur/$newfilename"))
			{
				unlink "$folderdir/tmp/$filename";
				return $newfilename;
			}
			else
			{
				sleep(1);
			};
		};
	};
	return '-1';
}
###########################################################
sub move_new_to_cur($)
{
	my $folderdir = shift;

	# just in case they are missing
	mkdir "$folderdir/new",0750;
	mkdir "$folderdir/cur",0750;
	mkdir "$folderdir/tmp",0750;

	my $dirhandle=do { local *FH };
	opendir($dirhandle ,"$folderdir/new");
	while (defined(my $filename = readdir($dirhandle)))
	{
		next if $filename =~ /^\./;
		next if ! -f "$folderdir/new/$filename";

		($filename =~ /^(.+)$/)
			&& ($filename = $1); # untaint ...

		if (link("$folderdir/new/$filename","$folderdir/cur/$filename"))
		{
			unlink "$folderdir/new/$filename"
				|| warn "unlink $folderdir/new/$filename";
		}
		else
		{
			# should not append
			warn "Unable to link $folderdir/new/$filename,$folderdir/cur/$filename\n";
		};
	};
	closedir($dirhandle);

	# cleanup tmp 
	opendir($dirhandle ,"$folderdir/tmp");
	my $old = time -36*3600;	# 36h in maildir spec
	while (defined(my $filename = readdir($dirhandle)))
	{
		next if $filename =~ /^\./;
		next if ! -f "$folderdir/tmp/$filename";
		if ((stat(_))[10]< $old)
		{
			my $path = "$folderdir/tmp/$filename";
			($path =~ /^(.+)$/)
				&& ($path= $1); # untaint ...
			unlink $path;
		};
	};	
	closedir($dirhandle);
}
###########################################################

################## STOREAGE_GETFOLDERS ####################
# return list of valid folders and calc the total folder usage(0..100%)
sub storage_getfolders($$$$$$)
{
	my ($folderdir,$r_folders, $r_usage, $user, $uuid, $ugid)=@_;
	my @userfolders;
	my $totalsize = 0;

	my $dirhandle=do { local *FH };
	opendir ($dirhandle, "$folderdir")
		or &main::openwebmailerror("$main::lang_err{'couldnt_open'} $folderdir! ($!)");
	while (defined(my $filename = readdir($dirhandle)))
	{
		next if ( $filename eq "." || $filename eq ".." );
		($filename =~ /^(.+)$/)
			&& ($filename = $1);   # untaint data from readdir
		if (-d "$folderdir/$filename")
		{
			next if $filename eq 'tmp' ||  $filename eq 'new';
			if ($filename eq 'cur')
			{
				$totalsize += &storage_folder_size($folderdir);
			}
			elsif ($filename =~ /^\.(.*)/)
			{
				# summary folder size
				$totalsize += &storage_folder_size("$folderdir/$filename");
	
				# find all user folders
				if ( $1 ne 'saved-messages' &&
					$1 ne  'sent-mail' &&
					$1 ne 'saved-drafts' &&
					$1 ne 'Trash'
					)
				{
					push(@userfolders, $1);
				}
			};
		};

	};
	closedir ($dirhandle) or
		&main::openwebmailerror("$main::lang_err{'couldnt_close'} $folderdir! ($!)");

	@{$r_folders}=();
	push (@{$r_folders},
		'INBOX', 'saved-messages', 'sent-mail', 'saved-drafts', 'mail-trash', 
		sort(@userfolders));

	${$r_usage}=$totalsize/1024;	# unit=k
	return;
}
################ END STORAGE_GETFOLDERS ##################

################## STORAGE_NEEDFOLDERLOCK ####################
sub storage_needfolderlock()
{
	return 0;
}
################## END STORAGE_NEEDFOLDERLOCK ####################

################## STORAGE_METAINFO ####################
sub storage_metainfo($)
{
	my $name = shift;
	if ( -e $name)
	{
		my @fdirs=("$name/new","$name/cur");      # start with root folderdir
		my $totalsize = 0;
		my $lasttime = 0;
		while (my $fdir=pop(@fdirs))
		{
			my $dirhandle=do { local *FH };
			opendir ($dirhandle, "$fdir")
				or next;
			while (defined(my $filename = readdir($dirhandle)))
			{
				next if $filename eq '..';

				my @a = stat ("$fdir/$filename");
				$lasttime = $a[9]
					if $lasttime<$a[9];
				$totalsize += $a[7];

			};
			close($dirhandle);
		};

		return("mtime=$lasttime size=$totalsize");
	}
	else
	{
		return("");
	};
}
################## END STORAGE_METAINFO ####################

############### STORAGE_GET_SPOOLFILE_FOLDERDB ################
sub storage_get_folderfile_headerdb($$$$)
{
	my ($username, $foldername, $folderdir, $homedir)=@_;
	my ($folderdirname, $headerdb);
	if ($foldername eq 'INBOX')
	{
		if ($main::config{'use_homedirspools'})
		{
			$folderdirname = "$homedir/$main::config{'homedirspoolname'}";
		}
		elsif ($main::config{'use_hashedmailspools'})
		{
			$username =~ /^(.)(.)/;
			my $firstchar = $1;
			my $secondchar = $2;
			$folderdirname = "$main::config{'mailspooldir'}/$firstchar/$secondchar/$username";
		}
		else
		{
			$folderdirname = "$main::config{'mailspooldir'}/$username";
		}

		($folderdirname =~ /^(.+)$/)
			&& ($folderdirname = $1); # untaint ...

		# move messages from new/* to cur/*
		&move_new_to_cur($folderdirname);

		$headerdb="$folderdirname/INBOX";
	}
	elsif ($foldername eq 'DELETE')
	{
		$folderdirname = $headerdb ='';
	}
	elsif ($foldername eq 'mail-trash' || $foldername eq 'Trash')
	{
		$folderdirname = "$folderdir/.Trash";
		$headerdb ="$folderdir/.Trash/Trash";
		($folderdirname =~ /^(.+)\/\.(.*)$/) && ($folderdirname = "$1/.$2");
		mkdir "$folderdirname",0750
			if (! -d $folderdirname);
		# move messages from new/* to cur/*
		&move_new_to_cur($folderdirname);

		($headerdb =~ /^(.+)\/\.(.*)$/) && ($headerdb = "$1/.$2");
	}
	else
	{
		$folderdirname = "$folderdir/.$foldername";
		($folderdirname =~ /^(.+)\/\.(.*)$/) && ($folderdirname = "$1/.$2");
		mkdir "$folderdirname",0750
			if (! -d $folderdirname &&
				(
					$foldername eq 'saved-messages' ||
					$foldername eq 'sent-mail' ||
					$foldername eq 'saved-drafts' ||
					$foldername eq 'mail-trash'
				)
			);

		# move messages from new/* to cur/*
		&move_new_to_cur($folderdirname);

		$headerdb = "$folderdirname/$foldername";
		($headerdb =~ /^(.+)\/\.(.*)$/) && ($headerdb = "$1/.$2");
	};

	($folderdirname =~ /^(.+)$/) && ($folderdirname = $1); # untaint ...
	($headerdb =~ /^(.+)$/) && ($headerdb = $1);

	return($folderdirname, $headerdb);
}
############### END STORAGE_GET_SPOOLFILE_FOLDERDB ################

######################### STORAGER_UPDATE_HEADERDB ############################
# this routine indexes the messages in a mailfolder
# and remove those with duplicated messageids
sub storage_update_headerdb($$)
{
	my ($headerdb, $folderfile) = @_;
	my (%HDB, %OLDHDB);
	my @oldmessageids=();
	my $dberr=0;

	($headerdb =~ /^(.+)$/) && ($headerdb = $1);		# untaint ...
	if ( -e "$headerdb$main::config{'dbm_ext'}" )
	{
		my ($metainfo, $allmessages, $internalmessages, $newmessages);

		if (!$main::config{'dbmopen_haslock'})
		{
			&main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_SH)
				or return -1;
		}
		dbmopen (%HDB, "$headerdb$main::config{'dbmopen_ext'}", undef);
		$metainfo=$HDB{'METAINFO'};
		$allmessages=$HDB{'ALLMESSAGES'};
		$internalmessages=$HDB{'INTERNALMESSAGES'};
		$newmessages=$HDB{'NEWMESSAGES'};
		dbmclose(%HDB);
		&main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_UN)
			if (!$main::config{'dbmopen_haslock'});

		if ( $metainfo eq &storage_metainfo($folderfile) && $allmessages >=0
			&& $internalmessages >=0 && $newmessages >=0 )
		{
			return 0;
		}

		if ($main::config{'dbm_ext'} eq 'dir')
		{
			rename("$headerdb.dir", "$headerdb.old.dir");
			rename("$headerdb.pag", "$headerdb.old.pag");
		}
		else
		{
			rename("$headerdb$main::config{'dbm_ext'}", "$headerdb.old$main::config{'dbm_ext'}");
		}

		# we will try to reference records in old headerdb if possible
		@oldmessageids=&main::get_messageids_sorted_by_offset("$headerdb.old");
		if (!$main::config{'dbmopen_haslock'})
		{
			&main::filelock("$headerdb.old$main::config{'dbm_ext'}", LOCK_SH)
				or return -1;
		}
		dbmopen(%OLDHDB, "$headerdb.old$main::config{'dbmopen_ext'}", undef);
	}

	my ($messagenumber, $newmessages, $internalmessages) = (-1, 0, 0);
	my ($inheader, $offset, $totalsize)=(0, 0, 0);
	my @duplicateids=();

	my ($line, $lastheader, $delimiter, $has_att, $verified);
	my ($_message_id);
	my ($_from, $_to, $_date, $_subject);
	my ($_content_type, $_status, $_messagesize, $_references, $_inreplyto, $_charset);

	dbmopen(%HDB, "$headerdb$main::config{'dbmopen_ext'}", 0600);
	if (!$main::config{'dbmopen_haslock'})
	{
		&main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_EX)
			or return -1;
	}
	%HDB=();	# ensure the headerdb is empty

	my $folderhandle=do { local *FH };

	opendir($folderhandle,"$folderfile/cur")
		|| warn "can't opendir $folderfile/cur: $!";
	
	while(defined (my $filename = readdir($folderhandle)))
	{
		next if $filename =~ /^\./;
		my $messagefile = "$folderfile/cur/$filename";
		next if ! -f $messagefile;
		($messagefile =~ /^(.+)$/)
			&& ($messagefile = $1); # untaint

		my $messagehandle=do { local *MH };

		if (!open($messagehandle,$messagefile))
		{
			warn "Can't open $messagefile $!";
			next;
		};
		$offset = $filename;
		($_message_id, $_from, $_to, $_date, $_subject, $_content_type, $_status, $_messagesize, $_references, $_charset)
			= &main::parse_message($messagehandle,\$newmessages,\$internalmessages);
		close($messagehandle);
		if (!defined $_message_id)
		{
			last;
		};

		$messagenumber++;
		$totalsize+=$_messagesize;
    
		if (! defined($HDB{$_message_id}) )
		{
			$HDB{$_message_id}=&main::make_msgrecord($_message_id, $offset, $_from, $_to,
				$_date, $_subject, $_content_type, $_status, $_messagesize, $_references, $_charset)
				or $dberr++;
			if ($dberr && defined(%OLDHDB))
			{
				dbmclose(%OLDHDB);
				&main::filelock("$headerdb.old$main::config{'dbm_ext'}", LOCK_UN) if (!$main::config{'dbmopen_haslock'});
				unlink("$headerdb.old$main::config{'dbm_ext'}", "$headerdb.old.dir", "$headerdb.old.pag");
				$dberr=0;
				$HDB{$_message_id}=&main::make_msgrecord($_message_id, $offset, $_from, $_to,
					$_date, $_subject, $_content_type, $_status, $_messagesize, $_references, $_charset)
					or $dberr++;
			}
		}
		else
		{
			my $dup=$#duplicateids+1;
			$HDB{"dup$dup-$_message_id"}=&main::make_msgrecord("dup$dup-$_message_id", $offset, $_from, $_to,
				$_date, $_subject, $_content_type, $_status, $_messagesize, $_references, $_charset)
				or $dberr++;
			if ($dberr && defined(%OLDHDB))
			{
				dbmclose(%OLDHDB);
				&main::filelock("$headerdb.old$main::config{'dbm_ext'}", LOCK_UN) if (!$main::config{'dbmopen_haslock'});
				unlink("$headerdb.old$main::config{'dbm_ext'}", "$headerdb.old.dir", "$headerdb.old.pag");
				$dberr=0;
				$HDB{"dup$dup-$_message_id"}=&main::make_msgrecord("dup$dup-$_message_id", $offset, $_from, $_to,
					$_date, $_subject, $_content_type, $_status, $_messagesize, $_references, $_charset)
					or $dberr++;
			}
			push(@duplicateids, "dup$dup-$_message_id") if (!$dberr);
		}

	};
	closedir($folderhandle);

	if ( !$dberr )
	{
		$HDB{'ALLMESSAGES'}=$messagenumber+1;
		$HDB{'INTERNALMESSAGES'}=$internalmessages;
		$HDB{'NEWMESSAGES'}=$newmessages;
		$HDB{'METAINFO'}=&storage_metainfo($folderfile) || $dberr++;
	}

	&main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_UN)
		if (!$main::config{'dbmopen_haslock'});
	dbmclose(%HDB);

	# remove old headerdb
	if (defined(%OLDHDB))
	{
		dbmclose(%OLDHDB);
		&main::filelock("$headerdb.old$main::config{'dbm_ext'}", LOCK_UN)
			if (!$main::config{'dbmopen_haslock'});
		unlink("$headerdb.old$main::config{'dbm_ext'}", "$headerdb.old.dir", "$headerdb.old.pag");
	}
	if ($dberr)
	{
		return -1;
	}
	else
	{
		# remove if any duplicates
		&storage_operate_message_with_ids("delete", \@duplicateids, $folderfile, $headerdb)
			if ($#duplicateids>=0);
		return 1;
	}
}
##################### END STORAGE_UPDATE_HEADERDB #########################
##################### STORAGE_GET_MESSAGE_BLOCK #########################
sub storage_get_message_block ($$$)
{
	my ($messageid, $headerdb, $folderdir)=@_;
	my (@attr, $buff);

	@attr=&main::get_message_attributes($messageid, $headerdb);
	if ($#attr>=7 && $attr[$main::_SIZE]>=0)
	{
		my $messagehandle=do { local *FH };
		open($messagehandle,"<$folderdir/cur/$attr[$main::_OFFSET]")
			|| return '';
		read($messagehandle, $buff, $attr[$main::_SIZE]);
		close($messagehandle);
	}
	else
	{
		$buff="";
	};

	return(\$buff);
}
##################### END STORAGE_GET_MESSAGE_BLOCK #########################
##################### STORAGE_GET_MESSAGE_BLOCK_OFFSET ######################
sub storage_get_message_block_offset($$$)
{
	my ($offset, $size, $folderdir)=@_;
	my $buff='';
	my @lines=();
	my $messagehandle=do { local *FH };

	open($messagehandle, "$folderdir/cur/$offset");

	if (wantarray)
	{
		my $left=$size;
		while ($left>0)
		{
			my $line = <$messagehandle>;
			$left-=length($line);
			push @lines,$line;
		};
	}
	else
	{
		read($messagehandle, $buff, $size);
	};

	close($messagehandle);

	return wantarray?@lines:\$buff;
}
##################### END STORAGE_GET_MESSAGE_BLOCK_OFFSET ###################


##################### STORAGE_GET_MESSAGE_HEADER #########################
sub storage_get_message_header($$$)
{
	my ($messageid, $headerdb, $folderdir)=@_;
	my (@attr, $header);

	my $messagehandle=do { local *FH };

	@attr=&main::get_message_attributes($messageid, $headerdb);
	if ($#attr>=0)
	{
		open($messagehandle,"<$folderdir/cur/$attr[$main::_OFFSET]")
			|| return "";
		$header="";
		while(<$messagehandle>)
		{
			$header.=$_;
			last if ($_ eq "\n");
		};
		close($messagehandle);
	}
	else
	{
		$header="";
	};
	return(\$header);
}
##################### END STORAGE_GET_MESSAGE_HEADER #########################

##################### STORAGE_GET_MESSAGE_HEADER_OFFSET ######################
sub storage_get_message_header_offset($$)
{
	my ($offset, $folderdir)=@_;

	my $messagehandle=do { local *FH };

	open($messagehandle, "<$folderdir/cur/$offset")
		|| return "";

	my $header="";
	while(<$messagehandle>)
	{
		$header.=$_;
		last if ($_ eq "\n");
	}
	close($messagehandle);

	return(\$header);
}
##################### END STORAGE_GET_MESSAGE_HEADER_OFFSET ##################

###################### STORAGE_UPDATE_MESSAGE_STATUS ########################
sub storage_update_message_status($$$$)
{
	my ($messageid, $status, $headerdb, $folderfile) = @_;
	my $messageoldstatus='';
	my $messagehandle=do { local *FH };
	my %HDB;
	my $ioerr=0;

	if (&storage_update_headerdb($headerdb, $folderfile)<0)
	{
		&main::folderfilelock($folderfile, LOCK_UN);
		&main::writelog("db error - Couldn't update index db $headerdb$main::config{'dbm_ext'}");
		&main::writehistory("db error - Couldn't update index db $headerdb$main::config{'dbm_ext'}");
		return -1;
	};

	my @messageids=&main::get_messageids_sorted_by_offset($headerdb);
	my $movement=0;
	my @attr;

	if (!$main::config{'dbmopen_haslock'})
	{
		&main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_EX)
			or return -1;
	};
	dbmopen (%HDB, "$headerdb$main::config{'dbmopen_ext'}", 0600);

	for (my $i=0; $i<=$#messageids; $i++)
	{
		if ($messageids[$i] eq $messageid)
		{
			@attr=split(/@@@/, $HDB{$messageid});

			$messageoldstatus=$attr[$main::_STATUS];
			last if ($messageoldstatus eq $status);

			my $messagestart=0;
			my $messagesize=$attr[$main::_SIZE];
			my ($header, $headerend, $headerlen, $newheaderlen);
			my $buff;

			&main::filelock("$folderfile/cur/$attr[$main::_OFFSET]",LOCK_SH|LOCK_NB)
				or &main::openwebmailerror("$main::lang_err{'couldnt_locksh'} $folderfile/cur/$attr[$main::_OFFSET]!");
			open ($messagehandle, "+<$folderfile/cur/$attr[$main::_OFFSET]");
			$header="";
			while (<$messagehandle>)
			{
				last if ($_ eq "\n" && $header=~/\n$/);
				$header.=$_;
			}
			$headerlen=length($header);
			$headerend=$messagestart+$headerlen;

			# update status, flags from rfc2076
			my $status_update = "";
			$status_update .= "R" if ($status =~ m/r/i); # Read
			$status_update .= "U" if ($status =~ m/u/i); # undownloaded & not deleted
			$status_update .= "N" if ($status =~ m/n/i); # New
			$status_update .= "D" if ($status =~ m/d/i); # to be Deleted
			$status_update .= "O" if ($status =~ m/o/i); # Old
			if ($status_update ne "")
			{
				if (!($header =~ s/^status:.*\n/Status: $status_update\n/im))
				{
					$header .= "Status: $status_update\n";
				};
			}
			else
			{
				$header =~ s/^status:.*\n//im;
			}

			# update x-status
			$status_update = "";
			$status_update .= "A" if ($status =~ m/a/i); # Answered
			$status_update .= "I" if ($status =~ m/i/i); # Important
			$status_update .= "D" if ($status =~ m/d/i); # to be Deleted
			if ($status_update ne "")
			{
				if (!($header =~ s/^x-status:.*\n/X-Status: $status_update\n/im))
				{
					$header .= "X-Status: $status_update\n";
				};
			}
			else
			{
				$header =~ s/^x-status:.*\n//im;
			};

			$newheaderlen=length($header);
			$movement=$newheaderlen-$headerlen;

			my $filesize=(stat($messagehandle))[7];
			if (&main::shiftblock($messagehandle, $headerend, $filesize-$headerend, $movement)<0)
			{
				&main::writelog("data error - msg $messageids[$i] in $folderfile/cur/$attr[$main::_OFFSET] shiftblock failed");
				&main::writehistory("data error - msg $messageids[$i] in $folderfile/cur/$attr[$main::_OFFSET] shiftblock failed");
				$ioerr++;
			};

			if (!$ioerr)
			{
				seek($messagehandle, $messagestart, 0);
				print $messagehandle $header || $ioerr++;
			}
			if (!$ioerr)
			{
				seek($messagehandle, $filesize+$movement, 0);
				truncate($messagehandle, tell($messagehandle));
			}
			close ($messagehandle);
			&main::filelock("$folderfile/cur/$attr[$main::_OFFSET]",LOCK_UN);

			if (!$ioerr)
			{
				# set attributes in headerdb for this status changed message
				if ($messageoldstatus!~/r/i && $status=~/r/i)
				{
					$HDB{'NEWMESSAGES'}--;
					$HDB{'NEWMESSAGES'}=0 if ($HDB{'NEWMESSAGES'}<0); # should not happen
				}
				elsif ($messageoldstatus=~/r/i && $status!~/r/i)
				{
					$HDB{'NEWMESSAGES'}++;
				}
				$attr[$main::_SIZE]=$messagesize+$movement;
				$attr[$main::_STATUS]=$status;
				$HDB{$messageid}=join('@@@', @attr);
			}

		last;
		}
	}

	# update folder metainfo
	$HDB{'METAINFO'}=&storage_metainfo($folderfile) if (!$ioerr);

	dbmclose(%HDB);
	&main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_UN) if (!$main::config{'dbmopen_haslock'});

	if (!$ioerr)
	{
		return 0;
	}
	else
	{
		return -3;
	};
}
#################### END STORAGE_UPDATE_MESSAGE_STATUS ######################

###################### STORAGE_OP_MESSAGE_WITH_IDS #########################
# operate messages with @messageids from src folderfile to dst folderfile
# available $op: "move", "copy", "delete"
sub storage_operate_message_with_ids($$$$;$$)
{
	my ($op, $r_messageids, $srcdir, $srcdb, $dstdir, $dstdb)=@_;
	my (%HDB, %HDB2);
	my $messageids = join("\n", @{$r_messageids});
	my $ioerr=0;

	# $lang_err{'inv_msg_op'}
	return -1 if ($op ne "move" && $op ne "copy" && $op ne "delete");
	return 0 if ($srcdir eq $dstdir || $#{$r_messageids} < 0);

	if (&storage_update_headerdb($srcdb, $srcdir)<0)
	{
		&main::writelog("db error - Couldn't update index db $srcdb$main::config{'dbm_ext'}");
		&main::writehistory("db error - Couldn't update index db $srcdb$main::config{'dbm_ext'}");
		return -2;
	};

	return -3 if ! -d $srcdir;

	if ($op eq "move" || $op eq "copy")
	{
		return -5 if ! -d $dstdir;
		if (&storage_update_headerdb("$dstdb", $dstdir)<0)
		{
			&main::writelog("db error - Couldn't update index db $dstdb$main::config{'dbm_ext'}");
			&main::writehistory("db error - Couldn't update index db $dstdb$main::config{'dbm_ext'}");
			return -4;
		}
	};

	my @allmessageids=&main::get_messageids_sorted_by_offset($srcdb);
	my (@attr, $buff);
	my $counted=0;

	if (!$main::config{'dbmopen_haslock'})
	{
		&main::filelock("$srcdb$main::config{'dbm_ext'}", LOCK_EX) or return -6;
	}
	dbmopen (%HDB, "$srcdb$main::config{'dbmopen_ext'}", 0600);

	if ($op eq "move" || $op eq "copy")
	{
		if (!$main::config{'dbmopen_haslock'})
		{
			&main::filelock("$dstdb$main::config{'dbm_ext'}", LOCK_EX) or return -7;
		}
		dbmopen (%HDB2, "$dstdb$main::config{'dbmopen_ext'}", 0600);
	}

	for (my $i=0; $i<=$#allmessageids; $i++)
	{
		@attr=split(/@@@/, $HDB{$allmessageids[$i]});

		if ($messageids =~ /^\Q$allmessageids[$i]\E$/m)
		{ # msg to be operated
			$counted++;

			# append msg to dst folder only if
			# op=move/copy and msg doesn't exist in dstfile
			if (($op eq "move" || $op eq "copy") &&
				!defined($HDB2{$allmessageids[$i]}) && !$ioerr )
			{
				my ($handledst,$filedst) = &make_new_tmp_message_file($dstdir,(stat("$srcdir/cur/$attr[$main::_OFFSET]"))[7]);
				open(SRC,"<$srcdir/cur/$attr[$main::_OFFSET]");

				# copy message from SRC to $handledst
				my $left=$attr[$main::_SIZE];
				while ($left>0)
				{
					if ($left>=32768)
					{
						read(SRC, $buff, 32768);
						print $handledst $buff || $ioerr++;
						$left=$left-32768;
					}
					else
					{
						read(SRC, $buff, $left);
						print $handledst $buff || $ioerr++;
						$left=0;
					};
				};
				close(SRC);
				close($handledst);
				if($ioerr)
				{
					&main::writelog("data error - msg $allmessageids[$i] in $filedst print failed");
                    &main::writehistory("data error - msg $allmessageids[$i] in $filedst print failed");
					unlink "$dstdir/tmp/$filedst";
				}
				else
				{
					my $newfiledest = &move_tmp_message_to_cur($dstdir,$filedst);
					if($newfiledest eq '-1')
					{
						unlink "$dstdir/tmp/$filedst";
						$ioerr++;
					}
					else
					{
						$filedst = $newfiledest;
						$HDB2{'NEWMESSAGES'}++ if ($attr[$main::_STATUS]!~/r/i);
						$HDB2{'INTERNALMESSAGES'}++ if (&main::is_internal_subject($attr[$main::_SUBJECT]));
						$HDB2{'ALLMESSAGES'}++;
						my @newattr=@attr;
						$newattr[$main::_OFFSET]=$filedst;
						$HDB2{$allmessageids[$i]}=join('@@@', @newattr);
					};
				};
			};

			if (($op eq 'move' || $op eq 'delete') && !$ioerr)
			{
				if (!unlink "$srcdir/cur/$attr[$main::_OFFSET]")
				{
					&main::writelog("data error - msg $allmessageids[$i] in $srcdir/$attr[$main::_OFFSET] unlink failed");
					&main::writehistory("data error - msg $allmessageids[$i] in $srcdir/$attr[$main::_OFFSET] unlink failed");
					$ioerr++;
				};

				$HDB{'NEWMESSAGES'}-- if ($attr[$main::_STATUS]!~/r/i);
				$HDB{'NEWMESSAGES'}=0 if ($HDB{'NEWMESSAGES'}<0); # should not happen
				$HDB{'INTERNALMESSAGES'}-- if (&main::is_internal_subject($attr[$main::_SUBJECT]));
				$HDB{'ALLMESSAGES'}--;
				delete $HDB{$allmessageids[$i]};
			};

		};

		last if ($ioerr);
	};

	if ($op eq "move" || $op eq "copy")
	{
		$HDB2{'METAINFO'}=&storage_metainfo($dstdir) if (!$ioerr);
		dbmclose(%HDB2);
		&main::filelock("$dstdb$main::config{'dbm_ext'}", LOCK_UN) if (!$main::config{'dbmopen_haslock'});
	};

	$HDB{'METAINFO'}=&storage_metainfo($srcdir) if (!$ioerr);
	dbmclose(%HDB);
	&main::filelock("$srcdb$main::config{'dbm_ext'}", LOCK_UN) if (!$main::config{'dbmopen_haslock'});

	unlink "$srcdir/../maildirsize";
	if (!$ioerr)
	{
		return($counted);
	}
	else
	{
		return -8;
	};
}
#################### END STORAGE_OP_MESSAGE_WITH_IDS #######################

##################### STORAGE_REBUILD_MESSAGE_WITH_PARTIALID #################
# rebuild orig msg with partial msgs in the same folder
sub xstorage_rebuild_message_with_partialid($$$)
{
   my ($folderfile, $headerdb, $partialid)=@_;
   my (%HDB, @messageids);
   my ($partialtotal, @partialmsgids, @offset, @size);

   if (&storage_update_headerdb($headerdb, $folderfile)<0) {
      &main::folderfilelock($folderfile, LOCK_UN);
      &main::writelog("db error - Couldn't update index db $headerdb$main::config{'dbm_ext'}");
      &main::writehistory("db error - Couldn't update index db $headerdb$main::config{'dbm_ext'}");
      return -1;
   }

   # find all partial msgids
   if (!$main::config{'dbmopen_haslock'}) {
      &main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_SH) or return -2;
   }
   dbmopen (%HDB, "$headerdb$main::config{'dbmopen_ext'}", undef);
   @messageids=keys %HDB;
   foreach my $id (@messageids) {
      next if ( $id eq 'METAINFO'
             || $id eq 'NEWMESSAGES'
             || $id eq 'INTERNALMESSAGES'
             || $id eq 'ALLMESSAGES'
             || $id eq "" );

      my @attr=split( /@@@/, $HDB{$id} );
      next if ($attr[$main::_CONTENT_TYPE] !~ /^message\/partial/i );

      $attr[$main::_CONTENT_TYPE] =~ /;\s*id="(.+?)";?/i;
      next if ($partialid ne $1);

      if ($attr[$main::_CONTENT_TYPE] =~ /;\s*number="?(.+?)"?;?/i) {
         my $n=$1;
         $partialmsgids[$n]=$id;
         $offset[$n]=$attr[$main::_OFFSET];
         $size[$n]=$attr[$main::_SIZE];
         $partialtotal=$1 if ($attr[$main::_CONTENT_TYPE] =~ /;\s*total="?(.+?)"?;?/i);
      }
   }
   dbmclose(%HDB);
   &main::filelock("$headerdb$main::config{'dbm_ext'}", LOCK_UN) if (!$main::config{'dbmopen_haslock'});

   # check completeness
   if ($partialtotal<1) {	# last part not found
      return -3;
   }
   for (my $i=1; $i<=$partialtotal; $i++) {
      if ($partialmsgids[$i] eq "") {	# some part missing
         return -4;
      }
   }

   my $tmpfile="/tmp/rebuild_tmp_$$";
   my $tmpdb="/tmp/.rebuild_tmp_$$";
   ($tmpfile =~ /^(.+)$/) && ($tmpfile = $1);
   ($tmpdb =~ /^(.+)$/) && ($tmpdb = $1);

   &main::folderfilelock("$tmpfile", LOCK_EX) or return -5;
   open (TMP,  ">$tmpfile");
   open (FOLDER, "$folderfile");

   seek(FOLDER, $offset[1], 0);
   my $line = <FOLDER>;
   my $writtensize = length($line);
   print TMP $line;	# copy delimiter line from 1st partial message

   for (my $i=1; $i<=$partialtotal; $i++) {
      my $currsize=0;
      seek(FOLDER, $offset[$i], 0);

      # skip header of the partial message
      while (defined($line = <FOLDER>)) {
         $currsize += length($line);
         last if ( $line =~ /^\r*$/ );
      }

      # read body of the partial message and copy to tmpfile
      while (defined($line = <FOLDER>)) {
         $currsize += length($line);
         $writtensize += length($line);
         print TMP $line;
         last if ( $currsize>=$size[$i] );
      }
   }

   close(TMP);
   close(FOLDER);
   &main::folderfilelock("$tmpfile", LOCK_EX) or return -6;

   # index tmpfile, get the msgid
   if (&storage_update_headerdb($tmpdb, $tmpfile)<0) {
      &main::folderfilelock($tmpfile, LOCK_UN);
      &main::writelog("db error - Couldn't update index db $tmpdb$main::config{'dbm_ext'}");
      &main::writehistory("db error - Couldn't update index db $tmpdb$main::config{'dbm_ext'}");
      return -7;
   }

   # check the rebuild integrity
   my @rebuildmsgids=&main::get_messageids_sorted_by_offset($tmpdb);
   if ($#rebuildmsgids!=0) {
      unlink("$tmpdb$main::config{'dbm_ext'}", $tmpfile);
      return -8;
   }
   my $rebuildsize=(&main::get_message_attributes($rebuildmsgids[0], $tmpdb))[$main::_SIZE];
   if ($writtensize!=$rebuildsize) {
      unlink("$tmpdb$main::config{'dbm_ext'}", $tmpfile);
      return -9;
   }

   my $moved=&storage_operate_message_with_ids("move", \@rebuildmsgids,
				$tmpfile, $tmpdb, $folderfile, $headerdb);

   unlink("$tmpdb$main::config{'dbm_ext'}", $tmpfile);

   return(0, $rebuildmsgids[0], @partialmsgids);
}
################ END STORAGE_REBUILD_MESSAGE_WITH_PARTIALID ################

################ STORAGE_ADD_MESSAGE ################
sub storage_add_message($$$)
{
	my ($folderdir,$r_message,$r_attr) = @_;

	my ($messagehandler,$messagefilename)=&make_new_tmp_message_file($folderdir,length(${$r_message}));
	return -1 if ! defined $messagehandler;

	if(!print $messagehandler ${$r_message})
	{
		close($messagehandler);
		unlink "$folderdir/$messagefilename";
		return -2;
	};
	close($messagehandler);
	my $newmessagefilename = &move_tmp_message_to_cur($folderdir,$messagefilename);
	if($newmessagefilename eq '-1')
	{
		unlink "$folderdir/tmp/$messagefilename";
		return -1;
	}
	else
	{
		$messagefilename = $newmessagefilename;
	}

	${$r_attr}[$main::_OFFSET]=$messagefilename;
	${$r_attr}[$main::_SIZE]=length(${$r_message});

	return 0;
}
################ END STORAGE_ADD_MESSAGE ################

################ STORAGE_REMOVE_MESSAGE_OFFSET ################
sub storage_remove_message_offset($$$)
{
	my ($offset, $size, $folderdir)=@_;

	my $messagefile="$folderdir/cur/$offset";
	return -1 if $messagefile =~ /\.\./;
	($messagefile =~ /^(.*)$/)
		&& ($messagefile = $1); # untaint
	unlink $messagefile
		|| return -1;

	unlink "$folderdir/../maildirsize";
	return 0;
}
################ END STORAGE_REMOVE_MESSAGE_OFFSET ################

################ STORAGE_FOLDER_EXIST ################
sub storage_folder_exist($)
{
	my $dirname = shift;

	return (-d $dirname);
}
################ END STORAGE_FOLDER_EXIST ################

################ STORAGE_FOLDER_SIZE ################
sub storage_folder_size($)
{
	my $dirname = shift;

	return 0 if ! -d $dirname;

	my @dirs=("$dirname/new","$dirname/cur");

	my $size = 0;
	while (my $dir=pop(@dirs))
	{
		my $dirhandle=do { local *FH };
		next if !opendir($dirhandle,$dir);
		
		while (defined(my $filename = readdir($dirhandle)))
		{
			# ignore .xxx names
			next if $filename =~ /^\./;

			$size += (stat(_))[7]
				if (-f "$dir/$filename");
		};
		closedir($dirhandle)
	};

	return $size
}
################ END STORAGE_FOLDER_SIZE ################

################ STORAGE_CREATE_FOLDER ################
sub storage_create_folder($)
{
	my $dirname = shift;

	mkdir $dirname,0750
		|| return -1;
	mkdir "$dirname/new",0750
		|| return -1;
	mkdir "$dirname/tmp",0750
		|| return -1;
	mkdir "$dirname/cur",0750
		|| return -1;
	open (MAILDIRFOLDER,">$dirname/maildirfolder")
		|| return -1;
	close(MAILDIRFOLDER);

	return 0;
}
################ END STORAGE_CREATE_FOLDER ################

################ STORAGE_DELETE_FOLDER ################
sub storage_delete_folder($)
{
	my $dirname = shift;

	my $err=0;
	my $dirhandle=do { local *FH };
	opendir($dirhandle,$dirname)
		|| return -1;

	while (defined(my $filename = readdir($dirhandle)))
	{
		next if ($filename eq '.') or ($filename eq '..');

		($filename =~ /^(.+)$/) && ($filename = $1);   # untaint data from readdir
		if (-d "$dirname/$filename")
		{
			$err = -1 if &storage_delete_folder("$dirname/$filename") == -1;
		}
		else
		{
			unlink("$dirname/$filename")
				or $err = -1;
		};
	};
	closedir($dirhandle) 
		or $err = -1;

	rmdir($dirname)
		or $err = -1;

	unlink "$dirname/../maildirsize";
	return $err;
}
################ END STORAGE_CREATE_FOLDER ################

################ STORAGE_EMPTY_FOLDER ################
sub storage_empty_folder($)
{
	my $dirname = shift;
	my $dirhandle=do { local *FH };

	opendir($dirhandle,"$dirname/cur")
		|| return -1;

	my $err=0;
	while (defined(my $filename = readdir($dirhandle)))
	{
		# ignore .xxx names
		next if $filename =~ /^\./;
		# ignore non files
		next if ! -f "$dirname/cur/$filename";

		($filename =~ /^(.+)$/) && ($filename = $1);   # untaint data from readdir
		unlink("$dirname/cur/$filename")
			or $err = -1;
	};
	closedir($dirhandle) 
		or $err = -1;

	unlink "$dirname/../maildirsize";
	return $err;
}
################ END STORAGE_CREATE_FOLDER ################

################ STORAGE_FOLDER_RENAME ################
sub storage_folder_rename($$)
{
    my ($old,$new) = @_;
	my $err=0;
	$err = &storage_create_folder($new);

	my $dirhandle=do { local *FH };

	opendir($dirhandle,"$old/new")
		or return -1;
	while (defined(my $filename = readdir($dirhandle)))
	{
		next if $filename =~ /^\./;
		($filename =~ /^(.+)$/) && ($filename = $1);
		if (!rename("$old/new/$filename","$new/new/$filename"))
		{
			closedir($dirhandle);
			return -1;
		};
	};
	closedir($dirhandle)
		or return -1;
	
	opendir($dirhandle,"$old/cur")
		or return -1;
	while (defined(my $filename = readdir($dirhandle)))
	{
		next if $filename =~ /^\./;
		($filename =~ /^(.+)$/) && ($filename = $1);
		if (!rename("$old/cur/$filename","$new/cur/$filename"))
		{
			closedir($dirhandle);
			return -1;
		};
	};
	closedir($dirhandle)
		or return -1;
	
	opendir($dirhandle,"$old")
		or return -1;
	while (defined(my $filename = readdir($dirhandle)))
	{
		next if $filename =~ /^\./;
		next if $filename eq 'maildirfolder';
		next if -d "$old/$filename";
		($filename =~ /^(.+)$/) && ($filename = $1);
		unlink "$old/$filename";
	};
	closedir($dirhandle)
		or return -1;

	rmdir("$old/new")or return -1;
	rmdir("$old/cur")or return -1;
	rmdir("$old/tmp")or return -1;
	unlink("$old/maildirfolder")or return -1;
	rmdir($old) or return -1;

	return $err;
}
################ END STORAGE_FOLDER_RENAME ################
################ STORAGE_COMPARE_OFFSET  ################
sub storage_compare_offset($$)
{
	# format  timestamp.pid_num.hostname,S=size(:[12],[A-Z]*)?
	# example 1053802755.P?12936[_Q]2.data.galacsys.net,S=1410:2,S
    my ($a,$b) = @_;
	$a =~ m/^(\d+)\.P?(\d+)[_Q](\d+)\.([^\,]+)/;
	my @a=($1,$2,$3,$4);
	$b =~ m/^(\d+)\.P?(\d+)[_Q](\d+)\.([^\,]+)/;
	my @b=($1,$2,$3,$4);

    return (
		($a[0] <=> $b[0]) || 
		($a[1] <=> $b[1]) || 
		($a[2] <=> $b[2]) || 
		($a[3] cmp $b[3])
	);
}
################ END STORAGE_COMPARE_OFFSET ################

1;
