/usr/share/usermin/mailbox
NameSizeModeActions
images/-0755rm
lang/-0755rm
ulang/-0755rm
address_chooser.cgi52170755editdlrm
add_address.cgi6110755editdlrm
auto.pl19880755editdlrm
boxes-lib.pl835470755editdlrm
CHANGELOG154490644editdlrm
classlinks.sh1600755editdlrm
config-aix4820644editdlrm
config-ALL-linux5030644editdlrm
config-freebsd4780644editdlrm
config-hpux4780644editdlrm
config-irix4770644editdlrm
config-macos4780644editdlrm
config-msc-linux4950644editdlrm
config-netbsd4780644editdlrm
config-openbsd4780644editdlrm
config-openserver4830644editdlrm
config-osf14840644editdlrm
config-qnx4990644editdlrm
config-solaris4770644editdlrm
config-unixware4770644editdlrm
config.info33070644editdlrm
config.info.ca36220755editdlrm
config.info.de16280644editdlrm
config.info.el48740644editdlrm
config.info.hu00644editdlrm
config.info.nl29670755editdlrm
config.info.no31660644editdlrm
copy.cgi8480755editdlrm
copy_form.cgi11610755editdlrm
defaultuconfig7630644editdlrm
delete_folders.cgi13520755editdlrm
delete_mail.cgi71090755editdlrm
detach.cgi31970755editdlrm
detachall.cgi20060755editdlrm
edit_auto.cgi18580755editdlrm
edit_comp.cgi16230755editdlrm
edit_folder.cgi27330755editdlrm
edit_ifolder.cgi10780755editdlrm
edit_imap.cgi19010755editdlrm
edit_pop3.cgi17290755editdlrm
edit_sig.cgi5520755editdlrm
edit_virt.cgi12750755editdlrm
export.cgi13540755editdlrm
folders-lib.pl1352040755editdlrm
import.cgi30970755editdlrm
inbox_login.cgi14470755editdlrm
inbox_login_error.cgi11950755editdlrm
inbox_logout.cgi5030755editdlrm
index.cgi125850755editdlrm
list_addresses.cgi87290755editdlrm
list_folders.cgi53620755editdlrm
list_ifolders.cgi30910755editdlrm
mailbox-lib.pl677380755editdlrm
mail_search.cgi76590755editdlrm
module.info2960644editdlrm
module.info.ar00644editdlrm
module.info.ar.auto2560644editdlrm
module.info.bg00644editdlrm
module.info.bg.auto3050644editdlrm
module.info.ca260644editdlrm
module.info.ca.auto1680644editdlrm
module.info.cs250644editdlrm
module.info.cs.auto1420644editdlrm
module.info.da00644editdlrm
module.info.da.auto1590644editdlrm
module.info.de1190644editdlrm
module.info.el00644editdlrm
module.info.el.auto3570644editdlrm
module.info.es260644editdlrm
module.info.es.auto1630644editdlrm
module.info.eu00644editdlrm
module.info.eu.auto1660644editdlrm
module.info.fa00644editdlrm
module.info.fa.auto2420644editdlrm
module.info.fi00644editdlrm
module.info.fi.auto1490644editdlrm
module.info.fr260644editdlrm
module.info.fr.auto1670644editdlrm
module.info.hr00644editdlrm
module.info.hr.auto1670644editdlrm
module.info.hu380644editdlrm
module.info.hu.auto1730644editdlrm
module.info.it00644editdlrm
module.info.it.auto1510644editdlrm
module.info.ja00644editdlrm
module.info.ja.auto2130644editdlrm
module.info.ko00644editdlrm
module.info.ko.auto1770644editdlrm
module.info.ms00644editdlrm
module.info.ms.auto1370644editdlrm
module.info.nl290644editdlrm
module.info.nl.auto1370644editdlrm
module.info.no190644editdlrm
module.info.no.auto1220644editdlrm
module.info.pl1790644editdlrm
module.info.pl.auto1700644editdlrm
module.info.pt00644editdlrm
module.info.pt.auto1590644editdlrm
module.info.pt_BR00644editdlrm
module.info.pt_BR.auto1650644editdlrm
module.info.ru550644editdlrm
module.info.ru.auto2640644editdlrm
module.info.sk00644editdlrm
module.info.sk.auto1620644editdlrm
module.info.sv00644editdlrm
module.info.sv.auto1500644editdlrm
module.info.tr00644editdlrm
module.info.tr.auto1720644editdlrm
module.info.uk00644editdlrm
module.info.uk.auto2980644editdlrm
module.info.zh00644editdlrm
module.info.zh.auto1410644editdlrm
module.info.zh_TW00644editdlrm
module.info.zh_TW.auto1470644editdlrm
newfolder.cgi2230755editdlrm
print_mail.cgi13580755editdlrm
quotacheck.pl32610755editdlrm
reply_mail.cgi272510755editdlrm
save_address.cgi10340755editdlrm
save_allow.cgi7610755editdlrm
save_auto.cgi11530755editdlrm
save_comp.cgi12660755editdlrm
save_folder.cgi40000755editdlrm
save_group.cgi7970755editdlrm
save_ifolder.cgi18200755editdlrm
save_imap.cgi18990755editdlrm
save_pop3.cgi16670755editdlrm
save_sig.cgi6480755editdlrm
save_virt.cgi10360755editdlrm
search_form.cgi26410755editdlrm
send_mail.cgi194460755editdlrm
slideshow.cgi18230755editdlrm
sort.cgi11630755editdlrm
specialsync.cgi12320755editdlrm
system_info.pl16430644editdlrm
uconfig.info54330644editdlrm
uconfig.info.ca57200755editdlrm
uconfig.info.cs50870644editdlrm
uconfig.info.da00644editdlrm
uconfig.info.de60020644editdlrm
uconfig.info.el74750644editdlrm
uconfig.info.es00644editdlrm
uconfig.info.fr1450644editdlrm
uconfig.info.hu00644editdlrm
uconfig.info.it33480644editdlrm
uconfig.info.nl53370644editdlrm
uconfig.info.no50780644editdlrm
uconfig.info.pl00755editdlrm
uconfig.info.pt_BR50730644editdlrm
uconfig_info.pl3170755editdlrm
view_mail.cgi180860755editdlrm
virtualize.cgi7530755editdlrm
webmin_menu.pl47780644editdlrm
xhr-lib.pl17370644editdlrm
xhr.cgi2520755editdlrm
Edit: /usr/share/usermin/mailbox/boxes-lib.pl (83547B)
# boxes-lib.pl # Functions to parsing user mail files use POSIX; use Fcntl; 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; my $umf = &user_mail_file($_[0]); &open_as_mail_user(MAIL, $umf) || &error("Failed to open $umf : $!"); &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]); $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_as_mail_user(MAIL, $umf) || &error("Failed to open $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 = ; 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); local $umf = &user_mail_file($_[0]); &open_as_mail_user(MAIL, $umf) || &error("Failed to open $umf : $!"); # 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 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 ($user, $index, $noperm) = @_; local $ifile = &user_index_file($user); local $umf = &user_mail_file($user); local @st = stat($umf); if (!defined($noperm)) { # Use global override setting $noperm = $no_permanent_index; } if ($noperm && &has_dbm_index($user)) { # Index already exists, so use it $noperm = 0; } if (!$noperm) { dbmopen(%$index, $ifile, 0600); } # Read file of IDs local $idsfile = $ifile.".ids"; local @ids; local $idschanged; if (!$noperm && open(IDSFILE, "<", $idsfile)) { @ids = ; chop(@ids); close(IDSFILE); } if (scalar(@ids) != $index->{'mailcount'}) { # Build for first time print DEBUG "need meta-index rebuild for $user ",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_as_mail_user(IMAIL, $umf); } else { if (&open_as_mail_user(IMAIL, $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(IMAIL, $idx[0], 0); $ll = ; $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(IMAIL, 0, 0); @ids = ( ); $idschanged = 1; %$index = ( ); } local ($doingheaders, @nidx); while() { 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(IMAIL); $index->{'lastchange'} = time(); $index->{'lastsize'} = $st[7]; $index->{'mailcount'} = $istart; $index->{'version'} = $dbm_index_version; } # Write out IDs file, if needed if ($idschanged && !$noperm) { 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 ($user) = @_; local $umf = &user_mail_file($user); local $ifile = &user_index_file($user); &open_as_mail_user(TRUNC, ">$umf") || &error("Failed to open $umf : $!"); close(TRUNC); # Set index size to 0 (if there is one) if (&has_dbm_index($user)) { 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 =~ /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); } elsif ($ct !~ /^multipart\/\S+/ && $ct =~ /^([^;\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'} = &decode_base64($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'} = "ed_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'} = ; 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() { $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' => &decode_base64($_[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 = &decode_base64($_[0]->{'body'}); } elsif (lc($_[0]->{'header'}->{'content-transfer-encoding'}) eq 'quoted-printable') { $body = "ed_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_as_mail_user(SOURCE, $f) || &error("Failed to open $f : $!"); &create_as_mail_user(DEST, ">$tmpf") || &error("Failed to open temp file $tmpf : $!"); while() { 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); # Force a total index re-build (XXX lazy!) $index{'mailcount'} = $in{'lastchange'} = 0; dbmclose(%index); if ($< == 0) { # Replace the mail file with the copy unlink($f); rename($tmpf, $f); if (!&should_switch_to_mail_user()) { # Since write was done as root, set back permissions on the # mail file to match the original chown($st[4], $st[5], $f); chmod($st[2], $f); } else { &chmod_as_mail_user($st[2], $f); } } else { system("cat ".quotemeta($tmpf)." > ".quotemeta($f). " && rm -f ".quotemeta($tmpf)); } } # 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_as_mail_user(SOURCE, $f) || &error("Failed to open $f : $!"); &create_as_mail_user(DEST, ">$tmpf") || &error("Failed to open temp file $tmpf : $!"); while() { 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_as_mail_user(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); if ($< == 0) { unlink($f); rename($tmpf, $f); if (!&should_switch_to_mail_user()) { # Since write was done as root, set back permissions on the # mail file to match the original chown($st[4], $st[5], $f); chmod($st[2], $f); } else { &chmod_as_mail_user($st[2], $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], # [¬ify-flags], [port], [use-ssl]) # Send out some email message or append it to a file. # Returns the number of lines written. sub send_mail { local ($mail, $file, $textonly, $nocr, $sm, $user, $pass, $auth, $flags, $port, $ssl) = @_; return 0 if (&is_readonly_mode()); local $lnum = 0; $sm ||= $config{'send_mode'}; local $eol = $nocr || !$sm ? "\n" : "\r\n"; $ssl = $config{'smtp_ssl'} if ($ssl eq ''); local $defport = $ssl == 1 ? 465 : 25; $port ||= $config{'smtp_port'} || $defport; my %header; foreach my $head (@{$mail->{'headers'}}) { $header{lc($head->[0])} = $head->[1]; } # Add the date header, always in english &clear_time_locale(); local @tm = localtime(time()); push(@{$mail->{'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 $qfromaddr = quotemeta($fromaddr); local $esmtp = $flags ? 1 : 0; my $h = { 'fh' => 'mailboxes::MAIL' }; if ($file) { # Just append the email to a file using mbox format &open_as_mail_user($h->{'fh'}, ">>$file") || &error("Write failed : $!"); $lnum++; &write_http_connection($h, $mail->{'fromline'} ? $mail->{'fromline'}.$eol : &make_from_line($fromaddr).$eol); } elsif ($sm) { # Connect to SMTP server &open_socket($sm, $port, $h->{'fh'}); if ($ssl == 1) { # Start using SSL mode right away &switch_smtp_to_ssl($h); } &smtp_command($h, undef, 0); # Send SMTP HELO my $helo = $config{'helo_name'} || &get_system_hostname(); my $helocmd = $esmtp ? "ehlo $helo\r\n" : "helo $helo\r\n"; &smtp_command($h, $helocmd, 0); if ($ssl == 2) { # Switch to SSL with STARTTLS, if possible my $rv = &smtp_command($h, "starttls\r\n", 1); if ($rv =~ /^2\d+/) { &switch_smtp_to_ssl($h); &smtp_command($h, $helocmd, 0); } else { $ssl = 0; } } # Get username and password from parameters, or from module config $user ||= $userconfig{'smtp_user'} || $config{'smtp_user'}; $pass ||= $userconfig{'smtp_pass'} || $config{'smtp_pass'}; $auth ||= $userconfig{'smtp_auth'} || $config{'smtp_auth'} || "Cram-MD5"; if ($user) { # Send authentication commands eval "use Authen::SASL"; if ($@) { &error("Perl module Authen::SASL 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($h, "AUTH ".uc($auth)."\r\n", 1); if ($arv =~ /^(334)(\-\S+)?\s+(.*)/) { # Server says to go ahead $extra = $3; local $initial = $conn->client_start(); local $auth_ok; if ($initial) { local $enc = &encode_base64($initial); $enc =~ s/\r|\n//g; $arv = &smtp_command($h, "$enc\r\n", 1); if ($arv =~ /^(\d+)(\-\S+)?\s+(.*)/) { if ($1 == 235) { $auth_ok = 1; } else { &error("Unknown SMTP authentication response : $arv"); } } $extra = $3; } 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($h, "$enc\r\n", 1); if ($arv =~ /^(\d+)(\-\S+)?\s+(.*)/) { if ($1 == 235) { $auth_ok = 1; } elsif ($1 == 535) { &error("SMTP authentication failed : $arv"); } $extra = $3; } else { &error("Unknown SMTP authentication response : $arv"); } } } } &smtp_command($h, "mail from: <$fromaddr>\r\n", 0); local $notify = $flags ? " NOTIFY=".join(",", @$flags) : ""; foreach my $u (@dests) { &smtp_command($h, "rcpt to: <$u>$notify\r\n", 0); } &smtp_command($h, "data\r\n", 0); } 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($h->{'fh'}, "| $cmd >/dev/null 2>&1"); } elsif ($config{'qmail_dir'}) { # Start qmail-inject open($h->{'fh'}, "| $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($h->{'fh'}, "| $cmd -f$qfromaddr $qdests >/dev/null 2>&1"); } else { # Start sendmail &has_command($config{'sendmail_path'}) || &error(&text('send_epath', "$config{'sendmail_path'}")); open($h->{'fh'}, "| $config{'sendmail_path'} -f$qfromaddr $qdests >/dev/null 2>&1"); } local $ctype = "multipart/mixed"; local $msg_id; foreach $head (@{$mail->{'headers'}}) { if (defined($mail->{'body'}) || $textonly) { &write_http_connection($h, $head->[0],": ",$head->[1],$eol); $lnum++; } else { if ($head->[0] !~ /^(MIME-Version|Content-Type)$/i) { &write_http_connection($h, $head->[0],": ",$head->[1],$eol); $lnum++; } elsif (lc($head->[0]) eq 'content-type') { $ctype = $head->[1]; } } if (lc($head->[0]) eq 'message-id') { $msg_id++; } } if (!$msg_id) { # Add a message-id header if missing $main::mailboxes_message_id_count++; &write_http_connection($h, "Message-Id: <",time().".".$$.".". $main::mailboxes_message_id_count."\@". &get_system_hostname(),">",$eol); } # Work out first attachment content type local ($ftype, $fenc); if (@{$mail->{'attach'}} >= 1) { local $first = $mail->{'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($mail->{'body'})) { # Use original mail body &write_http_connection($h, $eol); $lnum++; $mail->{'body'} =~ s/\r//g; $mail->{'body'} =~ s/\n\.\n/\n\. \n/g; $mail->{'body'} =~ s/\n/$eol/g; $mail->{'body'} .= $eol if ($mail->{'body'} !~ /\n$/); &write_http_connection($h, $mail->{'body'}) || &error("Write failed : $!"); $lnum += ($mail->{'body'} =~ tr/\n/\n/); } elsif (!@{$mail->{'attach'}}) { # No content, so just send empty email &write_http_connection($h, "Content-Type: text/plain",$eol); &write_http_connection($h, $eol); $lnum += 2; } elsif (!$textonly || $ftype !~ /text\/plain/i || $fenc =~ /quoted-printable|base64/) { # Sending MIME-encoded email if ($ctype !~ /multipart\/report/i) { $ctype =~ s/;.*$//; } &write_http_connection($h, "MIME-Version: 1.0",$eol); local $bound = "bound".time(); &write_http_connection($h, "Content-Type: $ctype; boundary=\"$bound\"",$eol); &write_http_connection($h, $eol); $lnum += 3; # Send attachments &write_http_connection($h, "This is a multi-part message in MIME format.",$eol); $lnum++; foreach $a (@{$mail->{'attach'}}) { &write_http_connection($h, $eol); &write_http_connection($h, "--",$bound,$eol); $lnum += 2; local $enc; foreach $head (@{$a->{'headers'}}) { &write_http_connection($h, $head->[0],": ",$head->[1],$eol); $enc = $head->[1] if (lc($head->[0]) eq 'content-transfer-encoding'); $lnum++; } &write_http_connection($h, $eol); $lnum++; if (lc($enc) eq 'base64') { local $enc = &encode_base64($a->{'data'}); $enc =~ s/\r//g; $enc =~ s/\n/$eol/g; &write_http_connection($h, $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; &write_http_connection($h, $a->{'data'}); $lnum += ($a->{'data'} =~ tr/\n/\n/); if ($a->{'data'} !~ /\n$/) { &write_http_connection($h, $eol); $lnum++; } } } &write_http_connection($h, $eol); &write_http_connection($h, "--",$bound,"--",$eol) || &error("Write failed : $!"); &write_http_connection($h, $eol); $lnum += 3; } else { # Sending text-only mail from first attachment local $a = $mail->{'attach'}->[0]; &write_http_connection($h, $eol); $lnum++; $a->{'data'} =~ s/\r//g; $a->{'data'} =~ s/\n/$eol/g; &write_http_connection($h, $a->{'data'}) || &error("Write failed : $!"); $lnum += ($a->{'data'} =~ tr/\n/\n/); if ($a->{'data'} !~ /\n$/) { &write_http_connection($h, $eol); $lnum++; } } if ($sm && !$file) { &smtp_command($h, ".$eol", 0); &smtp_command($h, "quit$eol", 0); } if (!&close_http_connection($h)) { # Only bother to report an error on close if writing to a file if ($file) { &error("Write failed : $!"); } } return $lnum; } # switch_smtp_to_ssl(&handle) # Switch an SMTP connection handle to SSL mode sub switch_smtp_to_ssl { my ($h) = @_; eval "use Net::SSLeay"; $@ && &error($text{'link_essl'}); eval "Net::SSLeay::SSLeay_add_ssl_algorithms()"; eval "Net::SSLeay::load_error_strings()"; $h->{'ssl_ctx'} = Net::SSLeay::CTX_new() || &error("Failed to create SSL context"); $h->{'ssl_con'} = Net::SSLeay::new($h->{'ssl_ctx'}) || &error("Failed to create SSL connection"); Net::SSLeay::set_fd($h->{'ssl_con'}, fileno($h->{'fh'})); Net::SSLeay::connect($h->{'ssl_con'}) || &error("SSL connect() failed"); } # 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]; } # 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() { 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() { $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) # Send a single SMTP command to some file handle, and read back the response sub smtp_command { my ($h, $c, $noerr) = @_; if ($c) { &write_http_connection($h, $c); } my $r = &read_http_connection($h); if ($r !~ /^[23]\d+/ && !$noerr) { $c =~ s/\r|\n//g; &error(&text('send_esmtp', "".&html_escape($c)."", "".&html_escape($r)."")); } $r =~ s/\r|\n//g; if ($r =~ /^(\d+)\-/) { # multi-line ESMTP response! while(1) { my $nr = &read_http_connection($h); $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\.\)])/$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)."". &html_escape($url).""; $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.""; } $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 and # return it as an HTML string sub simplify_date { local ($date, $fmt) = @_; local $u = &parse_mail_date($date); if ($u) { my $locale; if ($userconfig{'date_fmt'} eq 'auto' || $config{'date_fmt'} eq 'auto') { eval "use DateTime; use DateTime::Locale; use DateTime::TimeZone;"; if (!$@ && $] > 5.011) { $locale++; return &make_date($u, undef, $fmt); } } if (!$locale) { $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 &html_escape("$2/$3/$4 $5:$6"); } elsif ($date =~ /^0*(\d+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+)/) { return &html_escape("$1/$2/$3 $4:$5"); } return &html_escape($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 = &convert_header_for_display($_[0], 0, 1); 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 ? " , ..." : ""); } } } # convert_header_for_display(string, [max-non-html-length], [no-escape]) # Given a string from an email header, perform all mime-decoding, charset # changes and HTML escaping needed to render it in a browser sub convert_header_for_display { local ($str, $max, $noescape) = @_; local ($mw, $cs) = &decode_mimewords($str); if (&get_charset() eq 'UTF-8' && &can_convert_to_utf8($mw, $cs)) { $mw = &convert_to_utf8($mw, $cs); } local $rv = &eucconv($mw); $rv = substr($rv, 0, $max)." .." if ($max && length($rv) > $max); return $noescape ? $rv : &html_escape($rv); } # simplify_subject(subject) # Simplifies and truncates a subject header for display in the mail list sub simplify_subject { return &convert_header_for_display($_[0], int($in{'simplify-subject-length'}) || 100); } # 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"; } if (wantarray) { return (join('',map {$_->[0]} @tokens), $charset); } else { 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 = 'UTF-8'; my $defenc = '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; } # can_convert_to_utf8(string, string-charset) # Check if the appropriate perl modules are available for UTF-8 conversion sub can_convert_to_utf8 { my ($str, $cs) = @_; return 0 if ($cs eq "UTF-8"); return 0 if (!$cs); eval "use Encode"; return 0 if ($@); eval "use utf8"; return 0 if ($@); return 1; } # convert_to_utf8(string, string-charset) # If possible, convert a string to the UTF-8 charset sub convert_to_utf8 { my ($str, $cs) = @_; &can_convert_to_utf8(@_); # Load modules eval { $str = Encode::decode($cs, $str); utf8::encode($str); }; return $str; } # decode_utf7(string) # If possible, convert a string like `Gel&APY-schte` to `Gelöschte` # It will also convert complex strings like `Gel&APY-schte & Spam` sub decode_utf7 { my ($a) = @_; eval "use Encode"; return $a if ($@); my $u = find_encoding("UTF-16BE"); return $a if (!$u); my $s = ' '; my @a = split($s, $a); my @b; foreach my $c (@a) { my $b; # Based on Encode::Unicode::UTF7 by Dan Kogai while (pos($c) < length($c)) { if ($c =~ /\G([^&]+)/ogc) { $b .= "$1"; } elsif ($c =~ /\G\&-/ogc) { $b .= "&"; } elsif ($c =~ /\G\&([A-Za-z0-9+,]+)-?/ogsc) { my $d = $1; $d =~ s/,/\//g; my $p = length($d) % 4; $d .= "=" x (4 - $p) if ($p); $b .= $u->decode(decode_base64($d)); } elsif ($c =~ /\G\&/ogc) { $b = $c; } else { return $a; } } push(@b, $b); } return join($s, @b); } # 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 = 'UTF-8'; my $defenc = '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 = 'UTF-8'; 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) # Given a directory and username, returns the path to that user's mail file # under the directory based on the style (which may force use of parts of # the username). 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"; } elsif ($user_module_config_directory) { # Under user's .usermin directory $f = "$user_module_config_directory/$us.findex"; } else { # Under /var/webmin or /etc/webmin $f = "$module_config_directory/$us.findex"; if (!glob($f."*")) { $f = "$module_var_directory/$us.findex"; } } } else { # A username .. the index file is in /var/webmin/modules/mailboxes or # /etc/webmin/mailboxes if ($user_module_config_directory) { $f = "$user_module_config_directory/$_[0].index"; } else { $f = "$module_config_directory/$_[0].index"; if (!glob($f."*")) { $f = "$module_var_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" # Fooey Bar # Fooey Bar # Fooey Bar [mailto:foo@bar.com] # # # 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) # Convert a string for display 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() { 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_as_mail_user(DIR, "$_[0]/$d") || &error("Failed to open $_[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_as_mail_user($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 { my ($mail, $dir, $textonly) = @_; # Work out last modified time, and don't update cache if too new local $cachefile = &get_maildir_cachefile($dir); 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(); $mail->{'id'} = &unique_maildir_filename($dir); $mf = "$dir/$mail->{'id'}"; &send_mail($mail, $mf, $textonly, 1); $mail->{'file'} = $mf; # Set ownership of the new message file to match the directory local @st = stat($dir); 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 = "$dir/$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, $mail->{'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_as_mail_user(DIR, $d) || &error("Failed to open $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; if ($user_module_config_directory) { $cd = $user_module_config_directory; } else { $cd = $module_config_directory; if (!-r "$cd/maildircache") { $cd = $module_var_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_as_mail_user(DIR, $_[0]) || &error("Failed to open $_[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_as_mail_user(DIR, $dir) || &error("Failed to open $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_as_mail_user(DIR, $file) || &error("Failed to open $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_as_mail_user(DIR, $_[0]) || &error("Failed to open $_[0] : $!"); foreach my $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 { &opendir_as_mail_user(DIR, $_[0]) || &error("Failed to open $_[0] : $!"); foreach my $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_as_mail_user(DIR, $_[0]) || &error("Failed to open $_[0] : $!"); local @files = grep { /^\d+$/ } readdir(DIR); closedir(DIR); return scalar(@files); } # list_mbxfile(file, start, end) # Return messages from an MBX format file sub list_mbxfile { local @rv; &open_as_mail_user(MBX, $_[0]) || &error("Failed to open $_[0] : $!"); seek(MBX, 2048, 0); while(my $line = ) { if ($line =~ m/( \d|\d\d)-(\w\w\w)-(\d\d\d\d) (\d\d):(\d\d):(\d\d) ([+-])(\d\d)(\d\d),(\d+);([[:xdigit:]]{8})([[:xdigit:]]{4})-([[:xdigit:]]{8})\r\n$/) { my $size = $10; my $mail = &read_mail_fh(MBX, $size, 0); push(@rv, $mail); } } close(MBX); return @rv; } # select_mbxfile(file, &ids, headersonly) # Returns a list of messages with the given indexes, from a MBX file sub select_mbxfile { local ($file, $ids, $headersonly) = @_; local @all = &list_mbxfile($file); local @rv; foreach my $i (@$ids) { push(@rv, $all[$i]); } return @rv; } # read_mail_file(file, [headersonly]) # Read a single message from a file sub read_mail_file { my ($file, $headersonly) = @_; # Open and read the mail file &open_as_mail_user(MAIL, $file) || return undef; my $mail = &read_mail_fh(MAIL, 0, $headersonly); $mail->{'file'} = $file; close(MAIL); local @st = stat($file); $mail->{'size'} = $st[7]; $mail->{'time'} = $st[9]; $mail->{'ctime'} = $st[10]; # 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 -, # higher = number of bytes sub read_mail_fh { my ($fh, $endmode, $headersonly) = @_; my @headers; my $mail = { }; # Read the headers my $lnum = 0; while(1) { $lnum++; my $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 my $bs = &get_buffer_size(); while(read($fh, $buf, $bs) > 0) { $mail->{'size'} += length($buf); $mail->{'body'} .= $buf; $lc = ($buf =~ tr/\n/\n/); $lnum += $lc; } close(MAIL); } elsif ($endmode > 2) { # Till we have enough bytes while($mail->{'size'} < $endmode) { $line = <$fh>; $lnum++; $mail->{'size'} += length($line); $mail->{'body'} .= $line; } } else { # Till 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_as_mail_user(DASH, &user_mail_file($_[0])) || return 0; # assume no local $line = ; 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], # [smtp-user, smtp-pass]) # A convenience function for sending a email with just a text body sub send_text_mail { local ($from, $to, $cc, $subject, $body, $smtp, $user, $pass) = @_; local $cs = &get_charset(); local $attach = { 'headers' => [ [ 'Content-Type', 'text/plain; charset='.$cs ], [ 'Content-Transfer-Encoding', 'quoted-printable' ] ], 'data' => "ed_encode($body) }; local $mail = { 'headers' => [ [ 'From', $from ], [ 'To', $to ], [ 'Cc', $cc ], [ 'Subject', &encode_mimewords($subject) ] ], 'attach' => [ $attach ] }; return &send_mail($mail, undef, 1, 0, $smtp, $user, $pass); } # 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() ]); } } # set_mail_open_user(user) # Sets the Unix user that will be used for all mail file open ops, by functions # like list_mail and select_maildir sub set_mail_open_user { my ($user) = @_; if ($user eq "root" || $user eq "0") { $main::mail_open_user = undef; } elsif (!$<) { $main::mail_open_user = $user; } } # clear_mail_open_user() # Resets the user to root sub clear_mail_open_user { my ($user) = @_; $main::mail_open_user = undef; } # open_as_mail_user(fh, file) # Calls the open function, but as the user set by set_mail_open_user sub open_as_mail_user { my ($fh, $file) = @_; my $switched = &switch_to_mail_user(); my $mode = "<"; if ($file =~ s/^(<|>>|>|\|)//) { $mode = $1; } my $rv = open($fh, $mode, $file); if ((!$mode || $mode eq "<") && $rv) { # Is it compressed? If so, switch to reading via gzip my $two; read($fh, $two, 2); seek($fh, 0, 0); if ($two eq "\037\213") { # Gzipped .. need to read-open close($fh); open($fh, "gunzip -c ".quotemeta($file)." |"); } } if ($switched) { # Now that it is open, switch back to root $) = 0; $> = 0; } return $rv; } # is_gzipped_file(file) # Returns 1 if a file is gzip compressed sub is_gzipped_file { my ($file) = @_; my $fh; my $rv = open($fh, "<", $file); return 0 if (!$rv); my $two; read($fh, $two, 2); close($fh); return $two eq "\037\213" ? 1 : 0; } # gunzip_mail_file(file) # Uncompress a mail file in place sub gunzip_mail_file { my ($file) = @_; my $switched = &switch_to_mail_user(); my $outfile = $file.".$$.uncompressed"; my @st = stat($file); my $ex = system("gunzip -c ".quotemeta($file)."> ".quotemeta($outfile)." 2>/dev/null"); if ($ex) { unlink($outfile); } else { rename($outfile, $file); &set_ownership_permissions($st[4], $st[5], $st[2], $file); utime($st[8], $st[9], $file); } if ($switched) { $) = 0; $> = 0; } return !$ex; } # create_as_mail_user(fh, file) # Creates a new file, but ensures that it does not yet exist first, and then # sets the ownership to the mail user sub create_as_mail_user { my ($fh, $file) = @_; if (&should_switch_to_mail_user()) { # Open the file as root, but ensure that it doesn't exist yet. Then # make it owned by the user $file =~ s/^>+//; my $rv = sysopen($fh, $file, O_CREAT|O_WRONLY, 0700); return $rv if (!$rv); my @uinfo = &get_switch_user_info(); &set_ownership_permissions($uinfo[2], $uinfo[3], undef, $file); return $rv; } else { # Operating as root, so no special behaviour needed if ($file =~ /^(<|>)/) { return open($fh, $file); } else { return open($fh, "<", $file); } } } # opendir_as_mail_user(fh, dir) # Calls the opendir function, but as the user set by set_mail_open_user sub opendir_as_mail_user { my ($fh, $dir) = @_; my $switched = &switch_to_mail_user(); my $rv = opendir($fh, $dir); if ($switched) { $) = 0; $> = 0; } return $rv; } # rename_as_mail_user(old, new) # Like the rename function, but as the user set by set_mail_open_user sub rename_as_mail_user { my ($oldfile, $newfile) = @_; my $switched = &switch_to_mail_user(); my $rv = &rename_file($oldfile, $newfile); if ($switched) { $) = 0; $> = 0; } return $rv; } # mkdir_as_mail_user(path, perms) # Like the mkdir function, but as the user set by set_mail_open_user sub mkdir_as_mail_user { my ($path, $perms) = @_; my $switched = &switch_to_mail_user(); my $rv = mkdir($path, $perms); if ($switched) { $) = 0; $> = 0; } return $rv; } # unlink_as_mail_user(path) # Like the unlink function, but as the user set by set_mail_open_user sub unlink_as_mail_user { my ($path) = @_; my $switched = &switch_to_mail_user(); my $rv = unlink($path); if ($switched) { $) = 0; $> = 0; } return $rv; } # copy_source_dest_as_mail_user(source, dest) # Copy a file, with perms of the user from set_mail_open_user sub copy_source_dest_as_mail_user { my ($src, $dst) = @_; if (&should_switch_to_mail_user()) { &open_as_mail_user(SRC, $src) || return 0; &open_as_mail_user(DST, ">$dst") || return 0; my $buf; my $bs = &get_buffer_size(); while(read(SRC, $buf, $bs) > 0) { print DST $buf; } close(SRC); close(DST); return 1; } else { return ©_source_dest($src, $dst); } } # chmod_as_mail_user(perms, file, ...) # Set file permissions, but with perms of the user from set_mail_open_user sub chmod_as_mail_user { my ($perms, @files) = @_; my $switched = &switch_to_mail_user(); my $rv = chmod($perms, @files); if ($switched) { $) = 0; $> = 0; } return $rv; } # should_switch_to_mail_user() # Returns 1 if file IO will be done as a mail owner user sub should_switch_to_mail_user { return defined($main::mail_open_user) && !$< && !$>; } # switch_to_mail_user() # Sets the permissions used for reading files sub switch_to_mail_user { if (&should_switch_to_mail_user()) { # Switch file permissions to the correct user my @uinfo = &get_switch_user_info(); @uinfo || &error("Mail open user $main::mail_open_user ". "does not exist"); $) = $uinfo[3]." ".join(" ", $uinfo[3], &other_groups($uinfo[0])); $> = $uinfo[2]; return 1; } return 0; } # get_switch_user_info() # Returns the getpw* function array for the user to switch to sub get_switch_user_info { if ($main::mail_open_user =~ /^\d+$/) { # Could be by UID .. but fall back to by name if there is no such UID my @rv = getpwuid($main::mail_open_user); return @rv if (@rv > 0); } return getpwnam($main::mail_open_user); } # is_ascii() # Checks if string is ASCII sub is_ascii { my ($str) = @_; my $str_ = $str; utf8::encode($str_); if ($str eq $str_) { return 1; } else { return 0; } } 1;