!c99Shell v. 1.0 pre-release build #16!

Software: Apache/2.2.3 (CentOS). PHP/5.1.6 

uname -a: Linux mx-ll-110-164-51-230.static.3bb.co.th 2.6.18-194.el5PAE #1 SMP Fri Apr 2 15:37:44
EDT 2010 i686
 

uid=48(apache) gid=48(apache) groups=48(apache) 

Safe-mode: OFF (not secure)

/usr/libexec/webmin/sendmail/   drwxr-xr-x
Free 50.88 GB of 127.8 GB (39.81%)
Home    Back    Forward    UPDIR    Refresh    Search    Buffer    Encoder    Tools    Proc.    FTP brute    Sec.    SQL    PHP-code    Update    Feedback    Self remove    Logout    


Viewing file:     boxes-lib.pl (68.78 KB)      -rwxr-xr-x
Select action/file-type:
(+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# boxes-lib.pl
# Functions to parsing user mail files

use POSIX;
if ($userconfig{'date_tz'} || $config{'date_tz'}) {
        # Set the timezone for all date calculations, and force a conversion
        # now as in some cases the first one fails!
        $ENV{'TZ'} = $userconfig{'date_tz'} ||
                     $config{'date_tz'};
        strftime('%H:%M', localtime(time()));
        }
use Time::Local;

$dbm_index_min = 1000000;
$dbm_index_version = 3;

# list_mails(user|file, [start], [end])
# Returns a subset of mail from a mbox format file
sub list_mails
{
local (@rv, $h, $done);
my %index;
&build_dbm_index($_[0], \%index);
local ($start, $end);
local $isize = $index{'mailcount'};
if (@_ == 1 || !defined($_[1]) && !defined($_[2])) {
    $start = 0; $end = $isize-1;
    }
elsif ($_[2] < 0) {
    $start = $isize+$_[2]-1; $end = $isize+$_[1]-1;
    $start = $start<0 ? 0 : $start;
    }
else {
    $start = $_[1]; $end = $_[2];
    $end = $isize-1 if ($end >= $isize);
    }
$rv[$isize-1] = undef if ($isize);    # force array to right size
local $dash = &dash_mode($_[0]);
open(MAIL, &user_mail_file($_[0]));
$start = 0 if ($start < 0);
for($i=$start; $i<=$end; $i++) {
    # Seek to mail position
    local @idx = split(/\0/, $index{$i});
    local $pos = $idx[0];
    local $startline = $idx[1];
    seek(MAIL, $pos, 0);

    # Read the mail
    local $mail = &read_mail_fh(MAIL, $dash ? 2 : 1, 0);
    $mail->{'line'} = $startline;
    $mail->{'eline'} = $startline + $mail->{'lines'} - 1;
    $mail->{'idx'} = $i;
    # ID is position in file and message ID
    $mail->{'id'} = $pos." ".$i." ".$startline." ".
        substr($mail->{'header'}->{'message-id'}, 0, 255);
    $rv[$i] = $mail;
    }
return @rv;
}

# select_mails(user|file, &ids, headersonly)
# Returns a list of messages from an mbox with the given IDs. The ID contains
# the file offset, message number, line and message ID, and the former is used
# if valid.
sub select_mails
{
local ($file, $ids, $headersonly) = @_;
local @rv;

local (@rv);
my %index;
local $gotindex;

local $umf = &user_mail_file($file);
local $dash = &dash_mode($umf);
open(MAIL, $umf);
foreach my $i (@$ids) {
    local ($pos, $idx, $startline, $wantmid) = split(/ /, $i);

    # Go to where the mail is supposed to be, and check if any starts there
    seek(MAIL, $pos, 0);
    local $ll = <MAIL>;
    local $fromok = $ll !~ /^From\s+(\S+).*\d+\r?\n/ ||
            ($1 eq '-' && !$dash) ? 0 : 1;
    print DEBUG "seeking to $pos in $umf, got $ll";
    if (!$fromok) {
        # Oh noes! Need to find it
        if (!$gotindex++) {
            &build_dbm_index($file, \%index);
            }
        $pos = undef;
        while(my ($k, $v) = each %index) {
            if (int($k) eq $k) {
                my ($p, $line, $subject, $from, $mid)=
                    split(/\0/, $v);
                if ($mid eq $wantmid) {
                    # Found it!
                    $pos = $p;
                    $idx = $k;
                    $startline = $line;
                    last;
                    }
                }
            }
        }

    if (defined($pos)) {
        # Now we can read
        seek(MAIL, $pos, 0);
        local $mail = &read_mail_fh(MAIL, $dash ? 2 : 1, $headersonly);
        $mail->{'line'} = $startline;
        $mail->{'eline'} = $startline + $mail->{'lines'} - 1;
        $mail->{'idx'} = $idx;
        $mail->{'id'} = "$pos $idx $startline $wantmid";
        push(@rv, $mail);
        }
    else {
        push(@rv, undef);    # Mail is gone?
        }
    }
close(MAIL);
return @rv;
}

# idlist_mails(user|file)
# Returns a list of IDs in some mbox
sub idlist_mails
{
my %index;
local $idlist = &build_dbm_index($_[0], \%index);
return @$idlist;
}

# search_mail(user, field, match)
# Returns an array of messages matching some search
sub search_mail
{
return &advanced_search_mail($_[0], [ [ $_[1], $_[2] ] ], 1);
}

# advanced_search_mail(user|file, &fields, andmode, [&limits], [headersonly])
# Returns an array of messages matching some search
sub advanced_search_mail
{
local (%index, @rv, $i);
local $dash = &dash_mode($_[0]);
local @possible;        # index positions of possible mails
local $possible_certain = 0;    # is possible list authoratative?
local ($min, $max);

# We have a DBM index .. if the search includes the from and subject
# fields, scan it first to cut down on the total time
&build_dbm_index($_[0], \%index);

# Check which fields are used in search
local @dbmfields = grep { $_->[0] eq 'from' ||
              $_->[0] eq 'subject' } @{$_[1]};
local $alldbm = (scalar(@dbmfields) == scalar(@{$_[1]}));

$min = 0;
$max = $index{'mailcount'}-1;
if ($_[3] && $_[3]->{'latest'}) {
    $min = $max - $_[3]->{'latest'};
    }

# Only check DBM if it contains some fields, and if it contains all
# fields when in 'or' mode.
if (@dbmfields && ($alldbm || $_[2])) {
    # Scan the DBM to build up a list of 'possibles'
    for($i=$min; $i<=$max; $i++) {
        local @idx = split(/\0/, $index{$i});
        local $fake = { 'header' => { 'from', $idx[2],
                          'subject', $idx[3] } };
        local $m = &mail_matches(\@dbmfields, $_[2], $fake);
        push(@possible, $i) if ($m);
        }
    $possible_certain = $alldbm;
    }
else {
    # None of the DBM fields are in the search .. have to scan all
    @possible = ($min .. $max);
    }

# Need to scan through possible messages to find those that match
open(MAIL, &user_mail_file($_[0]));
local $headersonly = !&matches_needs_body($_[1]);
foreach $i (@possible) {
    # Seek to mail position
    local @idx = split(/\0/, $index{$i});
    local $pos = $idx[0];
    local $startline = $idx[1];
    seek(MAIL, $pos, 0);

    # Read the mail
    local $mail = &read_mail_fh(MAIL, $dash ? 2 : 1, $headersonly);
    $mail->{'line'} = $startline;
    $mail->{'eline'} = $startline + $mail->{'lines'} - 1;
    $mail->{'idx'} = $i;
    $mail->{'id'} = $pos." ".$i." ".$startline." ".
            substr($mail->{'header'}->{'message-id'}, 0, 255);
    push(@rv, $mail) if ($possible_certain ||
                 &mail_matches($_[1], $_[2], $mail));
    }
return @rv;
}

# build_dbm_index(user|file, &index)
# Updates a reference to a DBM hash that indexes the given mail file.
# Hash contains keys 0, 1, 2 .. each of which has a value containing the
# position of the mail in the file, line number, subject, sender and message ID.
# Special key lastchange = time index was last updated
#          mailcount = number of messages in index
#          version = index format version
# Returns a list of all IDs
sub build_dbm_index
{
local $ifile = &user_index_file($_[0]);
local $umf = &user_mail_file($_[0]);
local @st = stat($umf);
local $index = $_[1];
dbmopen(%$index, $ifile, 0600);

# Read file of IDs
local $idsfile = $ifile.".ids";
local @ids;
local $idschanged;
if (open(IDSFILE, $idsfile)) {
    @ids = <IDSFILE>;
    chop(@ids);
    close(IDSFILE);
    }

if (scalar(@ids) != $index->{'mailcount'}) {
    # Build for first time
    print DEBUG "need meta-index rebuild for $_[0] ",scalar(@ids)," != ",$index->{'mailcount'},"\n";
    @ids = ( );
    while(my ($k, $v) = each %$index) {
        if ($k eq int($k) && $k < $index->{'mailcount'}) {
            local ($pos, $line, $subject, $sender, $mid) =
                split(/\0/, $v);
            $ids[$k] = $pos." ".$k." ".$line." ".$mid;
            }
        elsif ($k >= $index->{'mailcount'}) {
            # Old crap that is off the end
            delete($index->{$k});
            }
        }
    $index->{'mailcount'} = scalar(@ids);    # Now known for sure
    $idschanged = 1;
    }

if (!@st ||
    $index->{'lastchange'} < $st[9] ||
    $index->{'lastsize'} != $st[7] ||
    $st[7] < $dbm_index_min ||
    $index->{'version'} != $dbm_index_version) {
    # The mail file is newer than the index, or we are always re-indexing
    local $fromok = 1;
    local ($ll, @idx);
    local $dash = &dash_mode($umf);
    if ($st[7] < $dbm_index_min ||
        $index->{'version'} != $dbm_index_version) {
        $fromok = 0;    # Always re-index
        open(MAIL, $umf);
        }
    else {
        if (open(MAIL, $umf)) {
            # Check the last 100 messages (at most), to see if
            # the mail file has been truncated, had mails deleted,
            # or re-written.
            local $il = $index->{'mailcount'}-1;
            local $i;
            for($i=($il>100 ? 100 : $il); $i>=0; $i--) {
                @idx = split(/\0/, $index->{$il-$i});
                seek(MAIL, $idx[0], 0);
                $ll = <MAIL>;
                $fromok = 0 if ($ll !~ /^From\s+(\S+).*\d+\r?\n/ ||
                        ($1 eq '-' && !$dash));
                }
            }
        else {
            $fromok = 0;    # No mail file yet
            }
        }
    local ($pos, $lnum, $istart);
    if ($index->{'mailcount'} && $fromok && $st[7] > $idx[0]) {
        # Mail file seems to have gotten bigger, most likely
        # because new mail has arrived ... only reindex the new mails
        print DEBUG "re-indexing from $idx[0]\n";
        $pos = $idx[0] + length($ll);
        $lnum = $idx[1] + 1;
        $istart = $index->{'mailcount'};
        }
    else {
        # Mail file has changed in some other way ... do a rebuild
        # of the whole index
        print DEBUG "totally re-indexing\n";
        $istart = 0;
        $pos = 0;
        $lnum = 0;
        seek(MAIL, 0, 0);
        @ids = ( );
        $idschanged = 1;
        %$index = ( );
        }
    local ($doingheaders, @nidx);
    while(<MAIL>) {
        if (/^From\s+(\S+).*\d+\r?\n/ && ($1 ne '-' || $dash)) {
            @nidx = ( $pos, $lnum );
            $idschanged = 1;
            push(@ids, $pos." ".$istart." ".$lnum);
            $index->{$istart++} = join("\0", @nidx);
            $doingheaders = 1;
            }
        elsif ($_ eq "\n" || $_ eq "\r\n") {
            $doingheaders = 0;
            }
        elsif ($doingheaders && /^From:\s*(.{0,255})/i) {
            $nidx[2] = $1;
            $index->{$istart-1} = join("\0", @nidx);
            }
        elsif ($doingheaders && /^Subject:\s*(.{0,255})/i) {
            $nidx[3] = $1;
            $index->{$istart-1} = join("\0", @nidx);
            }
        elsif ($doingheaders && /^Message-ID:\s*(.{0,255})/i) {
            $nidx[4] = $1;
            $index->{$istart-1} = join("\0", @nidx);
            $ids[$#ids] .= " ".$1;
            }
        $pos += length($_);
        $lnum++;
        }
    close(MAIL);
    $index->{'lastchange'} = time();
    $index->{'lastsize'} = $st[7];
    $index->{'mailcount'} = $istart;
    $index->{'version'} = $dbm_index_version;
    }

# Write out IDs file, if needed
if ($idschanged) {
    open(IDSFILE, ">$idsfile");
    foreach my $id (@ids) {
        print IDSFILE $id,"\n";
        }
    close(IDSFILE);
    }

return \@ids;
}

# has_dbm_index(user|file)
# Returns 1 if a DBM index exists for some user or file
sub has_dbm_index
{
local $ifile = &user_index_file($_[0]);
foreach my $ext (".dir", ".pag", ".db") {
    return 1 if (-r $ifile.$ext);
    }
return 0;
}

# delete_dbm_index(user|file)
# Deletes all DBM indexes for a user or file
sub delete_dbm_index
{
local $ifile = &user_index_file($_[0]);
foreach my $ext (".dir", ".pag", ".db") {
    &unlink_file($ifile.$ext);
    }
}

# empty_mail(user|file)
# Truncate a mail file to nothing
sub empty_mail
{
local $umf = &user_mail_file($_[0]);
local $ifile = &user_index_file($_[0]);
open(TRUNC, ">$umf");
close(TRUNC);

# Set index size to 0
local %index;
dbmopen(%index, $ifile, 0600);
$index{'mailcount'} = 0;
$index{'lastchange'} = time();
dbmclose(%index);
}

# count_mail(user|file)
# Returns the number of messages in some mail file
sub count_mail
{
my %index;
&build_dbm_index($_[0], \%index);
return $index{'mailcount'};
}

# parse_mail(&mail, [&parent], [savebody], [keep-cr])
# Extracts the attachments from the mail body
sub parse_mail
{
return if ($_[0]->{'parsed'}++);
local $ct = $_[0]->{'header'}->{'content-type'};
local (@attach, $h, $a);
if ($ct =~ /multipart\/(\S+)/i && ($ct =~ /boundary="([^"]+)"/i ||
                   $ct =~ /boundary=([^;\s]+)/i)) {
    # Multipart MIME message
    local $bound = "--".$1;
    local @lines = $_[3] ? split(/\n/, $_[0]->{'body'})
                 : split(/\r?\n/, $_[0]->{'body'});
    local $l;
    local $max = @lines;
    while($l < $max && $lines[$l++] ne $bound) {
        # skip to first boundary
        }
    while(1) {
        # read attachment headers
        local (@headers, $attach);
        while($lines[$l]) {
            $attach->{'raw'} .= $lines[$l]."\n";
            $attach->{'rawheaders'} .= $lines[$l]."\n";
            if ($lines[$l] =~ /^(\S+):\s*(.*)/) {
                push(@headers, [ $1, $2 ]);
                }
            elsif ($lines[$l] =~ /^\s+(.*)/) {
                $headers[$#headers]->[1] .= " ".$1
                    unless($#headers < 0);
                }
            $l++;
            }
        $attach->{'raw'} .= $lines[$l]."\n";
        $l++;
        $attach->{'headers'} = \@headers;
        foreach $h (@headers) {
            $attach->{'header'}->{lc($h->[0])} = $h->[1];
            }
        if ($attach->{'header'}->{'content-type'} =~ /^([^;\s]+)/) {
            $attach->{'type'} = lc($1);
            }
        else {
            $attach->{'type'} = 'text/plain';
            }
        if ($attach->{'header'}->{'content-disposition'} =~
            /filename\s*=\s*"([^"]+)"/i) {
            $attach->{'filename'} = $1;
            }
        elsif ($attach->{'header'}->{'content-disposition'} =~
               /filename\s*=\s*([^;\s]+)/i) {
            $attach->{'filename'} = $1;
            }
        elsif ($attach->{'header'}->{'content-type'} =~
               /name\s*=\s*"([^"]+)"/i) {
            $attach->{'filename'} = $1;
            }
        elsif ($attach->{'header'}->{'content-type'} =~
               /name\s*=\s*([^;\s]+)/i) {
            $attach->{'filename'} = $1;
            }

        # read the attachment body
        while($l < $max && $lines[$l] ne $bound && $lines[$l] ne "$bound--") {
            $attach->{'data'} .= $lines[$l]."\n";
            $attach->{'raw'} .= $lines[$l]."\n";
            $l++;
            }
        $attach->{'data'} =~ s/\n\n$/\n/;    # Lose trailing blank line
        $attach->{'raw'} =~ s/\n\n$/\n/;

        # decode if necessary
        if (lc($attach->{'header'}->{'content-transfer-encoding'}) eq
            'base64') {
            # Standard base64 encoded attachment
            $attach->{'data'} = &b64decode($attach->{'data'});
            }
        elsif (lc($attach->{'header'}->{'content-transfer-encoding'}) eq
               'x-uue') {
            # UUencoded attachment
            $attach->{'data'} = &uudecode($attach->{'data'});
            }
        elsif (lc($attach->{'header'}->{'content-transfer-encoding'}) eq
               'quoted-printable') {
            # Quoted-printable text attachment
            $attach->{'data'} = &quoted_decode($attach->{'data'});
            }
        elsif (lc($attach->{'type'}) eq 'application/mac-binhex40' && &has_command("hexbin")) {
            # Macintosh binhex encoded attachment
            local $temp = &transname();
            mkdir($temp, 0700);
            open(HEXBIN, "| (cd $temp ; hexbin -n attach -d 2>/dev/null)");
            print HEXBIN $attach->{'data'};
            close(HEXBIN);
            if (!$?) {
                open(HEXBIN, "$temp/attach.data");
                local $/ = undef;
                $attach->{'data'} = <HEXBIN>;
                close(HEXBIN);
                local $ct = &guess_mime_type($attach->{'filename'});
                $attach->{'type'} = $ct;
                $attach->{'header'} = { 'content-type' => $ct };
                $attach->{'headers'} = [ [ 'Content-Type', $ct ] ];
                }
            unlink("$temp/attach.data");
            rmdir($temp);
            }

        $attach->{'idx'} = scalar(@attach);
        $attach->{'parent'} = $_[1] ? $_[1] : $_[0];
        push(@attach, $attach) if (@headers || $attach->{'data'});
        if ($attach->{'type'} =~ /multipart\/(\S+)/i) {
            # This attachment contains more attachments ..
            # expand them.
            local $amail = { 'header' => $attach->{'header'},
                     'body' => $attach->{'data'} };
            &parse_mail($amail, $attach, 0, $_[3]);
            $attach->{'attach'} = [ @{$amail->{'attach'}} ];
            map { $_->{'idx'} += scalar(@attach) }
                @{$amail->{'attach'}};
            push(@attach, @{$amail->{'attach'}});
            }
        elsif (lc($attach->{'type'}) eq 'application/ms-tnef') {
            # This attachment is a winmail.dat file, which may
            # contain multiple other attachments!
            local ($opentnef, $tnef);
            if (!($opentnef = &has_command("opentnef")) &&
                !($tnef = &has_command("tnef"))) {
                $attach->{'error'} = "tnef command not installed";
                }
            else {
                # Can actually decode
                local $tempfile = &transname();
                open(TEMPFILE, ">$tempfile");
                print TEMPFILE $attach->{'data'};
                close(TEMPFILE);
                local $tempdir = &transname();
                mkdir($tempdir, 0700);
                if ($opentnef) {
                    system("$opentnef -d $tempdir -i $tempfile >/dev/null 2>&1");
                    }
                else {
                    system("$tnef -C $tempdir -f $tempfile >/dev/null 2>&1");
                    }
                pop(@attach);    # lose winmail.dat
                opendir(DIR, $tempdir);
                while($f = readdir(DIR)) {
                    next if ($f eq '.' || $f eq '..');
                    local $data;
                    open(FILE, "$tempdir/$f");
                    while(<FILE>) {
                        $data .= $_;
                        }
                    close(FILE);
                    local $ct = &guess_mime_type($f);
                    push(@attach,
                      { 'type' => $ct,
                        'idx' => scalar(@attach),
                        'header' =>
                        { 'content-type' => $ct },
                        'headers' =>
                        [ [ 'Content-Type', $ct ] ],
                        'filename' => $f,
                        'data' => $data });
                    }
                closedir(DIR);
                unlink(glob("$tempdir/*"), $tempfile);
                rmdir($tempdir);
                }
            }
        last if ($l >= $max || $lines[$l] eq "$bound--");
        $l++;
        }
    $_[0]->{'attach'} = \@attach;
    }
elsif ($_[0]->{'body'} =~ /begin\s+([0-7]+)\s+(.*)/i) {
    # Message contains uuencoded file(s)
    local @lines = split(/\n/, $_[0]->{'body'});
    local ($attach, $rest);
    foreach $l (@lines) {
        if ($l =~ /^begin\s+([0-7]+)\s+(.*)/i) {
            $attach = { 'type' => &guess_mime_type($2),
                    'idx' => scalar(@{$_[0]->{'attach'}}),
                    'parent' => $_[1],
                    'filename' => $2 };
            push(@{$_[0]->{'attach'}}, $attach);
            }
        elsif ($l =~ /^end/ && $attach) {
            $attach = undef;
            }
        elsif ($attach) {
            $attach->{'data'} .= unpack("u", $l);
            }
        else {
            $rest .= $l."\n";
            }
        }
    if ($rest =~ /\S/) {
        # Some leftover text
        push(@{$_[0]->{'attach'}},
            { 'type' => "text/plain",
              'idx' => scalar(@{$_[0]->{'attach'}}),
              'parent' => $_[1],
              'data' => $rest });
        }
    }
elsif (lc($_[0]->{'header'}->{'content-transfer-encoding'}) eq 'base64') {
    # Signed body section
    $ct =~ s/;.*$//;
    $_[0]->{'attach'} = [ { 'type' => lc($ct),
                'idx' => 0,
                'parent' => $_[1],
                'data' => &b64decode($_[0]->{'body'}) } ];
    }
elsif (lc($_[0]->{'header'}->{'content-type'}) eq 'x-sun-attachment') {
    # Sun attachment format, which can contain several sections
    local $sun;
    foreach $sun (split(/----------/, $_[0]->{'body'})) {
        local ($headers, $rest) = split(/\r?\n\r?\n/, $sun, 2);
        local $attach = { 'idx' => scalar(@{$_[0]->{'attach'}}),
                  'parent' => $_[1],
                  'data' => $rest };
        if ($headers =~ /X-Sun-Data-Name:\s*(\S+)/) {
            $attach->{'filename'} = $1;
            }
        if ($headers =~ /X-Sun-Data-Type:\s*(\S+)/) {
            local $st = $1;
            $attach->{'type'} = $st eq "text" ? "text/plain" :
                        $st eq "html" ? "text/html" :
                        $st =~ /\// ? $st : "application/octet-stream";
            }
        elsif ($attach->{'filename'}) {
            $attach->{'type'} =
                &guess_mime_type($attach->{'filename'});
            }
        else {
            $attach->{'type'} = "text/plain";    # fallback
            }
        push(@{$_[0]->{'attach'}}, $attach);
        }
    }
else {
    # One big attachment (probably text)
    local ($type, $body);
    ($type = $ct) =~ s/;.*$//;
    $type = 'text/plain' if (!$type);
    if (lc($_[0]->{'header'}->{'content-transfer-encoding'}) eq 'base64') {
        $body = &b64decode($_[0]->{'body'});
        }
    elsif (lc($_[0]->{'header'}->{'content-transfer-encoding'}) eq 
           'quoted-printable') {
        $body = &quoted_decode($_[0]->{'body'});
        }
    else {
        $body = $_[0]->{'body'};
        }
    if ($body =~ /\S/) {
        $_[0]->{'attach'} = [ { 'type' => lc($type),
                    'idx' => 0,
                    'parent' => $_[1],
                    'data' => $body } ];
        }
    else {
        # Body is completely empty
        $_[0]->{'attach'} = [ ];
        }
    }
delete($_[0]->{'body'}) if (!$_[2]);
}

# delete_mail(user|file, &mail, ...)
# Delete mail messages from a user by copying the file and rebuilding the index
sub delete_mail
{
# Validate messages
local @m = sort { $a->{'line'} <=> $b->{'line'} } @_[1..@_-1];
foreach my $m (@m) {
    defined($m->{'line'}) && defined($m->{'eline'}) &&
      $m->{'eline'} > $m->{'line'} ||
        &error("Message to delete is invalid, perhaps to due to ".
           "out-of-date index");
    }

local $i = 0;
local $f = &user_mail_file($_[0]);
local $ifile = &user_index_file($_[0]);
local $lnum = 0;
local (%dline, @fline);
local ($dpos = 0, $dlnum = 0);
local (@index, %index);
&build_dbm_index($_[0], \%index);

local $tmpf = $< == 0 ? "$f.del" :
          $_[0] =~ /^\/.*\/([^\/]+)$/ ?
           "$user_module_config_directory/$1.del" :
          "$user_module_config_directory/$_[0].del";
if (-l $f) {
    $f = &resolve_links($f);
    }
open(SOURCE, $f) || &error("Read failed : $!");
open(DEST, ">$tmpf") || &error("Open of $tmpf failed : $!");
while(<SOURCE>) {
    if ($i >= @m || $lnum < $m[$i]->{'line'}) {
        # Within a range that we want to preserve
        $dpos += length($_);
        $dlnum++;
        local $w = (print DEST $_);
        if (!$w) {
            local $e = "$!";
            close(DEST);
            close(SOURCE);
            unlink($tmpf);
            &error("Write to $tmpf failed : $e");
            }
        }
    elsif (!$fline[$i]) {
        # Start line of a message to delete
        if (!/^From\s/) {
            # Not actually a message! Fail now
            close(DEST);
            close(SOURCE);
            unlink($tmpf);
            &error("Index on $f is corrupt - did not find expected message start at line $lnum");
            }
        $fline[$i] = 1;
        }
    elsif ($lnum == $m[$i]->{'eline'}) {
        # End line of the current message to delete
        $dline{$m[$i]->{'line'}}++;
        $i++;
        }
    $lnum++;
    }
close(SOURCE);
close(DEST) || &error("Write to $tmpf failed : $?");
local @st = stat($f);
unlink($f) if ($< == 0);

# Force a total index re-build (XXX lazy!)
$index{'mailcount'} = $in{'lastchange'} = 0;
dbmclose(%index);

if ($< == 0) {
    rename($tmpf, $f);
    }
else {
    system("cat ".quotemeta($tmpf)." > ".quotemeta($f).
           " && rm -f ".quotemeta($tmpf));
    }
chown($st[4], $st[5], $f);
chmod($st[2], $f);
}

# modify_mail(user|file, old, new, textonly)
# Modify one email message in a mailbox by copying the file and rebuilding
# the index.
sub modify_mail
{
local $f = &user_mail_file($_[0]);
local $ifile = &user_index_file($_[0]);
local $lnum = 0;
local ($sizediff, $linesdiff);
local %index;
&build_dbm_index($_[0], \%index);

# Replace the email that gets modified
local $tmpf = $< == 0 ? "$f.del" :
          $_[0] =~ /^\/.*\/([^\/]+)$/ ?
        "$user_module_config_directory/$1.del" :
          "$user_module_config_directory/$_[0].del";
if (-l $f) {
    $f = &resolve_links($f);
    }
open(SOURCE, $f);
open(DEST, ">$tmpf");
while(<SOURCE>) {
    if ($lnum < $_[1]->{'line'} || $lnum > $_[1]->{'eline'}) {
        # before or after the message to change
        local $w = (print DEST $_);
        if (!$w) {
            local $e = "$?";
            close(DEST);
            close(SOURCE);
            unlink($tmpf);
            &error("Write to $tmpf failed : $e");
            }
        }
    elsif ($lnum == $_[1]->{'line'}) {
        # found start of message to change .. put in the new one
        close(DEST);
        local @ost = stat($tmpf);
        local $nlines = &send_mail($_[2], $tmpf, $_[3], 1);
        local @nst = stat($tmpf);
        local $newsize = $nst[7] - $ost[7];
        $sizediff = $newsize - $_[1]->{'size'};
        $linesdiff = $nlines - ($_[1]->{'eline'} - $_[1]->{'line'} + 1);
        open(DEST, ">>$tmpf");
        }
    $lnum++;
    }
close(SOURCE);
close(DEST) || &error("Write failed : $!");

# Now update the index and delete the temp file
for($i=0; $i<$index{'mailcount'}; $i++) {
    local @idx = split(/\0/, $index{$i});
    if ($idx[1] > $_[1]->{'line'}) {
        $idx[0] += $sizediff;
        $idx[1] += $linesdiff;
        $index{$i} = join("\0", @idx);
        }
    }
$index{'lastchange'} = time();
local @st = stat($f);
unlink($f);
if ($< == 0) {
    rename($tmpf, $f);
    }
else {
    system("cat $tmpf >$f && rm -f $tmpf");
    }
chown($st[4], $st[5], $f);
chmod($st[2], $f);
}

# send_mail(&mail, [file], [textonly], [nocr], [smtp-server],
#        [smtp-user], [smtp-pass], [smtp-auth-mode],
#        [&notify-flags], [port])
# Send out some email message or append it to a file.
# Returns the number of lines written.
sub send_mail
{
return 0 if (&is_readonly_mode());
local (%header, $h);
local $lnum = 0;
local $sm = $_[4] || $config{'send_mode'};
local $eol = $_[3] || !$sm ? "\n" : "\r\n";
local $port = $_[9] || $config{'smtp_port'} || 25;
foreach $h (@{$_[0]->{'headers'}}) {
    $header{lc($h->[0])} = $h->[1];
    }

# Add the date header, always in english
&clear_time_locale();
local @tm = localtime(time());
push(@{$_[0]->{'headers'}},
     [ 'Date', strftime("%a, %d %b %Y %H:%M:%S %z (%Z)", @tm) ])
    if (!$header{'date'});
&reset_time_locale();

# Build list of destination email addresses
my @dests;
foreach my $f ("to", "cc", "bcc") {
    if ($header{$f}) {
        push(@dests, &address_parts($header{$f}));
        }
    }
my $qdests = join(" ", map { quotemeta($_) } @dests);

local @from = &address_parts($header{'from'});
local $fromaddr;
if (@from && $from[0] =~ /\S/) {
    $fromaddr = $from[0];
    }
else {
    local @uinfo = getpwuid($<);
    $fromaddr = $uinfo[0] || "nobody";
    $fromaddr .= '@'.&get_system_hostname();
    }
local $esmtp = $_[8] ? 1 : 0;
if ($_[1]) {
    # Just append the email to a file using mbox format
    open(MAIL, ">>$_[1]") || &error("Write failed : $!");
    $lnum++;
    print MAIL $_[0]->{'fromline'} ? $_[0]->{'fromline'}.$eol :
                     &make_from_line($fromaddr).$eol;
    }
elsif ($sm) {
    # Connect to SMTP server
    &open_socket($sm, $port, MAIL);
    &smtp_command(MAIL);
    if ($esmtp) {
        &smtp_command(MAIL, "ehlo ".&get_system_hostname()."\r\n");
        }
    else {
        &smtp_command(MAIL, "helo ".&get_system_hostname()."\r\n");
        }

    # Get username and password from parameters, or from module config
    local $user = $_[5] || $userconfig{'smtp_user'} || $config{'smtp_user'};
    local $pass = $_[6] || $userconfig{'smtp_pass'} || $config{'smtp_pass'};
    local $auth = $_[7] || $userconfig{'smtp_auth'} ||
              $config{'smtp_auth'} || "Cram-MD5";
    if ($user) {
        # Send authentication commands
        eval "use Authen::SASL";
        if ($@) {
            &error("Perl module <tt>Authen::SASL</tt> is needed for SMTP authentication");
            }
        my $sasl = Authen::SASL->new('mechanism' => uc($auth),
                         'callback' => {
                        'auth' => $user,
                        'user' => $user,
                        'pass' => $pass } );
        &error("Failed to create Authen::SASL object") if (!$sasl);
        local $conn = $sasl->client_new("smtp", &get_system_hostname());
        local $arv = &smtp_command(MAIL, "auth $auth\r\n", 1);
        if ($arv =~ /^(334)\s+(.*)/) {
            # Server says to go ahead
            $extra = $2;
            local $initial = $conn->client_start();
            local $auth_ok;
            if ($initial) {
                local $enc = &encode_base64($initial);
                $enc =~ s/\r|\n//g;
                $arv = &smtp_command(MAIL, "$enc\r\n", 1);
                if ($arv =~ /^(\d+)\s+(.*)/) {
                    if ($1 == 235) {
                        $auth_ok = 1;
                        }
                    else {
                        &error("Unknown SMTP authentication response : $arv");
                        }
                    }
                $extra = $2;
                }
            while(!$auth_ok) {
                local $message = &decode_base64($extra);
                local $return = $conn->client_step($message);
                local $enc = &encode_base64($return);
                $enc =~ s/\r|\n//g;
                $arv = &smtp_command(MAIL, "$enc\r\n", 1);
                if ($arv =~ /^(\d+)\s+(.*)/) {
                    if ($1 == 235) {
                        $auth_ok = 1;
                        }
                    elsif ($1 == 535) {
                        &error("SMTP authentication failed : $arv");
                        }
                    $extra = $2;
                    }
                else {
                    &error("Unknown SMTP authentication response : $arv");
                    }
                }
            }
        }

    &smtp_command(MAIL, "mail from: <$fromaddr>\r\n");
    local $notify = $_[8] ? " NOTIFY=".join(",", @{$_[8]}) : "";
    foreach my $u (@dests) {
        &smtp_command(MAIL, "rcpt to: <$u>$notify\r\n");
        }
    &smtp_command(MAIL, "data\r\n");
    }
elsif (defined(&send_mail_program)) {
    # Use specified mail injector
    local $cmd = &send_mail_program($fromaddr, \@dests);
    $cmd || &error("No mail program was found on your system!");
    open(MAIL, "| $cmd >/dev/null 2>&1");
    }
elsif ($config{'qmail_dir'}) {
    # Start qmail-inject
    open(MAIL, "| $config{'qmail_dir'}/bin/qmail-inject");
    }
elsif ($config{'postfix_control_command'}) {
    # Start postfix's sendmail wrapper
    local $cmd = -x "/usr/lib/sendmail" ? "/usr/lib/sendmail" :
            &has_command("sendmail");
    $cmd || &error($text{'send_ewrapper'});
    open(MAIL, "| $cmd -f$fromaddr $qdests >/dev/null 2>&1");
    }
else {
    # Start sendmail
    &has_command($config{'sendmail_path'}) ||
        &error(&text('send_epath', "<tt>$config{'sendmail_path'}</tt>"));
    open(MAIL, "| $config{'sendmail_path'} -f$fromaddr $qdests >/dev/null 2>&1");
    }
local $ctype = "multipart/mixed";
local $msg_id;
foreach $h (@{$_[0]->{'headers'}}) {
    if (defined($_[0]->{'body'}) || $_[2]) {
        print MAIL $h->[0],": ",$h->[1],$eol;
        $lnum++;
        }
    else {
        if ($h->[0] !~ /^(MIME-Version|Content-Type)$/i) {
            print MAIL $h->[0],": ",$h->[1],$eol;
            $lnum++;
            }
        elsif (lc($h->[0]) eq 'content-type') {
            $ctype = $h->[1];
            }
        }
    if (lc($h->[0]) eq 'message-id') {
        $msg_id++;
        }
    }
if (!$msg_id) {
    # Add a message-id header if missing
    $main::mailboxes_message_id_count++;
    print MAIL "Message-Id: <",time().".".$$.".".
                $main::mailboxes_message_id_count."\@".
                &get_system_hostname(),">",$eol;
    }

# Work out first attachment content type
local ($ftype, $fenc);
if (@{$_[0]->{'attach'}} >= 1) {
    local $first = $_[0]->{'attach'}->[0];
    $ftype = "text/plain";
    foreach my $h (@{$first->{'headers'}}) {
        if (lc($h->[0]) eq "content-type") {
            $ftype = $h->[1];
            }
        if (lc($h->[0]) eq "content-transfer-encoding") {
            $fenc = $h->[1];
            }
        }
    }

if (defined($_[0]->{'body'})) {
    # Use original mail body
    print MAIL $eol;
    $lnum++;
    $_[0]->{'body'} =~ s/\r//g;
    $_[0]->{'body'} =~ s/\n\.\n/\n\. \n/g;
    $_[0]->{'body'} =~ s/\n/$eol/g;
    $_[0]->{'body'} .= $eol if ($_[0]->{'body'} !~ /\n$/);
    (print MAIL $_[0]->{'body'}) || &error("Write failed : $!");
    $lnum += ($_[0]->{'body'} =~ tr/\n/\n/);
    }
elsif (!@{$_[0]->{'attach'}}) {
    # No content, so just send empty email
    print MAIL "Content-Type: text/plain",$eol;
    print MAIL $eol;
    $lnum += 2;
    }
elsif (!$_[2] || $ftype !~ /text\/plain/i ||
       $fenc =~ /quoted-printable|base64/) {
    # Sending MIME-encoded email
    if ($ctype !~ /multipart\/report/i) {
        $ctype =~ s/;.*$//;
        }
    print MAIL "MIME-Version: 1.0",$eol;
    local $bound = "bound".time();
    print MAIL "Content-Type: $ctype; boundary=\"$bound\"",$eol;
    print MAIL $eol;
    $lnum += 3;

    # Send attachments
    print MAIL "This is a multi-part message in MIME format.",$eol;
    $lnum++;
    foreach $a (@{$_[0]->{'attach'}}) {
        print MAIL $eol;
        print MAIL "--",$bound,$eol;
        $lnum += 2;
        local $enc;
        foreach $h (@{$a->{'headers'}}) {
            print MAIL $h->[0],": ",$h->[1],$eol;
            $enc = $h->[1]
                if (lc($h->[0]) eq 'content-transfer-encoding');
            $lnum++;
            }
        print MAIL $eol;
        $lnum++;
        if (lc($enc) eq 'base64') {
            local $enc = &encode_base64($a->{'data'});
            $enc =~ s/\r//g;
            $enc =~ s/\n/$eol/g;
            print MAIL $enc;
            $lnum += ($enc =~ tr/\n/\n/);
            }
        else {
            $a->{'data'} =~ s/\r//g;
            $a->{'data'} =~ s/\n\.\n/\n\. \n/g;
            $a->{'data'} =~ s/\n/$eol/g;
            print MAIL $a->{'data'};
            $lnum += ($a->{'data'} =~ tr/\n/\n/);
            if ($a->{'data'} !~ /\n$/) {
                print MAIL $eol;
                $lnum++;
                }
            }
        }
    print MAIL $eol;
    (print MAIL "--",$bound,"--",$eol) || &error("Write failed : $!");
    print MAIL $eol;
    $lnum += 3;
    }
else {
    # Sending text-only mail from first attachment
    local $a = $_[0]->{'attach'}->[0];
    print MAIL $eol;
    $lnum++;
    $a->{'data'} =~ s/\r//g;
    $a->{'data'} =~ s/\n/$eol/g;
    (print MAIL $a->{'data'}) || &error("Write failed : $!");
    $lnum += ($a->{'data'} =~ tr/\n/\n/);
    if ($a->{'data'} !~ /\n$/) {
        print MAIL $eol;
        $lnum++;
        }
    }
if ($sm && !$_[1]) {
    &smtp_command(MAIL, ".$eol");
    &smtp_command(MAIL, "quit$eol");
    }
if (!close(MAIL)) {
    # Only bother to report an error on close if writing to a file
    if ($_[1]) {
        &error("Write failed : $!");
        }
    }
return $lnum;
}

# unparse_mail(&attachments, eol, boundary)
# Convert an array of attachments into MIME format, and return them as an
# array of lines.
sub unparse_mail
{
local ($attach, $eol, $bound) = @_;
local @rv;
foreach my $a (@$attach) {
    push(@rv, $eol);
    push(@rv, "--".$bound.$eol);
    local $enc;
    foreach my $h (@{$a->{'headers'}}) {
        push(@rv, $h->[0].": ".$h->[1].$eol);
        $enc = $h->[1]
            if (lc($h->[0]) eq 'content-transfer-encoding');
        }
    push(@rv, $eol);
    if (lc($enc) eq 'base64') {
        local $enc = &encode_base64($a->{'data'});
        $enc =~ s/\r//g;
        foreach my $l (split(/\n/, $enc)) {
            push(@rv, $l.$eol);
            }
        }
    else {
        $a->{'data'} =~ s/\r//g;
        $a->{'data'} =~ s/\n\.\n/\n\. \n/g;
        foreach my $l (split(/\n/, $a->{'data'})) {
            push(@rv, $l.$eol);
            }
        }
    }
push(@rv, $eol);
push(@rv, "--".$bound."--".$eol);
push(@rv, $eol);
return @rv;
}

# mail_size(&mail, [textonly])
# Returns the size of an email message in bytes
sub mail_size
{
local ($mail, $textonly) = @_;
local $temp = &transname();
&send_mail($mail, $temp, $textonly);
local @st = stat($temp);
unlink($temp);
return $st[7];
}

# b64decode(string)
# Converts a string from base64 format to normal
sub b64decode
{
    local($str) = $_[0];
    local($res);
    $str =~ tr|A-Za-z0-9+=/||cd;
    $str =~ s/=+$//;
    $str =~ tr|A-Za-z0-9+/| -_|;
    while ($str =~ /(.{1,60})/gs) {
        my $len = chr(32 + length($1)*3/4);
        $res .= unpack("u", $len . $1 );
    }
    return $res;
}

# can_read_mail(user)
sub can_read_mail
{
return 1 if ($_[0] && $access{'sent'} eq $_[0]);
local @u = getpwnam($_[0]);
return 0 if (!@u);
return 0 if ($_[0] =~ /\.\./);
return 0 if ($access{'mmode'} == 0);
return 1 if ($access{'mmode'} == 1);
local $u;
if ($access{'mmode'} == 2) {
    foreach $u (split(/\s+/, $access{'musers'})) {
        return 1 if ($u eq $_[0]);
        }
    return 0;
    }
elsif ($access{'mmode'} == 4) {
    return 1 if ($_[0] eq $remote_user);
    }
elsif ($access{'mmode'} == 5) {
    return $u[3] eq $access{'musers'};
    }
elsif ($access{'mmode'} == 3) {
    foreach $u (split(/\s+/, $access{'musers'})) {
        return 0 if ($u eq $_[0]);
        }
    return 1;
    }
elsif ($access{'mmode'} == 6) {
    return ($_[0] =~ /^$access{'musers'}$/);
    }
elsif ($access{'mmode'} == 7) {
    return (!$access{'musers'} || $u[2] >= $access{'musers'}) &&
           (!$access{'musers2'} || $u[2] <= $access{'musers2'});
    }
return 0;    # can't happen!
}

# from_hostname()
sub from_hostname
{
local ($d, $masq);
local $conf = &get_sendmailcf();
foreach $d (&find_type("D", $conf)) {
    if ($d->{'value'} =~ /^M\s*(\S*)/) { $masq = $1; }
    }
return $masq ? $masq : &get_system_hostname();
}

# mail_from_queue(qfile, [dfile|"auto"])
# Reads a message from the Sendmail mail queue
sub mail_from_queue
{
local $mail = { 'file' => $_[0] };
$mail->{'quar'} = $_[0] =~ /\/hf/;
$mail->{'lost'} = $_[0] =~ /\/Qf/;
if ($_[1] eq "auto") {
    $mail->{'dfile'} = $_[0];
    $mail->{'dfile'} =~ s/\/(qf|hf|Qf)/\/df/;
    }
elsif ($_[1]) {
    $mail->{'dfile'} = $_[1];
    }
$mail->{'lfile'} = $_[0];
$mail->{'lfile'} =~ s/\/(qf|hf|Qf)/\/xf/;
local $_;
local @headers;
open(QF, $_[0]) || return undef;
while(<QF>) {
    s/\r|\n//g;
    if (/^M(.*)/) {
        $mail->{'status'} = $1;
        }
    elsif (/^H\?[^\?]*\?(\S+):\s+(.*)/ || /^H(\S+):\s+(.*)/) {
        push(@headers, [ $1, $2 ]);
        $mail->{'rawheaders'} .= "$1: $2\n";
        }
    elsif (/^\s+(.*)/) {
        $headers[$#headers]->[1] .= $1 unless($#headers < 0);
        $mail->{'rawheaders'} .= $_."\n";
        }
    }
close(QF);
$mail->{'headers'} = \@headers;
foreach $h (@headers) {
    $mail->{'header'}->{lc($h->[0])} = $h->[1];
    }

if ($mail->{'dfile'}) {
    # Read the mail body
    open(DF, $mail->{'dfile'});
    while(<DF>) {
        $mail->{'body'} .= $_;
        }
    close(DF);
    }
local $datafile = $mail->{'dfile'};
if (!$datafile) {
    ($datafile = $mail->{'file'}) =~ s/\/(qf|hf|Qf)/\/df/;
    }
local @st0 = stat($mail->{'file'});
local @st1 = stat($datafile);
$mail->{'size'} = $st0[7] + $st1[7];
return $mail;
}

# wrap_lines(text, width)
# Given a multi-line string, return an array of lines wrapped to
# the given width
sub wrap_lines
{
local @rv;
local $w = $_[1];
foreach $rest (split(/\n/, $_[0])) {
    if ($rest =~ /\S/) {
        while($rest =~ /^(.{1,$w}\S*)\s*([\0-\377]*)$/) {
            push(@rv, $1);
            $rest = $2;
            }
        }
    else {
        # Empty line .. keep as it is
        push(@rv, $rest);
        }
    }
return @rv;
}

# smtp_command(handle, command, no-error)
sub smtp_command
{
local ($m, $c) = @_;
print $m $c;
local $r = <$m>;
if ($r !~ /^[23]\d+/ && !$_[2]) {
    &error(&text('send_esmtp', "<tt>".&html_escape($c)."</tt>",
                   "<tt>".&html_escape($r)."</tt>"));
    }
$r =~ s/\r|\n//g;
if ($r =~ /^(\d+)\-/) {
    # multi-line ESMTP response!
    while(1) {
        local $nr = <$m>;
        $nr =~ s/\r|\n//g;
        if ($nr =~ /^(\d+)\-(.*)/) {
            $r .= "\n".$2;
            }
        elsif ($nr =~ /^(\d+)\s+(.*)/) {
            $r .= "\n".$2;
            last;
            }
        }
    }
return $r;
}

# address_parts(string)
# Returns the email addresses in a string
sub address_parts
{
local @rv = map { $_->[0] } &split_addresses($_[0]);
return wantarray ? @rv : $rv[0];
}

# link_urls(text, separate)
# Converts URLs into HTML links
sub link_urls
{
local $r = $_[0];
local $tar = $_[1] ? "target=_blank" : "";
$r =~ s/((http|ftp|https|mailto):[^><"'\s]+[^><"'\s\.\)])/<a href="$1" $tar>$1<\/a>/g;
return $r;
}

# link_urls_and_escape(text, separate)
# HTML escapes some text, as well as properly linking URLs in it
sub link_urls_and_escape
{
local $l = $_[0];
local $rv;
local $tar = $_[1] ? " target=_blank" : "";
while($l =~ /^(.*?)((http|ftp|https|mailto):[^><"'\s]+[^><"'\s\.\)])(.*)/) {
    local ($before, $url, $after) = ($1, $2, $4);
    $rv .= &eucconv_and_escape($before)."<a href='$url' $tar>".
           &html_escape($url)."</a>";
    $l = $after;
    }
$rv .= &eucconv_and_escape($l);
return $rv;
}

# links_urls_new_target(html)
# Converts any links without targets to open in a new window
sub links_urls_new_target
{
local $l = $_[0];
local $rv;
while($l =~ s/^([\0-\377]*?)<\s*a\s+([^>]*href[^>]*)>//i) {
    local ($before, $a) = ($1, $2);
    if ($a !~ /target\s*=/i) {
        $a .= " target=_blank";
        }
    $rv .= $before."<a ".$a.">";
    }
$rv .= $l;
return $rv;
}

# uudecode(text)
sub uudecode
{
local @lines = split(/\n/, $_[0]);
local ($l, $data);
for($l=0; $lines[$l] !~ /begin\s+([0-7]+)\s/i; $l++) { }
while($lines[++$l]) {
    $data .= unpack("u", $lines[$l]);
    }
return $data;
}

# simplify_date(datestring, [format])
# Given a date from an email header, convert to the user's preferred format
sub simplify_date
{
local ($date, $fmt) = @_;
local $u = &parse_mail_date($date);
if ($u) {
    $fmt ||= $userconfig{'date_fmt'} || $config{'date_fmt'} || "dmy";
    local $strf = $fmt eq "dmy" ? "%d/%m/%Y" :
              $fmt eq "mdy" ? "%m/%d/%Y" :
                      "%Y/%m/%d";
    return strftime("$strf %H:%M", localtime($u));
        }
elsif ($date =~ /^(\S+),\s+0*(\d+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+)/) {
    return "$2/$3/$4 $5:$6";
    }
elsif ($date =~ /^0*(\d+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+)/) {
    return "$1/$2/$3 $4:$5";
    }
return $date;
}

# simplify_from(from)
# Simplifies a From: address for display in the mail list. Only the first
# address is returned.
sub simplify_from
{
local $rv = &eucconv(&decode_mimewords($_[0]));
local @sp = &split_addresses($rv);
if (!@sp) {
    return $text{'mail_nonefrom'};
    }
else {
    local $first = &html_escape($sp[0]->[1] ? $sp[0]->[1] : $sp[0]->[2]);
    if (length($first) > 80) {
        return substr($first, 0, 80)." ..";
        }
    else {
        return $first.(@sp > 1 ? " , ..." : "");
        }
    }
}

# simplify_subject(subject)
# Simplifies and truncates a subject for display in the mail list
sub simplify_subject
{
local $rv = &eucconv(&decode_mimewords($_[0]));
$rv = substr($rv, 0, 80)." .." if (length($rv) > 80);
return &html_escape($rv);
}

# quoted_decode(text)
# Converts quoted-printable format to the original
sub quoted_decode
{
local $t = $_[0];
$t =~ s/[ \t]+?(\r?\n)/$1/g;
$t =~ s/=\r?\n//g;
$t =~ s/(^|[^\r])\n\Z/$1\r\n/;
$t =~ s/=([a-fA-F0-9]{2})/pack("c",hex($1))/ge;
return $t;
}

# quoted_encode(text)
# Encodes text to quoted-printable format
sub quoted_encode
{
local $t = $_[0];
$t =~ s/([=\177-\377])/sprintf("=%2.2X",ord($1))/ge;
return $t;
}

# decode_mimewords(string)
# Converts a string in MIME words format like
# =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= to actual 8-bit characters
sub decode_mimewords {
    my $encstr = shift;
    my %params = @_;
    my @tokens;
    $@ = '';           ### error-return

    ### Collapse boundaries between adjacent encoded words:
    $encstr =~ s{(\?\=)\r?\n[ \t](\=\?)}{$1$2}gs;
    pos($encstr) = 0;
    ### print STDOUT "ENC = [", $encstr, "]\n";

    ### Decode:
    my ($charset, $encoding, $enc, $dec);
    while (1) {
    last if (pos($encstr) >= length($encstr));
    my $pos = pos($encstr);               ### save it

    ### Case 1: are we looking at "=?..?..?="?
    if ($encstr =~    m{\G             # from where we left off..
                =\?([^?]*)     # "=?" + charset +
                 \?([bq])      #  "?" + encoding +
                 \?([^?]+)     #  "?" + data maybe with spcs +
                 \?=           #  "?="
                }xgi) {
        ($charset, $encoding, $enc) = ($1, lc($2), $3);
        $dec = (($encoding eq 'q') ? _decode_Q($enc) : _decode_B($enc));
        push @tokens, [$dec, $charset];
        next;
    }

    ### Case 2: are we looking at a bad "=?..." prefix? 
    ### We need this to detect problems for case 3, which stops at "=?":
    pos($encstr) = $pos;               # reset the pointer.
    if ($encstr =~ m{\G=\?}xg) {
        $@ .= qq|unterminated "=?..?..?=" in "$encstr" (pos $pos)\n|;
        push @tokens, ['=?'];
        next;
    }

    ### Case 3: are we looking at ordinary text?
    pos($encstr) = $pos;               # reset the pointer.
    if ($encstr =~ m{\G                # from where we left off...
             ([\x00-\xFF]*?    #   shortest possible string,
              \n*)             #   followed by 0 or more NLs,
                 (?=(\Z|=\?))      # terminated by "=?" or EOS
            }xg) {
        length($1) or die "MIME::Words: internal logic err: empty token\n";
        push @tokens, [$1];
        next;
    }

    ### Case 4: bug!
    die "MIME::Words: unexpected case:\n($encstr) pos $pos\n\t".
        "Please alert developer.\n";
    }
    return join('',map {$_->[0]} @tokens);
}

# _decode_Q STRING
#     Private: used by _decode_header() to decode "Q" encoding, which is
#     almost, but not exactly, quoted-printable.  :-P
sub _decode_Q {
    my $str = shift;
    $str =~ s/_/\x20/g;                                # RFC-1522, Q rule 2
    $str =~ s/=([\da-fA-F]{2})/pack("C", hex($1))/ge;  # RFC-1522, Q rule 1
    $str;
}

# _decode_B STRING
#     Private: used by _decode_header() to decode "B" encoding.
sub _decode_B {
    my $str = shift;
    &decode_base64($str);
}

# encode_mimewords(string, %params)
# Converts a word with 8-bit characters to MIME words format
sub encode_mimewords
{
my ($rawstr, %params) = @_;
my $charset  = $params{Charset} || 'ISO-8859-1';
my $defenc = uc($charset) eq 'ISO-2022-JP' ? 'b' : 'q';
my $encoding = lc($params{Encoding} || $defenc);
my $NONPRINT = "\\x00-\\x1F\\x7F-\\xFF";

if ($rawstr =~ /^[\x20-\x7E]*$/) {
    # No encoding needed
    return $rawstr;
    }

### Encode any "words" with unsafe characters.
###    We limit such words to 18 characters, to guarantee that the
###    worst-case encoding give us no more than 54 + ~10 < 75 characters
my $word;
$rawstr =~ s{([ a-zA-Z0-9\x7F-\xFF]{1,18})}{     ### get next "word"
    $word = $1;
    $word =~ /(?:[$NONPRINT])|(?:^\s+$)/o ?
    encode_mimeword($word, $encoding, $charset) :    # unsafe chars
    $word                        # OK word
}xeg;
$rawstr =~ s/\?==\?/?= =?/g;
return $rawstr;
}

# encode_mimewords_address(string, %params)
# Given a string containing addresses into one with real names mime-words
# escaped
sub encode_mimewords_address
{
my ($rawstr, %params) = @_;
my $charset  = $params{Charset} || 'ISO-8859-1';
my $defenc = uc($charset) eq 'ISO-2022-JP' ? 'b' : 'q';
my $encoding = lc($params{Encoding} || $defenc);
if ($rawstr =~ /^[\x20-\x7E]*$/) {
    # No encoding needed
    return $rawstr;
    }
my @rv;
foreach my $addr (&split_addresses($rawstr)) {
    my ($email, $name, $orig) = @$addr;
    if ($name =~ /^[\x20-\x7E]*$/) {
        # No encoding needed
        push(@rv, $orig);
        }
    else {
        # Re-encode name
        my $ename = encode_mimeword($name, $encoding, $charset);
        push(@rv, $ename." <".$email.">");
        }
    }
return join(", ", @rv);
}

# encode_mimeword(string, [encoding], [charset])
# Converts a word with 8-bit characters to MIME words format
sub encode_mimeword
{
my $word = shift;
my $encoding = uc(shift || 'Q');
my $charset  = uc(shift || 'ISO-8859-1');
my $encfunc  = (($encoding eq 'Q') ? \&_encode_Q : \&_encode_B);
return "=?$charset?$encoding?" . &$encfunc($word) . "?=";
}

# _encode_Q STRING
#     Private: used by _encode_header() to decode "Q" encoding, which is
#     almost, but not exactly, quoted-printable.  :-P
sub _encode_Q {
    my $str = shift;
    my $NONPRINT = "\\x00-\\x1F\\x7F-\\xFF";
    $str =~ s{([ _\?\=$NONPRINT])}{sprintf("=%02X", ord($1))}eog;
    return $str;
}

# _encode_B STRING
#     Private: used by _decode_header() to decode "B" encoding.
sub _encode_B {
    my $str = shift;
    my $enc = &encode_base64($str);
    $enc =~ s/\n//;
    return $enc;
}

# user_mail_file(user|file, [other details])
sub user_mail_file
{
if ($_[0] =~ /^\//) {
    return $_[0];
    }
elsif ($config{'mail_dir'}) {
    return &mail_file_style($_[0], $config{'mail_dir'},
                $config{'mail_style'});
    }
elsif (@_ > 1) {
    return "$_[7]/$config{'mail_file'}";
    }
else {
    local @u = getpwnam($_[0]);
    return "$u[7]/$config{'mail_file'}";
    }
}

# mail_file_style(user, basedir, style)
sub mail_file_style
{
if ($_[2] == 0) {
    return "$_[1]/$_[0]";
    }
elsif ($_[2] == 1) {
    return $_[1]."/".substr($_[0], 0, 1)."/".$_[0];
    }
elsif ($_[2] == 2) {
    return $_[1]."/".substr($_[0], 0, 1)."/".
        substr($_[0], 0, 2)."/".$_[0];
    }
else {
    return $_[1]."/".substr($_[0], 0, 1)."/".
        substr($_[0], 1, 1)."/".$_[0];
    }
}

# user_index_file(user|file)
sub user_index_file
{
local $us = $_[0];
$us =~ s/\//_/g;
local $f;
local $hn = &get_system_hostname();
if ($_[0] =~ /^\/.*\/([^\/]+)$/) {
    # A file .. the index file is in ~/.usermin/mailbox or
    # /etc/webmin/mailboxes
    if ($user_module_config_directory && $config{'shortindex'}) {
        # Use short name for index file
        $f = "$user_module_config_directory/$1.findex";
        }
    else {
        $f = $user_module_config_directory ?
            "$user_module_config_directory/$us.findex" :
            "$module_config_directory/$us.findex";
        }
    }
else {
    # A username .. the index file is in /etc/webmin/mailboxes
        $f = $user_module_config_directory ?
        "$user_module_config_directory/$_[0].index" :
        "$module_config_directory/$_[0].index";
    }
# Append hostname if requested, unless an index file without the hostname
# already exists
return $config{'noindex_hostname'} ? $f :
       -r $f && !-r "$f.$hn" ? $f : "$f.$hn";
}

# extract_mail(data)
# Converts the text of a message into mail object.
sub extract_mail
{
local $text = $_[0];
$text =~ s/^\s+//;
local ($amail, @aheaders, $i);
local @alines = split(/\n/, $text);
while($i < @alines && $alines[$i]) {
    if ($alines[$i] =~ /^(\S+):\s*(.*)/) {
        push(@aheaders, [ $1, $2 ]);
        $amail->{'rawheaders'} .= $alines[$i]."\n";
        }
    elsif ($alines[$i] =~ /^\s+(.*)/) {
        $aheaders[$#aheaders]->[1] .= $1 unless($#aheaders < 0);
        $amail->{'rawheaders'} .= $alines[$i]."\n";
        }
    $i++;
    }
$amail->{'headers'} = \@aheaders;
foreach $h (@aheaders) {
    $amail->{'header'}->{lc($h->[0])} = $h->[1];
    }
splice(@alines, 0, $i);
$amail->{'body'} = join("\n", @alines)."\n";
return $amail;
}

# split_addresses(string)
# Splits a comma-separated list of addresses into [ email, real-name, original ]
# triplets
sub split_addresses
{
local (@rv, $str = $_[0]);
while(1) {
    $str =~ s/\\"/\0/g;
    if ($str =~ /^[\s,]*(([^<>\(\)\s"]+)\s+\(([^\(\)]+)\))(.*)$/) {
        # An address like  foo@bar.com (Fooey Bar)
        push(@rv, [ $2, $3, $1 ]);
        $str = $4;
        }
    elsif ($str =~ /^[\s,]*("([^"]+)"\s*<([^\s<>,]+)>)(.*)$/ ||
           $str =~ /^[\s,]*(([^<>\@]+)\s+<([^\s<>,]+)>)(.*)$/ ||
           $str =~ /^[\s,]*(([^<>\@]+)<([^\s<>,]+)>)(.*)$/ ||
           $str =~ /^[\s,]*(([^<>\[\]]+)\s+\[mailto:([^\s\[\]]+)\])(.*)$/||
           $str =~ /^[\s,]*(()<([^<>,]+)>)(.*)/ ||
           $str =~ /^[\s,]*(()([^\s<>,]+))(.*)/) {
        # Addresses like  "Fooey Bar" <foo@bar.com>
        #                 Fooey Bar <foo@bar.com>
        #                 Fooey Bar<foo@bar.com>
        #          Fooey Bar [mailto:foo@bar.com]
        #          <foo@bar.com>
        #          <group name>
        #          foo@bar.com or foo
        my ($all, $name, $email, $rest) = ($1, $2, $3, $4);
        $all =~ s/\0/\\"/g;
        $name =~ s/\0/"/g;
        push(@rv, [ $email, $name eq "," ? "" : $name, $all ]);
        $str = $rest;
        }
    else {
        last;
        }
    }
return @rv;
}

$match_ascii = '\x1b\([BHJ]([\t\x20-\x7e]*)';
$match_jis = '\x1b\$[@B](([\x21-\x7e]{2})*)';

sub eucconv {
    local($_) = @_;
    if ($current_lang eq 'ja_JP.euc') {
        s/$match_jis/&j2e($1)/geo;
        s/$match_ascii/$1/go;
        }
    $_;
}

sub j2e {
    local($_) = @_;
    tr/\x21-\x7e/\xa1-\xfe/;
    $_;
}

# eucconv_and_escape(string)
sub eucconv_and_escape {
    return &html_escape(&eucconv($_[0]));
}

# list_maildir(file, [start], [end], [headersonly])
# Returns a subset of mail from a maildir format directory
sub list_maildir
{
local (@rv, $i, $f);
&mark_read_maildir($_[0]);
local @files = &get_maildir_files($_[0]);

local ($start, $end);
if (!defined($_[1])) {
    $start = 0;
    $end = @files - 1;
    }
elsif ($_[2] < 0) {
    $start = @files + $_[2] - 1;
    $end = @files + $_[1] - 1;
    $start = 0 if ($start < 0);
    }
else {
    $start = $_[1];
    $end = $_[2];
    $end = @files-1 if ($end >= @files);
    }
foreach $f (@files) {
    if ($i < $start || $i > $end) {
        # Skip files outside requested index range
        push(@rv, undef);
        $i++;
        next;
        }
    local $mail = &read_mail_file($f, $_[3]);
    $mail->{'idx'} = $i++;
    $mail->{'id'} = $f;    # ID is relative path, like cur/4535534
    $mail->{'id'} = substr($mail->{'id'}, length($_[0])+1);
    push(@rv, $mail);
    }
return @rv;
}

# idlist_maildir(file)
# Returns a list of files in a maildir, which form the IDs
sub idlist_maildir
{
local ($file) = @_;
&mark_read_maildir($file);
return map { substr($_, length($file)+1) } &get_maildir_files($file);
}

# select_maildir(file, &ids, headersonly)
# Returns a list of messages with the given IDs, from a maildir directory
sub select_maildir
{
local ($file, $ids, $headersonly) = @_;
&mark_read_maildir($file);
local @files = &get_maildir_files($file);
local @rv;
foreach my $i (@$ids) {
    local $path = "$file/$i";
    local $mail = &read_mail_file($path, $headersonly);
    if (!$mail && $path =~ /^(.*)\/(cur|tmp|new)\/([^:]*)(:2,([A-Za-z]*))?$/) {
        # Flag may have changed - update path
        local $suffix = "$2/$3";
        local ($newfile) = grep
          { substr($_, length($file)+1, length($suffix)) eq $suffix }
          @files;
        if ($newfile) {
            $path = $newfile;
            $mail = &read_mail_file($path, $headersonly);
            }
        }
    if (!$mail && $path =~ /\/cur\//) {
        # May have moved - update path
        $path =~ s/\/cur\//\/new\//g;
        $mail = &read_mail_file($path, $headersonly);
        }
    if ($mail) {
        # Set ID from corrected path
        $mail->{'id'} = $path;
        $mail->{'id'} = substr($mail->{'id'}, length($file)+1);
        # Get index in directory
        $mail->{'idx'} = &indexof($path, @files);
        }
    push(@rv, $mail);
    }
return @rv;
}

# Get ordered list of message files (with in-memory and on-disk caching, as
# this can be slow)
# get_maildir_files(directory)
sub get_maildir_files
{
# Work out last modified time
local $newest;
foreach my $d ("$_[0]/cur", "$_[0]/new") {
    local @dst = stat($d);
    $newest = $dst[9] if ($dst[9] > $newest);
    }
local $skipt = $config{'maildir_deleted'} || $userconfig{'maildir_deleted'};

local @files;
if (defined($main::list_maildir_cache{$_[0]}) &&
    $main::list_maildir_cache_time{$_[0]} == $newest) {
    # Use the in-memory cache cache
    @files = @{$main::list_maildir_cache{$_[0]}};
    }
else {
    # Check the on-disk cache file
    local $cachefile = &get_maildir_cachefile($_[0]);
    local @cst = $cachefile ? stat($cachefile) : ( );
    if ($cst[9] >= $newest) {
        # Can read the cache
        open(CACHE, $cachefile);
        while(<CACHE>) {
            chop;
            push(@files, $_[0]."/".$_);
            }
        close(CACHE);
        $main::list_maildir_cache_time{$_[0]} = $cst[9];
        }
    else {
        # Really read
        local @shorts;
        foreach my $d ("cur", "new") {
            opendir(DIR, "$_[0]/$d");
            while(my $f = readdir(DIR)) {
                next if ($f eq "." || $f eq "..");
                if ($skipt && $f =~ /:2,([A-Za-z]*T[A-Za-z]*)$/) {
                    # Flagged as deleted by IMAP .. skip
                    next;
                    }
                push(@shorts, "$d/$f")
                }
            closedir(DIR);
            }
        @shorts = sort { substr($a, 4) cmp substr($b, 4) } @shorts;
        @files = map { "$_[0]/$_" } @shorts;

        # Write out the on-disk cache
        if ($cachefile) {
            &open_tempfile(CACHE, ">$cachefile", 1);
            my $err;
            foreach my $f (@shorts) {
                my $ok = (print CACHE $f,"\n");
                $err++ if (!$ok);
                }
            &close_tempfile(CACHE) if (!$err);
            local @st = stat($_[0]);
            if ($< == 0) {
                # Cache should have some ownership as directory
                &set_ownership_permissions($st[4], $st[5],
                               undef, $cachefile);
                }
            }
        $main::list_maildir_cache_time{$_[0]} = $st[9];
        }
    $main::list_maildir_cache{$_[0]} = \@files;
    }
return @files;
}

# search_maildir(file, field, what)
# Search for messages in a maildir directory, and return the results
sub search_maildir
{
return &advanced_search_maildir($_[0], [ [ $_[1], $_[2] ] ], 1);
}

# advanced_search_maildir(user|file, &fields, andmode, [&limit], [headersonly])
# Search for messages in a maildir directory, and return the results
sub advanced_search_maildir
{
&mark_read_maildir($_[0]);
local @rv;
local ($min, $max);
if ($_[3] && $_[3]->{'latest'}) {
    $min = -1;
    $max = -$_[3]->{'latest'};
    }
local $headersonly = $_[4] && !&matches_needs_body($_[1]);
foreach $mail (&list_maildir($_[0], $min, $max, $headersonly)) {
    push(@rv, $mail) if ($mail &&
                 &mail_matches($_[1], $_[2], $mail));
    }
return @rv;
}

# mark_read_maildir(dir)
# Move any messages in the 'new' directory of this maildir to 'cur'
sub mark_read_maildir
{
local ($dir) = @_;
local @files = &get_maildir_files($dir);
local $i = 0;
foreach my $nf (@files) {
    if (substr($nf, length($dir)+1, 3) eq "new") {
        local $cf = $nf;
        $cf =~ s/\/new\//\/cur\//g;
        if (rename($nf, $cf)) {
            $files[$i] = $cf;
            $changed = 1;
            }
        }
    $i++;
    }
if ($changed) {
    # Update the cache
    $main::list_maildir_cache{$dir} = \@files;
    local $cachefile = &get_maildir_cachefile($dir);
    if ($cachefile) {
        &open_tempfile(CACHE, ">$cachefile", 1);
        foreach my $f (@files) {
            local $short = substr($f, length($dir)+1);
            &print_tempfile(CACHE, $short,"\n");
            }
        &close_tempfile(CACHE);
        local @st = stat($_[0]);
        if ($< == 0) {
            &set_ownership_permissions($st[4], $st[5],
                           undef, $cachefile);
            }
        }
    }
}

# delete_maildir(&mail, ...)
# Delete messages from a maildir directory
sub delete_maildir
{
local $m;

# Find all maildirs being deleted from
local %dirs;
foreach $m (@_) {
    if ($m->{'file'} =~ /^(.*)\/(cur|new)\/([^\/]+)$/) {
        $dirs{$1}->{"$2/$3"} = 1;
        }
    }

# Delete from caches
foreach my $dir (keys %dirs) {
    local $cachefile = &get_maildir_cachefile($dir);
    next if (!$cachefile);
    local @cst = stat($cachefile);
    next if (!@cst);

    # Work out last modified time, and don't update cache if too new
    local $newest;
    foreach my $d ("$dir/cur", "$dir/new") {
        local @dst = stat($d);
        $newest = $dst[9] if ($dst[9] > $newest);
        }
    next if ($newest > $cst[9]);

    local $lref = &read_file_lines($cachefile);
    for(my $i=0; $i<@$lref; $i++) {
        if ($dirs{$dir}->{$lref->[$i]}) {
            # Found an entry to remove
            splice(@$lref, $i--, 1);
            }
        }
    &flush_file_lines($cachefile);
    }

# Actually delete the files
foreach $m (@_) {
    unlink($m->{'file'});
    }

}

# modify_maildir(&oldmail, &newmail, textonly)
# Replaces a message in a maildir directory
sub modify_maildir
{
unlink($_[0]->{'file'});
&send_mail($_[1], $_[0]->{'file'}, $_[2], 1);
}

# write_maildir(&mail, directory, textonly)
# Adds some message in maildir format to a directory
sub write_maildir
{
# Work out last modified time, and don't update cache if too new
local $cachefile = &get_maildir_cachefile($_[1]);
local $up2date = 0;
if ($cachefile) {
    local @cst = stat($cachefile);
    if (@cst) {
        local $newest;
        foreach my $d ("$dir/cur", "$dir/new") {
            local @dst = stat($d);
            $newest = $dst[9] if ($dst[9] > $newest);
            }
        $up2date = 1 if ($newest <= $cst[9]);
        }
    }

# Select a unique filename and write to it
local $now = time();
$_[0]->{'id'} = &unique_maildir_filename($_[1]);
$mf = "$_[1]/$_[0]->{'id'}";
&send_mail($_[0], $mf, $_[2], 1);
$_[0]->{'file'} = $mf;

# Set ownership of the new message file to match the directory
local @st = stat($_[1]);
if ($< == 0) {
    &set_ownership_permissions($st[4], $st[5], undef, $mf);
    }

# Create tmp and new sub-dirs, if missing
foreach my $sd ("tmp", "new") {
    local $sdpath = "$_[1]/$sd";
    if (!-d $sdpath) {
        mkdir($sdpath, 0755);
        if ($< == 0) {
            &set_ownership_permissions($st[4], $st[5],
                           undef, $sdpath);
            }
        }
    }

if ($up2date && $cachefile) {
    # Bring cache up to date
    $now--;
    local $lref = &read_file_lines($cachefile);
    push(@$lref, $_[0]->{'id'});
    &flush_file_lines($cachefile);
    }
}

# unique_maildir_filename(dir)
# Returns a filename for a new message in a maildir, relative to the directory
sub unique_maildir_filename
{
local ($dir) = @_;
mkdir("$dir/cur", 0755);
local $now = time();
local $hn = &get_system_hostname();
++$main::write_maildir_count;
local $rv;
do {
    $rv = "cur/$now.$$.$main::write_maildir_count.$hn";
    $now++;
    } while(-r "$dir/$rv");
return $rv;
}

# empty_maildir(file)
# Delete all messages in an maildir directory
sub empty_maildir
{
local $d;
foreach $d ("$_[0]/cur", "$_[0]/new") {
    local $f;
    opendir(DIR, $d);
    while($f = readdir(DIR)) {
        unlink("$d/$f") if ($f ne '.' && $f ne '..');
        }
    closedir(DIR);
    }
&flush_maildir_cachefile($_[0]);
}

# get_maildir_cachefile(dir)
# Returns the cache file for a maildir directory
sub get_maildir_cachefile
{
local ($dir) = @_;
local $cd = $user_module_config_directory || $module_config_directory;
local $sd = "$cd/maildircache";
if (!-d $sd) {
    &make_dir($sd, 0755) || return undef;
    }
$dir =~ s/\//_/g;
return "$sd/$dir";
}

# flush_maildir_cachefile(dir)
# Clear the on-disk and in-memory maildir caches
sub flush_maildir_cachefile
{
local ($dir) = @_;
local $cachefile = &get_maildir_cachefile($dir);
unlink($cachefile) if ($cachefile);
delete($main::list_maildir_cache{$dir});
delete($main::list_maildir_cache_time{$dir});
}

# count_maildir(dir)
# Returns the number of messages in a maildir directory
sub count_maildir
{
local @files = &get_maildir_files($_[0]);
return scalar(@files);
}

# list_mhdir(file, [start], [end], [headersonly])
# Returns a subset of mail from an MH format directory
sub list_mhdir
{
local ($start, $end, $f, $i, @rv);
opendir(DIR, $_[0]);
local @files = map { "$_[0]/$_" }
        sort { $a <=> $b }
         grep { /^\d+$/ } readdir(DIR);
closedir(DIR);
if (!defined($_[1])) {
    $start = 0;
    $end = @files - 1;
    }
elsif ($_[2] < 0) {
    $start = @files + $_[2] - 1;
    $end = @files + $_[1] - 1;
    $start = 0 if ($start < 0);
    }
else {
    $start = $_[1];
    $end = $_[2];
    $end = @files-1 if ($end >= @files);
    }
foreach $f (@files) {
    if ($i < $start || $i > $end) {
        # Skip files outside requested index range
        push(@rv, undef);
        $i++;
        next;
        }
    local $mail = &read_mail_file($f, $_[3]);
    $mail->{'idx'} = $i++;
    $mail->{'id'} = $f;    # ID is message number
    $mail->{'id'} = substr($mail->{'id'}, length($_[0])+1);
    push(@rv, $mail);
    }
return @rv;
}

# idlist_mhdir(directory)
# Returns a list of files in an MH directory, which are the IDs
sub idlist_mhdir
{
local ($dir) = @_;
opendir(DIR, $dir);
local @files = grep { /^\d+$/ } readdir(DIR);
closedir(DIR);
return @files;
}

# get_mhdir_files(directory)
# Returns a list of full paths to files in an MH directory
sub get_mhdir_files
{
local ($dir) = @_;
return map { "$dir/$_" } &idlist_mhdir($dir);
}

# select_mhdir(file, &ids, headersonly)
# Returns a list of messages with the given indexes, from an mhdir directory
sub select_mhdir
{
local ($file, $ids, $headersonly) = @_;
local @rv;
opendir(DIR, $file);
local @files = map { "$file/$_" }
        sort { $a <=> $b }
         grep { /^\d+$/ } readdir(DIR);
closedir(DIR);
foreach my $i (@$ids) {
    local $mail = &read_mail_file("$file/$i", $headersonly);
    if ($mail) {
        $mail->{'idx'} = &indexof("$file/$i", @files);
        $mail->{'id'} = $i;
        }
    push(@rv, $mail);
    }
return @rv;
}

# search_mhdir(file|user, field, what)
# Search for messages in an MH directory, and return the results
sub search_mhdir
{
return &advanced_search_mhdir($_[0], [ [ $_[1], $_[2] ] ], 1);
}

# advanced_search_mhdir(file|user, &fields, andmode, &limit, [headersonly])
# Search for messages in an MH directory, and return the results
sub advanced_search_mhdir
{
local @rv;
local ($min, $max);
if ($_[3] && $_[3]->{'latest'}) {
    $min = -1;
    $max = -$_[3]->{'latest'};
    }
local $headersonly = $_[4] && !&matches_needs_body($_[1]);
foreach $mail (&list_mhdir($_[0], $min, $max, $headersonly)) {
    push(@rv, $mail) if ($mail && &mail_matches($_[1], $_[2], $mail));
    }
return @rv;
}

# delete_mhdir(&mail, ...)
# Delete messages from an MH directory
sub delete_mhdir
{
local $m;
foreach $m (@_) {
    unlink($m->{'file'});
    }
}

# modify_mhdir(&oldmail, &newmail, textonly)
# Replaces a message in a maildir directory
sub modify_mhdir
{
unlink($_[0]->{'file'});
&send_mail($_[1], $_[0]->{'file'}, $_[2], 1);
}

# max_mhdir(dir)
# Returns the maximum message ID in the directory
sub max_mhdir
{
local $max = 1;
opendir(DIR, $_[0]);
foreach $f (readdir(DIR)) {
    $max = $f if ($f =~ /^\d+$/ && $f > $max);
    }
closedir(DIR);
return $max;
}

# empty_mhdir(file)
# Delete all messages in an MH format directory
sub empty_mhdir
{
local $f;
opendir(DIR, $_[0]);
foreach $f (readdir(DIR)) {
    unlink("$_[0]/$f") if ($f =~ /^\d+$/);
    }
closedir(DIR);
}

# count_mhdir(file)
# Returns the number of messages in an MH directory
sub count_mhdir
{
opendir(DIR, $_[0]);
local @files = grep { /^\d+$/ } readdir(DIR);
closedir(DIR);
return scalar(@files);
}

# read_mail_file(file, [headersonly])
# Read a single message from a file
sub read_mail_file
{
local (@headers, $mail);

# Open and read the mail file
open(MAIL, $_[0]) || return undef;
$mail = &read_mail_fh(MAIL, 0, $_[1]);
$mail->{'file'} = $_[0];
close(MAIL);
local @st = stat($_[0]);
$mail->{'size'} = $st[7];
$mail->{'time'} = $st[9];

# Set read flags based on the name
if ($_[0] =~ /:2,([A-Za-z]*)$/) {
    local @flags = split(//, $1);
    $mail->{'read'} = &indexoflc("S", @flags) >= 0 ? 1 : 0;
    $mail->{'special'} = &indexoflc("F", @flags) >= 0 ? 1 : 0;
    $mail->{'replied'} = &indexoflc("R", @flags) >= 0 ? 1 : 0;
    $mail->{'flags'} = 1;
    }

return $mail;
}

# read_mail_fh(handle, [end-mode], [headersonly])
# Reads an email message from the given file handle, either up to end of
# the file, or a From line. End mode 0 = EOF, 1 = From without -,
#                     2 = From possibly with -
sub read_mail_fh
{
local ($fh, $endmode, $headeronly) = @_;
local (@headers, $mail);

# Read the headers
local $lnum = 0;
while(1) {
    $lnum++;
    local $line = <$fh>;
    $mail->{'size'} += length($line);
    $line =~ s/\r|\n//g;
    last if ($line eq '');
    if ($line =~ /^(\S+):\s*(.*)/) {
        push(@headers, [ $1, $2 ]);
        $mail->{'rawheaders'} .= $line."\n";
        }
    elsif ($line =~ /^\s+(.*)/) {
        $headers[$#headers]->[1] .= " ".$1 unless($#headers < 0);
        $mail->{'rawheaders'} .= $line."\n";
        }
    elsif ($line =~ /^From\s+(\S+).*\d+/ &&
           ($1 ne '-' || $endmode == 2)) {
        $mail->{'fromline'} = $line;
        }
    }
$mail->{'headers'} = \@headers;
foreach $h (@headers) {
    $mail->{'header'}->{lc($h->[0])} = $h->[1];
    }

if (!$headersonly) {
    # Read the mail body
    if ($endmode == 0) {
        # Till EOF
        while(read($fh, $buf, 1024) > 0) {
            $mail->{'size'} += length($buf);
            $mail->{'body'} .= $buf;
            $lc = ($buf =~ tr/\n/\n/);
            $lnum += $lc;
            }
        close(MAIL);
        }
    else {
        # Tell next From line
        while(1) {
            $line = <$fh>;
            last if (!$line || $line =~ /^From\s+(\S+).*\d+\r?\n/ &&
                 ($1 ne '-' || $endmode == 2));
            $lnum++;
            $mail->{'size'} += length($line);
            $mail->{'body'} .= $line;
            }
        }
    $mail->{'lines'} = $lnum;
    }
elsif ($endmode) {
    # Not reading the body, but we still need to search till the next
    # From: line in order to get the size 
    while(1) {
        $line = <$fh>;
        last if (!$line || $line =~ /^From\s+(\S+).*\d+\r?\n/ &&
             ($1 ne '-' || $endmode == 2));
        $lnum++;
        $mail->{'size'} += length($line);
        }
    $mail->{'lines'} = $lnum;
    }
return $mail;
}

# dash_mode(user|file)
# Returns 1 if the messages in this folder are separated by lines like
# From - instead of the usual From foo@bar.com
sub dash_mode
{
open(DASH, &user_mail_file($_[0])) || return 0;    # assume no
local $line = <DASH>;
close(DASH);
return $line =~ /^From\s+(\S+).*\d/ && $1 eq '-';
}

# mail_matches(&fields, andmode, &mail)
# Returns 1 if some message matches a search
sub mail_matches
{
local $count = 0;
local $f;
foreach $f (@{$_[0]}) {
    local $field = $f->[0];
    local $what = $f->[1];
    local $neg = ($field =~ s/^\!//);
    local $re = $f->[2] ? $what : "\Q$what\E";
    if ($field eq 'body') {
        $count++
            if (!$neg && $_[2]->{'body'} =~ /$re/i ||
                 $neg && $_[2]->{'body'} !~ /$re/i);
        }
    elsif ($field eq 'size') {
        $count++
            if (!$neg && $_[2]->{'size'} > $what ||
                 $neg && $_[2]->{'size'} < $what);
        }
    elsif ($field eq 'headers') {
        local $headers = $_[2]->{'rawheaders'} ||
            join("", map { $_->[0].": ".$_->[1]."\n" }
                     @{$_[2]->{'headers'}});
        $count++
            if (!$neg && $headers =~ /$re/i ||
             $neg && $headers !~ /$re/i);
        }
    elsif ($field eq 'all') {
        local $headers = $_[2]->{'rawheaders'} ||
            join("", map { $_->[0].": ".$_->[1]."\n" }
                     @{$_[2]->{'headers'}});
        $count++
            if (!$neg && ($_[2]->{'body'} =~ /$re/i ||
                  $headers =~ /$re/i) ||
                 $neg && ($_[2]->{'body'} !~ /$re/i &&
                  $headers !~ /$re/i));
        }
    elsif ($field eq 'status') {
        $count++
            if (!$neg && $_[2]->{$field} =~ /$re/i||
                 $neg && $_[2]->{$field} !~ /$re/i);
        }
    else {
        $count++
            if (!$neg && $_[2]->{'header'}->{$field} =~ /$re/i||
                 $neg && $_[2]->{'header'}->{$field} !~ /$re/i);
        }
    return 1 if ($count && !$_[1]);
    }
return $count == scalar(@{$_[0]});
}

# search_fields(&fields)
# Returns an array of headers/fields from a search
sub search_fields
{
local @rv;
foreach my $f (@{$_[0]}) {
    $f->[0] =~ /^\!?(.*)$/;
    push(@rv, $1);
    }
return &unique(@rv);
}

# matches_needs_body(&fields)
# Returns 1 if a search needs to check the mail body
sub matches_needs_body
{
foreach my $f (@{$_[0]}) {
    return 1 if ($f->[0] eq 'body' || $f->[0] eq 'all');
    }
return 0;
}

# parse_delivery_status(text)
# Returns the fields from a message/delivery-status attachment
sub parse_delivery_status
{
local @lines = split(/[\r\n]+/, $_[0]);
local (%rv, $l);
foreach $l (@lines) {
    if ($l =~ /^(\S+):\s*(.*)/) {
        $rv{lc($1)} = $2;
        }
    }
return \%rv;
}

# parse_mail_date(string)
# Converts a mail Date: header into a unix time
sub parse_mail_date
{
local ($str) = @_;
$str =~ s/^[, \t]+//;
$str =~ s/\s+$//;
open(OLDSTDERR, ">&STDERR");    # suppress STDERR from Time::Local
close(STDERR);
my $rv = eval {
    if ($str =~ /^(\S+),\s+(\d+)\s+(\S+)\s+(\d+)\s+(\d+):\s?(\d+):\s?(\d+)\s+(\S+)/) {
        # Format like Mon, 13 Dec 2004 14:40:41 +0100
        # or          Mon, 13 Dec 2004 14:18:16 GMT
        # or          Tue, 14 Sep 04 02:45:09 GMT
        local $tm = timegm($7, $6, $5, $2, &month_to_number($3),
                   $4 < 50 ? $4+100 : $4 < 1000 ? $4 : $4-1900);
        local $tz = $8;
        if ($tz =~ /^(\-|\+)?\d+$/) {
            local $tz = int($tz);
            $tz = $tz/100 if ($tz >= 50 || $tz <= -50);
            $tm -= $tz*60*60;
            }
        return $tm;
        }
    elsif ($str =~ /^(\S+),\s+(\d+),?\s+(\S+)\s+(\d+)\s+(\d+):\s?(\d+):\s?(\d+)/) {
        # Format like Mon, 13 Dec 2004 14:40:41 or
        #          Mon, 13, Dec 2004 14:40:41
        # No timezone, so assume local
        local $tm = timelocal($7, $6, $5, $2, &month_to_number($3),
                   $4 < 50 ? $4+100 : $4 < 1000 ? $4 : $4-1900);
        return $tm;
        }
    elsif ($str =~ /^(\S+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+)/) {
        # Format like Tue Dec  7 12:58:52 2004
        local $tm = timelocal($6, $5, $4, $3, &month_to_number($2),
                      $7 < 50 ? $7+100 : $7 < 1000 ? $7 : $7-1900);
        return $tm;
        }
    elsif ($str =~ /^(\S+)\s+(\S+)\s+(\d{1,2})\s+(\d+):(\d+):(\d+)/ &&
           &month_to_number($2)) {
        # Format like Tue Dec  7 12:58:52
        local @now = localtime(time());
        local $tm = timelocal($6, $5, $4, $3, &month_to_number($2),
                      $now[5]);
        return $tm;
        }
    elsif ($str =~ /^(\S+)\s+(\S+)\s+(\d{1,2})\s+(\d+):(\d+)$/ &&
           defined(&month_to_number($2))) {
        # Format like Tue Dec  7 12:58
        local @now = localtime(time());
        local $tm = timelocal(0, $5, $4, $3, &month_to_number($2),
                      $now[5]);
        return $tm;
        }
    elsif ($str =~ /^(\S+)\s+(\d{1,2})\s+(\d+):(\d+)$/ &&
           defined(&month_to_number($1))) {
        # Format like Dec  7 12:58
        local @now = localtime(time());
        local $tm = timelocal(0, $4, $3, $2, &month_to_number($1),
                      $now[5]);
        return $tm;
        }
    elsif ($str =~ /^(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+)\s+(\S+)/) {
        # Format like Dec  7 12:58:52 2004 GMT
        local $tm = timegm($5, $4, $3, $2, &month_to_number($1),
                  $6 < 50 ? $6+100 : $6 < 1000 ? $6 : $6-1900);
        local $tz = $7;
        if ($tz =~ /^(\-|\+)?\d+$/) {
            $tz = int($tz);
            $tz = $tz/100 if ($tz >= 50 || $tz <= -50);
            $tm -= $tz*60*60;
            }
        return $tm;
        }
    elsif ($str =~ /^(\d{4})\-(\d+)\-(\d+)\s+(\d+):(\d+)/) {
        # Format like 2004-12-07 12:53
        local $tm = timelocal(0, $4, $4, $3, $2-1,
                      $1 < 50 ? $1+100 : $1 < 1000 ? $1 : $1-1900);
        return $tm;
        }
    elsif ($str =~ /^(\d+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\S+)/) {
        # Format like 30 Jun 2005 21:01:01 -0000
        local $tm = timegm($6, $5, $4, $1, &month_to_number($2),
                   $3 < 50 ? $3+100 : $3 < 1000 ? $3 : $3-1900);
        local $tz = $7;
        if ($tz =~ /^(\-|\+)?\d+$/) {
            $tz = int($tz);
            $tz = $tz/100 if ($tz >= 50 || $tz <= -50);
            $tm -= $tz*60*60;
            }
        return $tm;
        }
    elsif ($str =~ /^(\d+)\/(\S+)\/(\d+)\s+(\d+):(\d+)/) {
        # Format like 21/Feb/2008 24:13
        local $tm = timelocal(0, $5, $4, $1, &month_to_number($2),
                      $3-1900);
        return $tm;
        }
    else {
        return undef;
        }
    };
open(STDERR, ">&OLDSTDERR");
close(OLDSTDERR);
if ($@) {
    #print STDERR "parsing of $str failed : $@\n";
    return undef;
    }
return $rv;
}

# send_text_mail(from, to, cc, subject, body, [smtp-server])
# A convenience function for sending a email with just a text body
sub send_text_mail
{
local ($from, $to, $cc, $subject, $body, $smtp) = @_;
local $cs = &get_charset();
local $attach = $body =~ /[\177-\377]/ ?
    { 'headers' => [ [ 'Content-Type', 'text/plain; charset='.$cs ],
                 [ 'Content-Transfer-Encoding', 'quoted-printable' ] ],
          'data' => &quoted_encode($body) } :
    { 'headers' => [ [ 'Content-type', 'text/plain' ] ],
      'data' => &entities_to_ascii($body) };
local $mail = { 'headers' =>
        [ [ 'From', $from ],
          [ 'To', $to ],
          [ 'Cc', $cc ],
          [ 'Subject', $subject ] ],
        'attach' => [ $attach ] };
return &send_mail($mail, undef, 1, 0, $smtp);
}

# make_from_line(address, [time])
# Returns a From line for mbox emails, based on the current time
sub make_from_line
{
local ($addr, $t) = @_;
$t ||= time();
&clear_time_locale();
local $rv = "From $addr ".strftime("%a %b %e %H:%M:%S %Y", localtime($t)); 
&reset_time_locale();
return $rv;
}

sub notes_decode
{
# Deprecated - does nothing
}

# add_mailer_ip_headers(&headers)
# Add X-Mailer and X-Originating-IP headers, if enabled
sub add_mailer_ip_headers
{
local ($headers) = @_;
if (!$config{'no_orig_ip'}) {
    push(@$headers, [ 'X-Originating-IP', $ENV{'REMOTE_ADDR'} ]);
    }
if (!$config{'no_mailer'}) {
    push(@$headers, [ 'X-Mailer', ucfirst(&get_product_name())." ".
                      &get_webmin_version() ]);
    }
}

1;

:: Command execute ::

Enter:
 
Select:
 

:: Shadow's tricks :D ::

Useful Commands
 
Warning. Kernel may be alerted using higher levels
Kernel Info:

:: Preddy's tricks :D ::

Php Safe-Mode Bypass (Read Files)

File:

eg: /etc/passwd

Php Safe-Mode Bypass (List Directories):

Dir:

eg: /etc/

:: Search ::
  - regexp 

:: Upload ::
 
[ Read-Only ]

:: Make Dir ::
 
[ Read-Only ]
:: Make File ::
 
[ Read-Only ]

:: Go Dir ::
 
:: Go File ::
 

--[ c999shell v. 1.0 pre-release build #16 Modded by Shadow & Preddy | RootShell Security Group | r57 c99 shell | Generation time: 0.0176 ]--