#!/usr/bin/perl use File::Find; use File::Copy; use File::Path; # From http://mail.us-it.net/ebay/cgi-bin/install357.pl # NOTE: If you had to change the path to perl (above) to execute this # script, don't worry, the install process will effect the change in # the cgi scripts used by webmail! COOL! # Your safest bet is to do a symbolic link from your current perl location # to /usr/bin/perl, like this: ln -s /usr/sbin/perl /usr/bin/perl require 5.004; # check to see if install option specified on command line... $option = shift @ARGV; # This file reads itself (Freud would have a field day with this one, # not to mention Letterman!) to determine where your perl executable is. # If it is NOT /usr/bin/perl, will change all cgi files # see if perl executable has been changed... open(FH,"<$0"); $_=; close(FH); /^(.*)\s+/ && ($perl_exec=$1); # just in case someone decides to add args... ($perl_exec !~ /^#!\/usr\/bin\/perl$/) && ($change_perl_exec=1); ($suid_perl_exec = $perl_exec) =~ s/[^\/]+$/suidperl/; ($suid_perl_fname = $suid_perl_exec) =~ s/^..//; # size before download (size install script SHOULD be!) $ftp_size = int(" 1157120"); # do not change this line!! $uname = (getpwuid($<))[0]; # get user name... if ( $uname eq "root" ) { print "\nERROR: You may not execute this script as 'root'!\n\n"; exit; } # get user id and group id for user "$uname" ($t0,$t1,$wm_uid,$wm_gid,$t4,$t5,$t6,$wm_home) = getpwnam($uname); $wm_home =~ s/\/+$//; # remove trailing slashes... $real_wm_home = $wm_home; # special thanks to Beth for catching this one... if (!open(WFH,">$wm_home/ADJExyz.txt")) { print "\n"; print "Can't write to webmail's home directory : $wm_home\n"; print "Please check permissions and try again...exiting...\n"; print "\n"; exit; } else { close(WFH); unlink "$wm_home/ADJExyz.txt"; } # should have done this long ago... if (! -f "$wm_home/colors.txt") { ! -d "$wm_home/ADJE_WebMail_Dir" && mkdir("$wm_home/ADJE_WebMail_Dir",0700); $wm_home="$wm_home/ADJE_WebMail_Dir"; } print<); ($cgidir,$lic) = split; $cgidir =~ s/\/+$//; # just in case there is a "/" at the end... } while (! -d $cgidir && print "\n ** ERROR: Directory $cgidir not found\n" ) ; # Test to make sure we can make directories in cgi-bin mkdir("$cgidir/ADJExyz",0777); if (! -d "$cgidir/ADJExyz") { print<) { /\s*#/ && next; # ignore comments... /POPSERVER.*"(.*?)"/ && ($last_pop = $1); /SMTPSERVER.*"(.*?)"/ && ($last_smtp = $1); /INSTALLOPTION.*"(.*?)"/ && ($last_option = $1); /\{LIC\}.*"(.*?)"/ && ($last_lic = $1); } } # This is now set up to copy permissions from a previous installation # should be handy for upgrades... $perm = "6755"; # for suidperl in Red Hat Linux (! -e "/usr/bin/suidperl") && ($perm = "755"); # if suidperl not present.. if (!$option) { $inbox = "$cgidir/WebMail/inbox.cgi"; if (-f $inbox) { my ($dev,$inode,$mode) = stat($inbox); #returns mode in base 10 my $oct = sprintf("%o",$mode); # converts mode to octal ($oct =~ /(....)$/) && ($perm = $1); # only need last 4 digits # $perm=oct($perm); # convert back to base 10 for chmode } } elsif ($option eq "server") { # for backward compatibility $perm = "0755"; } elsif ($option eq "setuid") { # for backward compatibility $perm = "6755"; } elsif ($option =~ /^\d+$/) { # if option is a number, set permissions to it.. $perm = $option; } # If POP server is not this machine (localhost), get IP address or # domain name of remote POP server... $default='localhost'; defined($last_pop) && ($default = $last_pop); print "\n"; print " Enter the IP address of the POP server\n"; print " OR enter the domain name of the POP server.\n"; print " (i.e. 205.167.234.14 or mail.ro.com ).\n"; print "\n"; print " [$default] : "; chomp($pop = ); !$pop && ($pop = $default); # If SMTP server is not this machine (localhost), get IP address or # domain name of remote SMTP server... $default='localhost'; defined($last_smtp) && ($default = $last_smtp); print "\n"; print " Enter the IP address of the SMTP (sendmail) server\n"; print " OR enter the domain name of the SMTP (sendmail) server.\n"; print " (i.e. 205.167.234.14 or smtp.ro.com ).\n"; print "\n"; print " [$default] : "; chomp($smtp = ); !$smtp && ($smtp = $default); $spss="1"; if ($option eq "server") { print<); !$tspss && ($tspss = "n"); $spss = ($tspss =~ /^n/i) ? "0" : "1"; } # # Read in archive (all that junk after the __END__) to # determine webmail version number # $start = tell(DATA); $fin = -s "$0"; $end = $fin - $start; open(FH,">$wm_home/ADJE_arc.tar"); binmode(DATA); while($bytes=read(DATA,$_,512)) { $tot += $bytes; # This is probably too cute to hold up, but what we're doing here is # eliminating any "\n"'s that vi may have put at the end of this file # if the user had to edit the location of perl, etc... # The archive MUST end with a NULL character... if ($tot == $end) { while( length && ($char=substr($_,-1)) && ord($char) ) { chop; } } print FH $_; } close(FH); $arcsize = -s "$wm_home/ADJE_arc.tar"; # Exit if download size is incorrect... # override size check if perl executable changed - this should be a rare event if ( $arcsize != $ftp_size ) { print<list_archive("$wm_home/ADJE_arc.tar"); chomp($wmver=$arclist[0]); $wmver =~ s/\///; # # Install archive contents to cgi-bin.... # $wmcgidir = "$cgidir/$wmver"; $WebMail_cgidir = "$cgidir/WebMail"; chdir($cgidir); $wmver && (-d $wmcgidir) && rmtree($wmcgidir); # remove previous install if ( !defined(Archive::Tar->extract_archive("$wm_home/ADJE_arc.tar")) ) { print "Could not extract archive, exiting...\n"; exit; } # we're done with the archive, let's remove it! -f "$wm_home/ADJE_arc.tar" && unlink "$wm_home/ADJE_arc.tar"; # prompt for language and copy to language.cgi opendir(FH,$wmcgidir); while ($_=readdir(FH)) { /language\.(.+?)\.cgi/ && push(@langfiles,$1); } @langs = sort {$a cmp $b} @langfiles; print "\n"; print " Enter language for labels, buttons, etc...\n"; print "\n"; $i=0; for (@langs) { print " $i) $_\n"; /^english$/ && ($engnum = $i); $i++; } do { print "\n [$engnum] : "; chomp($langnum=); ($langnum eq '') && ($langnum=$engnum); } while ( ( ($langnum !~ /\d+/) || ($langnum > $#langs) ) && print "\n ** ERROR: Enter 0 - $#langs\n" ) ; $langfname = "language.$langs[$langnum].cgi"; $langname = $langs[$langnum]; print "\n Language = $langname\n"; if (! -f "$wm_home/trailers.txt") { # rename("$wmcgidir/trailers.txt","$wm_home/trailers.txt"); copy("$wmcgidir/trailers.txt","$wm_home/trailers.txt"); copy("$wmcgidir/trailers.txt","$wm_home/trailers.txt.bkup"); chmod(0644,"$wm_home/trailers.txt"); } # these cause too much confusion, so nuke 'em unlink "$wmcgidir/trailers.txt"; unlink "$WebMail_cgidir/trailers.txt"; if (! -f "$wm_home/ads.html") { # rename("$wmcgidir/ads.html","$wm_home/ads.html"); copy("$wmcgidir/ads.html","$wm_home/ads.html"); copy("$wmcgidir/ads.html","$wm_home/ads.html.bkup"); chmod(0644,"$wm_home/ads.html"); } unlink "$wmcgidir/ads.html"; unlink "$WebMail_cgidir/ads.html"; if (! -f "$wm_home/colors.txt") { # rename("$wmcgidir/colors.txt","$wm_home/colors.txt"); copy("$wmcgidir/colors.txt","$wm_home/colors.txt"); copy("$wmcgidir/colors.txt","$wm_home/colors.txt.bkup"); chmod(0644,"$wm_home/colors.txt"); } unlink "$wmcgidir/colors.txt"; unlink "$WebMail_cgidir/colors.txt"; # Copy entire directory structure to the WebMail directory. # This is our first stab at an easy upgrade process... $file = File::NCopy->new( 'recursive' => 1, 'preserve' => 1, 'follow_links' => 1, 'force_write' => 1, ); # if WebMail dir exists, remove all contents, if not, create it... $main_wmdir = "$cgidir/WebMail"; if ( -d $main_wmdir) { rmtree("$main_wmdir"); } mkdir($main_wmdir,0755); # copy all files/dirs from version to main (WebMail) directory.. $file->copy("$wmcgidir/.",$main_wmdir); # # copy language files... # copy("$wmcgidir/$langfname","$wmcgidir/language.cgi"); copy("$wmcgidir/$langfname","$main_wmdir/language.cgi"); chmod(0755,"$wmcgidir/language.cgi"); chmod(0755,"$main_wmdir/language.cgi"); # This is where we get to change the source to suit this # system. The dash is for DC..! GO VANESSA! # # - Update perl executable if needed # - Update use lib "." with hard coded current cgi directory # opendir(DFH,$wmcgidir); while($_=readdir(DFH)) { !/cgi$/ && next; # only mod cgi files... ($base,$ext) = split(/\./,$_); $src_fullpath="$wmcgidir/$_"; $tmp_fullpath="$wmcgidir/$base.tmp"; open(SFH,"<$src_fullpath"); open(TFH,">$tmp_fullpath"); open(WFH,">$main_wmdir/$_"); # change perl executable in source if not /usr/bin/perl... # you may not like the coding, but ya gotta "hats-off" to the # attention to detail... if ($change_perl_exec) { $src_exec = ; ($new_perl_exec = $src_exec) =~ s/^#!.*? /$perl_exec /; # @srcexec=split(/ +/,$src_exec); # get command line arguments... # $srcexec[0]=$perl_exec; # $new_perl_exec = "@srcexec"; # joins elements with a " " (space)... print TFH "$new_perl_exec"; print WFH "$new_perl_exec"; } # hard code cgi dir path in cgi scripts... while () { if (/^use +lib +\"(.)/) { ($tfh=$_)=~s/\"./\"$wmcgidir/; ($wfh=$_)=~s/\"./\"$main_wmdir/; print TFH $tfh; print WFH $wfh; next; } if (/^require +\"./) { ($tfh=$_)=~s/\"./\"$wmcgidir/; ($wfh=$_)=~s/\"./\"$main_wmdir/; print TFH $tfh; print WFH $wfh; next; } if (/^do +\'./) { ($tfh=$_)=~s/\'./\'$wmcgidir/; ($wfh=$_)=~s/\'./\'$main_wmdir/; print TFH $tfh; print WFH $wfh; next; } print TFH $_; print WFH $_; } close(SFH); close(TFH); close(WFH); # copy modified source over original source.... rename($tmp_fullpath,$src_fullpath); } close(DFH); # # Read in params.cgi from cgi-bin directory # $p_fname = "$wmcgidir/params.cgi"; # full path to params.cgi ! -f $p_fname && (print "* ERROR, exiting.. *, can't find $p_fname\n") && exit; open(PFH,"<$p_fname"); @params = ; # "slurp" the params.cgi file into an array.... close(PFH); # Write params.cgi with new POP, SMTP and ISP parameters... ($wmver =~ /wm(\d)(\d+)/) && ($version = "$1.$2"); for (@params) { / *#/ && (print PFH $_) && next; # do not process comments /\{POPSERVER\}/ && s/".*?"/"$pop"/; /\{SMTPSERVER\}/ && s/".*?"/"$smtp"/; /\{CGIBIN\}/ && s/".*?"/"$cgidir"/; # for version 3.54 /\{INSTALLOPTION\}/ && s/".*?"/"$option"/; /\{SPSS\}/ && s/".*?"/"$spss"/; /\{DEBUG\}/ && s/".*?"/"0"/; # disable debug flag /\{WEBMAILDIR\}/ && s/".*?"/"$wm_home"/; # set home directory (/\{LIC\}/ && $last_lic) && s/".*?"/"$last_lic"/; (/\{LIC\}/ && $lic) && s/".*?"/"$lic"/; /\{VERSION\}/ && s/".*?"/"$version"/; /\{ADID\}/ && s/".*?"/"temp"/; /\{ADNO\}/ && s/".*?"/"temp"/; /\{LANGUAGE\}/ && s/".*?"/"$langname"/; } # write params file with install inputs to version directory AND # WebMail directory and our home directory.... $"=""; open(PFH,">$wmcgidir/params.cgi"); print PFH "@params"; close(PFH); open(PFH,">$cgidir/WebMail/params.cgi"); print PFH "@params"; close(PFH); open(PFH,">$wm_home/.params.cgi.backup"); # for version 3.54 print PFH "@params"; close(PFH); # Set cgi file permissions $octperm = $perm; $perm=oct($perm); # convert back to base 10 for chmod find(\&chmod_cgis,$wmcgidir); find(\&chmod_cgis,$main_wmdir); # only set webmail's home directory to 777 if cgi files are 755 ($octperm =~ /[^6]755$/) && chmod(0777,$wm_home); ($octperm =~ /[^6]755$/) && chmod(0777,$real_wm_home); # Ensure the permissions on cgitest.cgi are 0755 $wm_cgitest = "$wmcgidir/cgitest.cgi"; $main_cgitest = "$main_wmdir/cgitest.cgi"; -f $wm_cgitest && chmod(0755,$wm_cgitest); -f $main_cgitest && chmod(0755,$main_cgitest); print< to continue.... HEREDOC $_=; print< Access Your E-Mail : $main_wmdir/index.cgi ==> Cgi scripts directory : $main_wmdir/ ==> README File : $main_wmdir/README.txt ==> E-Mail Trailer File : $wm_home/trailers.txt ==> Web Page Color Control : $wm_home/colors.txt ==> README File Contents: 1) INSTALLATION EXAMPLES 2) CUSTOMIZING/IMPROVING THE SOFTWARE 3) SOFTWARE FUNCTIONAL OVERVIEW 4) SYSTEM REQUIREMENTS AND LIMITATIONS 5) TERMS OF USE 6) TECHNICAL SUPPORT!!! 7) VERSION IMPROVEMENTS ==> IMPORTANT: Sign up for free software updates at http://www.adjeweb.com! ** NOTES ** - Edit the E-Mail Trailer File (above) to PROMOTE YOUR WEB SITE! - Edit the Color Control File (above) to control web page background colors! ** FINISHED Installing ADJE Webmail Version $version ** HEREDOC # yes, I know, there are always more graceful ways of doing things! sub chmod_cgis { # set cgi file permissions to passed in argument, an octal number # my ($perm) = @_; # $perm=oct($perm); # convert back to base 10 for chmod my $name = "$File::Find::dir/$_"; chown $wm_uid, $wm_gid, $name; # some screwy systems need this! chmod(0755,$name); -f $name && ($name =~ /cgi$/) && chmod($perm,$name); } sub chmod_06755 { my $name = "$File::Find::dir/$_"; chown $wm_uid, $wm_gid, $name; # some screwy systems need this! chmod(0755,$name); -f $name && ($name =~ /cgi$/) && chmod(06755,$name); } sub chmod_0755 { my $name = "$File::Find::dir/$_"; chown $wm_uid, $wm_gid, $name; # some screwy systems need this! chmod(0755,$name); } # ya gotta LOVE this.. finally, almost system independent... BEGIN { package Archive::Tar; use strict; use Carp qw(carp); use Cwd; use Fcntl qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC); use File::Basename; use Symbol; require Time::Local if $^O eq "MacOS"; use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS); $VERSION = do { my @a=q$Name: version_0_21 $ =~ /\d+/g; sprintf "%d." . ("%02d" x $#a ),@a }; require Exporter; @ISA = qw(Exporter); @EXPORT_OK = qw(FILE HARDLINK SYMLINK CHARDEV BLOCKDEV DIR FIFO SOCKET INVALID); %EXPORT_TAGS = (filetypes => \@EXPORT_OK); # Check if symbolic links are available my $symlinks = eval { readlink $0 or 1; }; carp "Symbolic links not available" unless $symlinks || !$^W; # Check if Compress::Zlib is available my $compression = eval { local $SIG{__DIE__}; require Compress::Zlib; sub Compress::Zlib::gzFile::gzseek { my $tmp; $_[0]->gzread ($tmp, 4096), $_[1] -= 4096 while ($_[1] > 4096); $_[0]->gzread ($tmp, $_[1]) if $_[1]; } 1; }; carp "Compression not available" unless $compression || !$^W; # Check for get* (they don't exist on WinNT) my $fake_getpwuid; $fake_getpwuid = "unknown" unless eval { $_ = getpwuid (0); }; # Pointless assigment to make -w shut up my $fake_getgrgid; $fake_getgrgid = "unknown" unless eval { $_ = getgrgid (0); }; # Pointless assigment to make -w shut up # Automagically detect gziped files if they start with this my $gzip_magic_number = "^(?:\037\213|\037\235)"; my $tar_unpack_header = 'A100 A8 A8 A8 A12 A12 A8 A1 A100 A6 A2 A32 A32 A8 A8 A155 x12'; my $tar_pack_header = 'a100 a8 a8 a8 a12 a12 A8 a1 a100 a6 a2 a32 a32 a8 a8 a155 x12', my $tar_header_length = 512; my $time_offset = ($^O eq "MacOS") ? Time::Local::timelocal(0,0,0,1,0,70) : 0; ## Subroutines to return type constants sub FILE() { return 0; } sub HARDLINK() { return 1; } sub SYMLINK() { return 2; } sub CHARDEV() { return 3; } sub BLOCKDEV() { return 4; } sub DIR() { return 5; } sub FIFO() { return 6; } sub SOCKET() { return 8; } sub UNKNOWN() { return 9; } ### ### Non-method functions ### my $error; sub _drat { $error = $! . ''; return; } sub error { $error; } ## filetype -- Determine the type value for a given file sub filetype { my $file = shift; return SYMLINK if (-l $file); # Symlink return FILE if (-f _); # Plain file return DIR if (-d _); # Directory return FIFO if (-p _); # Named pipe return SOCKET if (-S _); # Socket return BLOCKDEV if (-b _); # Block special return CHARDEV if (-c _); # Character special return UNKNOWN; # Something else (like what?) } sub _make_special_file_UNIX { my ($file) = @_; if ($file->{type} == SYMLINK) { symlink $_->{linkname}, $file or $^W && carp ("Making symbolic link from ", $file->{linkname}, " to ", $file->{name}, ", failed.\n"); } elsif ($file->{type} == HARDLINK) { link $_->{linkname}, $file or $^W && carp ("Hard linking ", $_->{linkname}, " to ", $file, ", failed.\n"); } elsif ($file->{type} == FIFO) { system("mknod","$file","p") or $^W && carp "Making fifo ", $file, ", failed.\n"; } elsif ($file->{type} == BLOCKDEV) { system("mknod","$file","b",$_->{devmajor},$_->{devminor}) or $^W && carp ("Making block device ", $file, " (maj=", $_->{devmajor}, ", min=", $_->{devminor}, "), failed.\n"); } elsif ($file->{type} == CHARDEV) { system("mknod", "$file", "c", $_->{devmajor}, $_->{devminor}) or $^W && carp ("Making block device ", $file, " (maj=", $_->{devmajor}, " ,min=", $_->{devminor}, "), failed.\n"); } } sub _make_special_file_Win32 { my ($file) = @_; if ($file->{type} == SYMLINK) { $^W && carp ("Making symbolic link from ", $file->{linkname}, " to ", $file->{name}, ", failed.\n"); } elsif ($file->{type} == HARDLINK) { link $_->{linkname}, $file->{name} or $^W && carp ("Making hard link from ", $file->{linkname}, " to ", $file->{name}, ", failed.\n"); } elsif ($file->{type} == FIFO) { $^W && carp "Making fifo ", $file, ", failed.\n"; } elsif ($file->{type} == BLOCKDEV) { $^W && carp ("Making block device ", $file->{name}, " (maj=", $file->{devmajor}, ", min=", $file->{devminor}, "), failed.\n"); } elsif ($file->{type} == CHARDEV) { $^W && carp ("Making block device ", $file->{name}, " (maj=", $file->{devmajor}, " ,min=", $file->{devminor}, "), failed.\n"); } } *_make_special_file = $^O eq "MSWin32" ? \&_make_special_file_Win32 : \&_make_special_file_UNIX; sub _munge_file { # # Mac path to the Unix like equivalent to be used in tar archives # my $inpath = $_[0]; # # If there are no :'s in the name at all, assume it's a single item in the # current directory. Return it, changing any / in the name into : # if ($inpath !~ m,:,) { $inpath =~ s,/,:,g; return $inpath; } # # If we now split on :, there will be just as many nulls in the list as # there should be up requests, except if it begins with a :, where there # will be one extra. # my @names = split (/:/, $inpath); shift (@names) if ($names[0] eq ""); my @outname = (); # # Work from the end. # my $i; for ($i = $#names; $i >= 0; --$i) { if ($names[$i] eq "") { unshift (@outname, ".."); } else { $names[$i] =~ s,/,:,g; unshift (@outname, $names[$i]); } } my $netpath = join ("/", @outname); $netpath = $netpath . "/" if ($inpath =~ /:$/); if ($inpath !~ m,^:,) { return "/".$netpath; } else { return $netpath; } } sub _get_handle { my $fh; sysseek $_[0], 0, 0; if ($compression && (@_ < 2 || $_[1] != 0)) { my $mode = $#_ ? (int($_[1]) > 1 ? "wb".int($_[1]) : "wb") : "rb"; $fh = Compress::Zlib::gzopen ($_[0], $mode) or goto &_drat; } else { $fh = bless *{$_[0]}{IO}, "Archive::Tar::_io"; binmode $fh or goto &_drat; } return $fh; } sub _read_tar { my ($file, $seekable, $extract) = @_; my $tarfile = []; my ($head, $offset, $size); $file->gzread ($head, $tar_header_length) or goto &_drat; if (substr ($head, 0, 2) =~ /$gzip_magic_number/o) { $error = "Compression not available\n"; return undef; } $offset = $tar_header_length if $seekable; READLOOP: while (length ($head) == $tar_header_length) { my ($name, # string $mode, # octal number $uid, # octal number $gid, # octal number $size, # octal number $mtime, # octal number $chksum, # octal number $type, # character $linkname, # string $magic, # string $version, # two bytes $uname, # string $gname, # string $devmajor, # octal number $devminor, # octal number $prefix) = unpack ($tar_unpack_header, $head); my ($data, $block, $entry); $mode = oct $mode; $uid = oct $uid; $gid = oct $gid; $size = oct $size; $mtime = oct $mtime; $chksum = oct $chksum; $devmajor = oct $devmajor; $devminor = oct $devminor; $name = $prefix."/".$name if $prefix; $prefix = ""; # some broken tar-s don't set the type for directories # so we ass_u_me a directory if the name ends in slash $type = DIR if $name =~ m|/$| and not $type; last READLOOP if $head eq "\0" x 512; # End of archive # Apparently this should really be two blocks of 512 zeroes, # but GNU tar sometimes gets it wrong. See comment in the # source code (tar.c) to GNU cpio. substr ($head, 148, 8) = " "; if (unpack ("%16C*", $head) != $chksum) { warn "$name: checksum error.\n"; } unless ($extract) { # Always read in full 512 byte blocks $block = $size & 0x01ff ? ($size & ~0x01ff) + 512 : $size; if ($seekable) { while ($block > 4096) { $file->gzread ($data, 4096) or goto &_drat; $block -= 4096; } $file->gzread ($data, $block) or goto &_drat if ($block); # Ignore everything we've just read. undef $data; } else { if ($file->gzread ($data, $block) < $block) { $error = "Read error on tarfile."; return undef; } # Throw away any trailing garbage substr ($data, $size) = ""; } } # Guard against tarfiles with garbage at the end last READLOOP if $name eq ''; $entry = {name => $name, mode => $mode, uid => $uid, gid => $gid, size => $size, mtime => $mtime, chksum => $chksum, type => $type, linkname => $linkname, magic => $magic, version => $version, uname => $uname, gname => $gname, devmajor => $devmajor, devminor => $devminor, prefix => $prefix, offset => $offset, data => $data}; if ($extract) { _extract_file ($entry, $file); $file->gzread ($head, 512 - ($size & 0x1ff)) or goto &_drat if ($size & 0x1ff); } else { push @$tarfile, $entry; } $offset += $tar_header_length + ($size & 0x01ff ? ($size & ~0x01ff) + 512 : $size) if $seekable; $file->gzread ($head, $tar_header_length) or goto &_drat; } $file->gzclose () unless $seekable; return $tarfile unless $extract; } sub _format_tar_entry { my ($ref) = shift; my ($tmp,$file,$prefix,$pos); $file = $ref->{name}; if (length ($file) > 99) { $pos = index $file, "/", (length ($file) - 100); next if $pos == -1; # Filename longer than 100 chars! $prefix = substr $file,0,$pos; $file = substr $file,$pos+1; substr ($prefix, 0, -155) = "" if length($prefix)>154; } else { $prefix=""; } $tmp = pack ($tar_pack_header, $file, sprintf("%6o ",$ref->{mode}), sprintf("%6o ",$ref->{uid}), sprintf("%6o ",$ref->{gid}), sprintf("%11o ",$ref->{size}), sprintf("%11o ",$ref->{mtime}), "", #checksum field - space padded by pack("A8") $ref->{type}, $ref->{linkname}, $ref->{magic}, $ref->{version} || '00', $ref->{uname}, $ref->{gname}, sprintf("%6o ",$ref->{devmajor}), sprintf("%6o ",$ref->{devminor}), $prefix); substr($tmp,148,7) = sprintf("%6o\0", unpack("%16C*",$tmp)); return $tmp; } sub _format_tar_file { my @tarfile = @_; my $file = ""; foreach (@tarfile) { $file .= _format_tar_entry $_; $file .= $_->{data}; $file .= "\0" x (512 - ($_->{size} & 0x1ff)) if ($_->{size} & 0x1ff); } $file .= "\0" x 1024; return $file; } sub _write_tar { my $file = shift; my $entry; foreach $entry ((ref ($_[0]) eq 'ARRAY') ? @{$_[0]} : @_) { next unless (ref ($entry) eq 'HASH'); my $src = $^O eq "MacOS" ? "" : $entry->{name}; sysopen (FH, $src, O_RDONLY) && binmode (FH) or next unless $entry->{type} != FILE || $entry->{data}; $file->gzwrite (_format_tar_entry ($entry)) or goto &_drat; if ($entry->{type} == FILE) { if ($entry->{data}) { $file->gzwrite ($entry->{data}) or goto &_drat; } else { my $size = $entry->{size}; my $data; while ($size >= 4096) { sysread (FH, $data, 4096) && $file->gzwrite ($data) or goto &_drat; $size -= 4096; } sysread (FH, $data, $size) && $file->gzwrite ($data) or goto &_drat if $size; close FH; } $file->gzwrite ("\0" x (512 - ($entry->{size} & 511))) or goto &_drat if ($entry->{size} & 511); } } $file->gzwrite ("\0" x 1024) and !$file->gzclose () or goto &_drat; } sub _add_file { my $file = shift; my ($mode,$nlnk,$uid,$gid,$rdev,$size,$mtime,$type,$linkname); if (($mode,$nlnk,$uid,$gid,$rdev,$size,$mtime) = (lstat $file)[2..7,9]) { $linkname = ""; $type = filetype ($file); $linkname = readlink $file if ($type == SYMLINK) && $symlinks; $file = _munge_file ($file) if ($^O eq "MacOS"); return +{name => $file, mode => $mode, uid => $uid, gid => $gid, size => $size, mtime => (($mtime - $time_offset) || 0), chksum => " ", type => $type, linkname => $linkname, magic => "ustar", version => "00", # WinNT protection uname => ($fake_getpwuid || scalar getpwuid($uid)), gname => ($fake_getgrgid || scalar getgrgid ($gid)), devmajor => 0, # We don't handle this yet devminor => 0, # We don't handle this yet prefix => "", data => undef, }; } } sub _extract_file { my ($entry, $handle) = @_; my ($file, $cwd, @path); # For the moment, we assume that all paths in tarfiles # are given according to Unix standards. # Which they *are*, according to the tar format spec! @path = split(/\//,$entry->{name}); $file = pop @path; $file =~ s,:,/,g if $^O eq "MacOS"; $cwd = cwd if @path; foreach (@path) { if ($^O eq "MacOS") { s,:,/,g; $_ = "::" if $_ eq ".."; $_ = ":" if $_ eq "."; } if (-e $_ && ! -d _) { $^W && carp "$_ exists but is not a directory!\n"; next; } mkdir $_, 0777 unless -d _; chdir $_; } if ($entry->{type} == FILE) { # Ordinary file sysopen (FH, $file, O_WRONLY|O_CREAT|O_TRUNC) and binmode FH or goto &_drat; if ($handle) { my $size = $entry->{size}; my $data; while ($size > 4096) { $handle->gzread ($data, 4096) and syswrite (FH, $data, length $data) or goto &_drat; $size -= 4096; } $handle->gzread ($data, $size) and syswrite (FH, $data, length $data) or goto &_drat if ($size); } else { syswrite FH, $entry->{data}, $entry->{size} or goto &_drat } close FH; } elsif ($entry->{type} == DIR) { # Directory goto &_drat if (-e $file && ! -d $file); mkdir $file,0777 unless -d $file; } elsif ($entry->{type} == UNKNOWN) { $error = "unknown file type: $_->{type}"; return undef; } else { _make_special_file ($entry); } utime time, $entry->{mtime}, $file; # We are root, and chown exists chown $entry->{uid}, $entry->{gid}, $file if ($> == 0 and $^O ne "MacOS" and $^O ne "MSWin32"); # chmod is done last, in case it makes file readonly # (this accomodates DOSish OSes) chmod $entry->{mode}, $file; chdir $cwd if @path; } ### ### Methods ### ## ## Class methods ## # Perfom the equivalent of ->new()->add_files(), ->write() without the # overhead of maintaining an Archive::Tar object. sub create_archive { my ($handle, $file, $compress) = splice (@_, 0, 3); if ($compress && !$compression) { $error = "Compression not available.\n"; return undef; } $handle = gensym; open $handle, ref ($file) ? ">&". fileno ($file) : ">" . $file or goto &_drat; _write_tar (_get_handle ($handle, int ($compress)), map {_add_file ($_)} @_); } # Perfom the equivalent of ->new()->list_files() without the overhead # of maintaining an Archive::Tar object. sub list_archive { my ($handle, $file, $fields) = @_; $handle = gensym; open $handle, ref ($file) ? "<&". fileno ($file) : "<" . $file or goto &_drat; my $data = _read_tar (_get_handle ($handle), 1); return map {my %h; @h{@$fields} = @$_{@$fields}; \%h} @$data if (ref $fields eq 'ARRAY' && (@$fields > 1 || $fields->[0] ne 'name')); return map {$_->{name}} @$data; } # Perform the equivalen of ->new()->extract() without the overhead of # maintaining an Archive::Tar object. sub extract_archive { my ($handle, $file) = @_; $handle = gensym; open $handle, ref ($file) ? "<&". fileno ($file) : "<" . $file or goto &_drat; _read_tar (_get_handle ($handle), 0, 1); } # Constructor. Reads tarfile if given an argument that's the name of a # readable file. sub new { my ($class, $file) = @_; my $self = bless {}, $class; $self->read ($file) if defined $file; return $self; } ## Return list with references to hashes representing the tar archive's ## component files. #sub data { # my $self = shift; # return @{$self->{'_data'}}; #} # Read a tarfile. Returns number of component files. sub read { my ($self, $file) = @_; $self->{_data} = []; $self->{_handle} = gensym; open $self->{_handle}, ref ($file) ? "<&". fileno ($file) : "<" . $file or goto &_drat; $self->{_data} = _read_tar (_get_handle ($self->{_handle}), sysseek $self->{_handle}, 0, 1); return scalar @{$self->{_data}}; } # Write a tar archive to file sub write { my ($self, $file, $compress) = @_; return _format_tar_file (@{$self->{_data}}) unless (@_ > 1); my $handle = gensym; open $handle, ref ($file) ? ">&". fileno ($file) : ">" . $file or goto &_drat; if ($compress && !$compression) { $error = "Compression not available.\n"; return undef; } _write_tar (_get_handle ($handle, $compress || 0), $self->{_data}); } # Add files to the archive. Returns number of successfully added files. sub add_files { my $self = shift; my ($counter, $file, $entry); foreach $file (@_) { if ($entry = _add_file ($file)) { push (@{$self->{'_data'}}, $entry); ++$counter; } } return $counter; } # Add data as a file sub add_data { my ($self, $file, $data, $opt) = @_; my $ref = {}; my ($key); if($^O eq "MacOS") { $file = _munge_file($file); } $ref->{'data'} = $data; $ref->{name} = $file; $ref->{mode} = 0666 & (0777 - umask); $ref->{uid} = $>; $ref->{gid} = (split(/ /,$)))[0]; # Yuck $ref->{size} = length $data; $ref->{mtime} = ((time - $time_offset) || 0), $ref->{chksum} = " "; # Utterly pointless $ref->{type} = FILE; # Ordinary file $ref->{linkname} = ""; $ref->{magic} = "ustar"; $ref->{version} = "00"; # WinNT protection $ref->{uname} = $fake_getpwuid || getpwuid ($>); $ref->{gname} = $fake_getgrgid || getgrgid ($ref->{gid}); $ref->{devmajor} = 0; $ref->{devminor} = 0; $ref->{prefix} = ""; if ($opt) { foreach $key (keys %$opt) { $ref->{$key} = $opt->{$key} } } push (@{$self->{'_data'}}, $ref); return 1; } sub remove { my ($self) = shift; my $file; foreach $file (@_) { @{$self->{_data}} = grep {$_->{name} ne $file} @{$self->{'_data'}}; } return $self; } # Get the content of a file sub get_content { my ($self, $file) = @_; my ($entry, $data); foreach $entry (@{$self->{_data}}) { next unless $entry->{name} eq $file; return $entry->{data} unless $entry->{offset}; my $handle = _get_handle ($self->{_handle}); $handle->gzseek ($entry->{offset}, 0) or goto &_drat; $handle->gzread ($data, $entry->{size}) or goto &_drat; return $data; } } # Replace the content of a file sub replace_content { my ($self, $file, $content) = @_; my $entry; foreach $entry (@{$self->{_data}}) { next unless $entry->{name} eq $file; $entry->{data} = $content; $entry->{size} = length $content; $entry->{offset} = undef; return 1; } } # Write a single (probably) file from the in-memory archive to disk sub extract { my $self = shift; my ($file, $entry); foreach $entry (@{$self->{_data}}) { my $cnt = 0; foreach $file (@_) { ++$cnt, next unless $entry->{name} eq $file; my $handle = $entry->{offset} && _get_handle ($self->{_handle}); $handle->gzseek ($entry->{offset}, 0) or goto &_drat if $handle; _extract_file ($entry, $handle); splice (@_, $cnt, 1); last; } last unless @_; } } # Return a list names or attribute hashes for all files in the # in-memory archive. sub list_files { my ($self, $fields) = @_; return map {my %h; @h{@$fields} = @$_{@$fields}; \%h} @{$self->{'_data'}} if (ref $fields eq 'ARRAY' && (@$fields > 1 || $fields->[0] ne 'name')); return map {$_->{name}} @{$self->{'_data'}} } ### Standard end of module :-) 1; # # Sub-package to hide I/O differences between compressed & # uncompressed archives. # # Yes, I could have used the IO::* class hierarchy here, but I'm # trying to minimise the necessity for non-core modules on perl5 # environments > 5.004 package Archive::Tar::_io; sub gzseek { sysseek $_[0], $_[1], $_[2]; } sub gzread { sysread $_[0], $_[1], $_[2]; } sub gzwrite { syswrite $_[0], $_[1], length $_[1]; } sub gzclose { !close $_[0]; } package File::NCopy; =head1 NAME B - Copy file, file Copy file[s] | dir[s], dir =head1 SYNOPSIS use File::NCopy qw(copy); copy "file","other_file"; copy "file1","file2","file3","directory"; # we want to copy the directory recursively copy \1,"directory1","directory2"; copy \1,"file1","file2","directory1","file3","directory2","file4", "directory"; # can also use references to file handles, this is for backward # compatibility with File::Copy copy \*FILE1,\*FILE2; copy \*FILE1,"file"; copy "file1",\*FILE2; # we don't specify \1 as the first argument because we don't want to # copy directories recursively copy "*.c","*.pl","programs"; copy "*", "backup"; use File::NCopy; # the below are the default config values $file = File::NCopy->new( 'recursive' => 0, 'preserve' => 0, 'follow_links' => 0, 'force_write' => 0, 'set_permission' => \&File::NCopy::u_chmod, 'file_check' => \&File::NCopy::f_check, 'set_times' => \&File::NCopy::s_times, ); set_permission will take two file names, the original to get the file permissions from and the new file to set the file permissions for. file_check takes two parameters, the file names to check the file to copy from and the file to copy to. I am using flock for Unix systems. Default for this is \&File::NCopy::f_check. On Unix you can also use \&File::NCopy::unix_check. This one compares the inode and device numbers. set_times is used if the preserve attribute is true. It preserves the access and modification time of the file and also attempts to set the owner of the file to the original owner. This can be useful in a script used by root, though enyone can preserve the access and modification times. This also takes two arguments. The file to get the stats from and apply the stats to. On Unix boxes you shouldn't need to worry. On other system you may want to supply your own sub references. $file = File::NCopy->new(recursive => 1); $file->copy "file","other_file"; $file->copy "directory1","directory2"; $file = File::NCopy->new(u_chmod => \&my_chmod,f_check => \&my_fcheck); $file->copy "directory1","directory2"; =head1 DESCRIPTION B copies files to directories, or a single file to another file. You can also use a reference to a file handle if you wish whem doing a file to file copy. The functionality is very similar to B. If the argument is a directory to directory copy and the recursive flag is set then it is done recursively like B. In fact it behaves like cp on Unix for the most part. If called in array context, an array of successful copies is returned, otherwise the number of succesful copies is returned. If passed a file handle, it's difficult to make sure the file we are copying isn't the same that we are copying to, since by opening the file in write mode it gets pooched. To avoid this use file names instead, if at all possible, especially for the to file. If passed a file handle, it is not closed when copy returns, files opened by copy are closed. =over 4 =item B Copies a file to another file. Or a file to a directory. Or multiple files and directories to another directory. Or a directory to another directory. Wildcard arguments are expanded, except for the last argument which should not be expanded. The file and directory permissions are set to the orginating file's permissions and if preserve is set the access and modification times are also set. If preserve is set then the uid and gid will also be attempted to be set, though this may only for for the men in white hats. In list context it returns all the names of the files/directories that were successfully copied. In scalar context it returns the number of successful copies made. A directory argument is considerd a single successful copy if it manages to copy anything at all. To make a directory to directory copy the recursive flag must be set. =item B Just calls copy. It's there to be compatible with File::Copy. =item B If used then you can treat this as an object oriented module with some configuration abilities. =item B If used as an object then you can use this to set the recursive attribute. It can also be set when instantiating with new. The other attributes must all be set when instantiating the object. If it isn't specified then directories are not followed. =item B Attempt to preserve the last modification and access time as well as user and group id's. This is a useful feature for sysadmins, though the access and modification time should always be preservable, the uid and gid may not. =item B If the link is to a directory and this attribute is true then the directory is followed and recursively copied. Otherwise a link is made to the root directory the link points to. eg. /sys/ is a link to /usr/src/sys/ is a link to /usr/src/i386/sys then the link /sys/ is actually created in the source directory as a link to /usr/src/i386/sys/ rather than /usr/src/sys/ since if the link /usr/src/sys/ is removed then we lost the link even though the directory we originally intended to link to still exists. =item B Force the writing of a file even if the permissions are read only on it. =back =head1 EXAMPLE See SYNOPSIS. =head1 BUGS When following links the target directory might not exactly the same as the source directory. The reason is that we have to make sure we don't follow circular or dead links. This is really a feature though the result may not quite resemble the source dir, the overall content will be the same. :) =head1 AUTHOR Gabor Egressy B Copyright (c) 1998 Gabor Egressy. All rights reserved. All wrongs reversed. This program is free software; you can redistribute and/or modify it under the same terms as Perl itself. Some ideas gleaned from File::Copy by Aaron Sherman & Charles Bailey, but the code was written from scratch. =cut use Cwd (); use strict; use vars qw(@EXPORT_OK @ISA $VERSION); @ISA = qw(Exporter); # we export nothing by default :) @EXPORT_OK = qw(copy cp); $VERSION = '0.32'; # this works on Unix sub u_chmod($$) { my ($file_from,$file_to) = @_; my ($mode) = (stat $file_from)[2]; chmod $mode & 0777,$file_to unless ref $file_to eq 'GLOB' || ref $file_to eq 'FileHandle'; 1; } # this also works on Unix sub f_check($$) { my ($file_from,$file_to) = @_; # get a shared lock on file to copy from flock $file_from,5 or return 0; # try and get an exclusive lock on the file to copy to flock $file_to,6 or do { flock $file_from,8; return 0; }; flock $file_from,8; flock $file_to,8; 1; } # this also works on Unix, it's not the default but you can easily use # it by using the module in an object oriented way # $copy = File::NCopy->new('file_check' => \&File::NCopy::unix_check); sub unix_check($$) { my ($file_from,$file_to) = @_; my ($fdev,$fino) = (stat $file_from)[0,1]; my ($tdev,$tino) = (stat $file_to)[0,1]; return 0 if $fdev == $tdev && $fino == $tino; 1; } sub s_times($$) { my ($file_from,$file_to) = @_; my ($uid,$gid,$atime,$mtime) = (stat $file_from)[4,5,8,9]; utime $atime,$mtime,$file_to unless ref $file_to eq 'GLOB' || ref $file_to eq 'FileHandle'; # this may only work for men in white hats; on Unix chown $uid,$gid,$file_to unless ref $file_to eq 'GLOB' || ref $file_to eq 'FileHandle'; 1; } # all the actual copying is done here, folks ;) sub _docopy_file_file($$$) { my $this = shift; my ($file_from,$file_to) = @_; local (*FILE_FROM,*FILE_TO); my ($was_handle); # did we get a file handle ? unless(ref $file_from eq 'GLOB' || ref $file_from eq 'FileHandle') { open FILE_FROM,"<$file_from" or do { print "*** Couldn\'t open from file <$!> ==> $file_from\n" if $this->{'_debug'}; return 0; }; } else { *FILE_FROM = *$file_from; } unless(ref $file_to eq 'GLOB' || ref $file_to eq 'FileHandle') { # we must open in update mode since on some systems exclusive # locks are only granted to files that are going to be written; open FILE_TO,"+<$file_to" or goto NO_FILE; # no file, so file can't be the same :) } else { *FILE_TO = *$file_to; $was_handle = 1; } unless(-t FILE_FROM || -t FILE_TO) { $this->{'file_check'}->(\*FILE_FROM,\*FILE_TO) or return 0; } NO_FILE: # files aren't the same; now open for writing unless we got a # filehandle if(! $was_handle) { open FILE_TO,">$file_to" or chmod 0644, "$file_to" if $this->{'force_write'}; open FILE_TO,">$file_to" or do { print "*** Couldn\'t open to file <$!> ==> $file_to\n" if $this->{'_debug'}; return 0; }; } # and now for the braindead OS's binmode FILE_FROM; binmode FILE_TO; my $buf = ''; my ($len,$write_n); # read file and write to new file, recover from write errors and # read errors; we accept however much we read and try to write it # 8K is a nice buffer size for most file systems while(1) { $len = sysread(FILE_FROM,$buf,8192); return 0 unless defined $len; last unless $len > 0; while($len) { $write_n = syswrite(FILE_TO,$buf,$len); return 0 unless defined $write_n; $len -= $write_n; } } $this->{'set_permission'}->($file_from,$file_to); $this->{'set_times'}->($file_from,$file_to) if $this->{'preserve'}; # we only close files we opened close FILE_FROM unless ref $file_from eq 'GLOB' || ref $file_from eq 'FileHandle'; close FILE_TO unless ref $file_to eq 'GLOB' || ref $file_to eq 'FileHandle'; print "$file_from ==> $file_to\n" if $this->{'_debug'}; 1; } sub get_path($) { my $dir = shift; my $save_dir = Cwd::cwd; chdir $dir or return undef; $dir = Cwd::cwd; chdir $save_dir; $dir; } sub _recurse_from_dir($$$); # we never actually change the directory :) sub _recurse_from_dir($$$) { my $this = shift; my ($from_dir,$to_dir) = @_; local (*DIR); opendir DIR,$from_dir or do { print "*** Couldn\'t opendir <$!> ==> $from_dir\n" if $this->{'_debug'}; return 0; }; my @files = readdir DIR or do { print "*** Couldn\'t read dir <$!> ==> $from_dir\n" if $this->{'_debug'}; return 0; }; closedir DIR; my $made_dir; unless(-e $to_dir) { mkdir $to_dir,0777 or return 0; $made_dir = 1; } my ($retval,$ret,$link,$save_link); # make sure we don't end up with a recursive, circular link # this isn't totally foolproof, though it does prevent circular # links if($this->{'follow_links'}) { if(defined($save_link = get_path $from_dir)) { $this->{'_links'}->{$save_link} = 1; } } for (@files) { next if /^\.\.?$/; if(-f "$from_dir/$_") { $ret = _docopy_file_file $this, $from_dir . '/' . $_ , $to_dir . '/' . $_; } elsif(-d "$from_dir/$_") { if($this->{'follow_links'} && -l "$from_dir/$_") { $link = get_path "$from_dir/$_"; } if(! -l "$from_dir/$_" || $this->{'follow_links'} && defined $link && ! exists $this->{'_links'}->{$link}) { $ret = _recurse_from_dir $this,$from_dir . '/' . $_ ,$to_dir . '/' . $_; } else { if(defined($link = get_path "$from_dir/$_")) { $ret = symlink $link, "$to_dir/$_"; } } } $retval = $retval || $ret; } if($made_dir) { $this->{'set_permission'}->($from_dir,$to_dir); $this->{'set_times'}->($from_dir,$to_dir) if $this->{'preserve'}; } # remove the name so that there can be link to it from other dirs # that are not subdirs of this one if($this->{'follow_links'}) { delete $this->{'_links'}->{$save_link}; } $retval; } sub _docopy_dir_dir($$$) { my $this = shift; my ($dir_from,$dir_to) = @_; my ($from_name); $dir_from =~ s/\/$//; # remove trailing slash, if any if($dir_from =~ tr/\///) { $from_name = substr $dir_from,rindex($dir_from,'/') + 1; } else { $from_name = $dir_from; if($from_name =~ /^\.\.?$/) { $from_name = ''; } } unless($dir_to =~ /\/$/) { $dir_to .= '/'; } $dir_to .= $from_name; $this->{'_links'} = {}; _recurse_from_dir $this, $dir_from,$dir_to; } sub _docopy_file_dir($$$) { my $this = shift; my ($file,$dir) = @_; my $file_to; if($file =~ tr/\///) { $file_to = substr $file,rindex($file,'/') + 1; } else { $file_to = $file; } $dir =~ s/\/$//; # remove trailing slash _docopy_file_file $this, $file,$dir.'/'.$file_to; } # this just redirects calls, like copy ;) sub _docopy_files_dir($$@) { my $this = shift; my $copies = shift; my $dir = pop; for (@_) { if(-d $_ && $this->{'recursive'}) { _docopy_dir_dir $this, $_, $dir and push @$copies, $_; } elsif(-f $_) { _docopy_file_dir $this, $_, $dir and push @$copies, $_; } } 1; } # does glob work on all systems? sub expand(@) { my @args; return if @_ < 2; for (my $i = 0;$i < $#_;++$i) { push @args,glob $_[$i]; } push @args,$_[$#_]; @args; } sub new(@); # this just redirects calls sub copy(@) { my $this; # were we called through an object reference? if(ref $_[0] eq 'File::NCopy') { $this = shift; } else { # no, so let's make one $this = new File::NCopy; if(ref $_[0] eq 'SCALAR') { my $rec = shift; $this->recursive($$rec); } } my @copies; my @args = expand @_; print "passed args ==> @args\n" if $this->{'_debug'}; # one or more files/directories to a directory if(@args >= 2 && -d $args[$#args]) { _docopy_files_dir $this, \@copies, @args; } # file to file elsif(@args == 2 && -f $args[0]) { _docopy_file_file $this, $args[0],$args[1] and push @copies, $args[0]; } @copies; } sub cp(@) { return copy @_; } # instantiate our object sub new(@) { my $this = shift; my $conf = { 'recursive' => 0, 'preserve' => 0, 'follow_links' => 0, 'force_write' => 0, '_debug' => 0, 'set_permission' => \&File::NCopy::u_chmod, 'file_check' => \&File::NCopy::f_check, 'set_times' => \&File::NCopy::s_times, '_links' => {}, }; my $ref; if(@_ % 2 == 0) { my %ref = @_; $ref = \%ref; } elsif(ref $_[0] eq 'HASH') { $ref = shift; } if(ref $ref eq 'HASH') { $conf->{'recursive'} = abs int $ref->{'recursive'} if defined $ref->{'recursive'}; $conf->{'preserve'} = abs int $ref->{'preserve'} if defined $ref->{'preserve'}; $conf->{'follow_links'} = abs int $ref->{'follow_links'} if defined $ref->{'follow_links'}; $conf->{'force_write'} = abs int $ref->{'force_write'} if defined $ref->{'force_write'}; $conf->{'_debug'} = abs int $ref->{'_debug'} if defined $ref->{'_debug'}; $conf->{'set_permission'} = $ref->{'set_permission'} if defined $ref->{'set_permission'} && ref $ref->{'set_permission'} eq 'CODE'; $conf->{'file_check'} = $ref->{'file_check'} if defined $ref->{'file_check'} && ref $ref->{'file_check'} eq 'CODE'; $conf->{'set_times'} = $ref->{'set_times'} if defined $ref->{'set_times'} && ref $ref->{'set_times'} eq 'CODE'; } bless $conf,$this; } sub recursive($;$) { return if @_ < 1; my $this = shift; return unless ref $this eq 'File::NCopy'; @_ ? $this->{'recursive'} = abs int shift : $this->{'recursive'}; } sub preserve($;$) { return if @_ < 1; my $this = shift; return unless ref $this eq 'File::NCopy'; @_ ? $this->{'preserve'} = abs int shift : $this->{'preserve'}; } sub follow_links($;$) { return if @_ < 1; my $this = shift; return unless ref $this eq 'File::NCopy'; @_ ? $this->{'follow_links'} = abs int shift : $this->{'follow_links'}; } sub force_write($;$) { return if @_ < 1; my $this = shift; return unless ref $this eq 'File::NCopy'; @_ ? $this->{'force_write'} = abs int shift : $this->{'force_write'}; } 1; } # end BEGIN # DO NOT MUCK WITH ANYTHING BELOW THIS LINE! THE CHAOS BEYOND THIS LINE # IS THE COMPRESSED WEBMAIL ARCHIVE (BELIEVE IT OR NOT!) __END__ wm357/ 40755 312 144 0 7245677670 10700 5ustar webmailuserswm357/Mail/ 40700 312 144 0 7245677670 11550 5ustar webmailuserswm357/Mail/Field.pm100755 312 144 22051 7070224304 13235 0ustar webmailusers# Mail::Field.pm # # Copyright (c) 1995-2000 Graham Barr . All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. package Mail::Field; # $Id: //depot/MailTools/Mail/Field.pm#6 $ use Carp; use strict; use vars qw($AUTOLOAD $VERSION); $VERSION = "1.07"; unless(defined &UNIVERSAL::can) { *UNIVERSAL::can = sub { my($obj,$meth) = @_; my $pkg = ref($obj) || $obj; my @pkg = ($pkg); my %done; while(@pkg) { $pkg = shift @pkg; next if exists $done{$pkg}; $done{$pkg} = 1; no strict 'refs'; unshift @pkg,@{$pkg . "::ISA"} if(@{$pkg . "::ISA"}); return \&{$pkg . "::" . $meth} if defined(&{$pkg . "::" . $meth}); } undef; } } sub _header_pkg_name { my($header) = lc shift; $header =~ s/((\b|_)\w)/\U$1/gio; if (length($header) > 8) { my @header = split /[-_]+/, $header; my $chars = int((7 + @header) / @header) || 1; $header = substr(join('', map { substr($_,0,$chars) } @header),0,8); } else { $header =~ s/[-_]+//go; } 'Mail::Field::' . $header; } ## ## Use the import method to load the sub-classes ## sub _require_dir { my($pkg,$dir,$dir_sep) = @_; if(opendir(DIR,$dir)) { my @inc = (); my $f; foreach $f (readdir(DIR)) { next unless $f =~ /^([\w\-]+)/; my $p = lc $1; my $n = $dir . $dir_sep . $p; if(-d $n ) { _require_dir( $pkg . "::" . $f, $n, $dir_sep); } else { $p =~ s/-/_/go; eval "require ${pkg}::$p" } } closedir(DIR); } } sub import { my $pkg = shift; if(@_) { local $_; map { eval "require " . _header_pkg_name($_) || die $@; } @_; } else { my($f,$dir,$dir_sep); foreach $f (keys %INC) { if($f =~ /^Mail(\W)Field\W/i) { $dir_sep = $1; $dir = ($INC{$f} =~ /(.*Mail\W+Field)/i)[0] . $dir_sep; last; } } _require_dir('Mail::Field', $dir, $dir_sep); } } ## ## register a header class, this creates a new method in Mail::Field ## which will call new on that class ## sub register { my $self = shift; my $method = lc shift; my $pkg = shift || ref($self) || $self; $method =~ tr/-/_/; $pkg = _header_pkg_name($method) if($pkg eq "Mail::Field"); croak "Re-register of $method" if Mail::Field->can($method); no strict 'refs'; *{$method} = sub { shift; unless ($pkg->can('stringify')) { eval "require $pkg" || die $@; } $pkg->_build(@_); }; } ## ## the *real* constructor ## if called with one argument then the `parse' method will be called ## otherwise the `create' method is called ## sub _build { my $type = shift; my $self = bless {}, $type; @_ == 1 ? $self->parse(@_) : $self->create(@_); } sub new { my $self = shift; # ignored my $field = lc shift; $field =~ tr/-/_/; $self->$field(@_); } ## ## A default create method. This allows us to do ## $s = Mail::Field->new('Subject', Text => "joe"); ## $s = Mail::Field->new('Subject', "joe"); ## sub create { my $self = shift; my %arg = @_; $self = bless {}, $self unless ref($self); %$self = (); $self->set(\%arg); } ## ## A default create method. This allows us to do ## $s = Mail::Field->new('Subject'); ## sub parse { my $self = shift; my $type = ref($self) || $self; croak "$type: Cannot parse"; } ## ## either get the text, or parse a new one ## sub text { my $self = shift; @_ ? $self->parse(@_) : $self->stringify; } ## ## Return the tag (in the correct case) for this item ## sub tag { my $self = shift; my $tag = ref($self) || $self; $tag =~ s/.*:://o; $tag =~ s/_/-/og; $tag =~ s/\b([a-z]+)/\L\u$1/gio; $tag =~ s/\b([b-df-hj-np-tv-z]+)\b/\U$1/gio; $tag; } ## ## a constructor ## create a new object by extracting from a Mail::Header object ## sub extract { my $self = shift; my $tag = shift; my $head = shift; my $method = lc $tag; $method =~ tr/-/_/; my $text; if(@_ == 0 && wantarray) { my @ret = (); foreach $text ($head->get($tag)) { chomp($text); push(@ret, $self->$method($text)); } return @ret; } my $idx = shift || 0; $text = $head->get($tag,$idx) or return undef; chomp($text); $self->$method($text); } ## ## Autoload sub-classes, or, if the .pm file cannot be found, create a dummy ## sub-class based on Mail::Field::Generic ## sub AUTOLOAD { my $method = $AUTOLOAD; $method =~ s/.*:://o; croak "Undefined subroutine &$AUTOLOAD called" unless $method =~ /^[^A-Z\x00-\x1f\x80-\xff :]+$/o; my $pkg = _header_pkg_name($method); unless(eval "require " . $pkg) { my $tag = $method; $tag =~ s/_/-/og; $tag =~ s/\b([a-z]+)/\L\u$1/gio; $tag =~ s/\b([b-df-hj-np-tv-z]+)\b/\U$1/gio; no strict; @{$pkg . "::ISA"} = qw(Mail::Field::Generic); *{$pkg . "::tag"} = sub { $tag }; } $pkg->register($method) unless(Mail::Field->can($method)); goto &$AUTOLOAD; } ## ## prevent the calling of AUTOLOAD for DESTROY :-) ## sub DESTROY {} ## ## A generic package for those not defined in thier own package. This is ## fine for fields like Subject, X-Mailer etc. where the field holds only ## a string of no particular importance/format. ## package Mail::Field::Generic; use Carp; use vars qw(@ISA); @ISA = qw(Mail::Field); sub create { my $self = shift; my %arg = @_; my $text = delete $arg{Text} || ""; croak "Unknown options " . join(",", keys %arg) if %arg; $self->{Text} = $text; $self; } sub parse { my $self = shift; $self->{Text} = shift || ""; $self; } sub stringify { my $self = shift; $self->{Text}; } 1; __END__ =head1 NAME Mail::Field - Base class for manipulation of mail header fields =head1 SYNOPSIS use Mail::Field; $field = Mail::Field->new('Subject', 'some subject text'); print $field->tag,": ",$field->stringify,"\n"; $field = Mail::Field->subject('some subject text'); =head1 DESCRIPTION C is a base class for packages that create and manipulate fields from Email (and MIME) headers. Each different field will have its own sub-class, defining its own interface. This document describes the minimum interface that each sub-class should provide, and also guidlines on how the field specific interface should be defined. =head1 CONSTRUCTOR Mail::Field, and it's sub-classes define several methods which return new objects. These can all be termed to be constructors. =over 4 =item new ( TAG [, STRING | OPTIONS ] ) The new constructor will create an object in the class which defines the field specified by the tag argument. After creation of the object :- If the tag argument is followed by a single string then the C method will be called with this string. If the tag argument is followed by more than one arguments then the C method will be called with these arguments. =item extract ( TAG, HEAD [, INDEX ] ) This constuctor takes as arguments the tag name, a C object and optionally an index. If the index argument is given then C will retrieve the given tag from the C object and create a new C based object. I will be returned in the field does not exist. If the index argument is not given the the result depends on the context in which C is called. If called in a scalar context the result will be as if C was called with an index value of zero. If called in an array context then all tags will be retrieved and a list of C objects will be returned. =item combine ( FIELD_LIST ) This constructor takes as arguments a list of C objects, which should all be of the same sub-class, and creates a new object in that same class. This constructor is nor defined in C as there is no generic way to combine the various field types. Each sub-class should define its own combine constructor, if combining is possible/allowed. =back =head1 METHODS =over 4 =item parse =item set =item tag =item stringify =back =head1 SUB-CLASS PACKAGE NAMES All sub-classes should be called Mail::Field::I where I is derived from the tag using these rules. =over 4 =item * Consider a tag as being made up of elements separated by '-' =item * Convert all characters to lowercase except the first in each element, which should be uppercase. =item * I is then created from these elements by using the first N characters from each element. =item * N is calculated by using the formula :- int((7 + #elements) / #elements) =item * I is then limited to a maximum of 8 characters, keeping the first 8 characters =back For an example of this take a look at the definition of the C<_header_pkg_name> subroutine in C =head1 AUTHOR Graham Barr =head1 SEE ALSO Ls =head1 CREDITS Eryq - for all the help in defining this package so that Mail::* and MIME::* can be integrated together. =head1 COPYRIGHT Copyright (c) 1995-2000 Graham Barr. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut wm357/Mail/Address.pm100755 312 144 17543 7052453243 13617 0ustar webmailusers# Mail::Address.pm # # Copyright (c) 1995-8 Graham Barr . All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. package Mail::Address; use strict; use Carp; use vars qw($VERSION); use locale; $VERSION = "1.16"; sub Version { $VERSION } # # given a comment, attempt to extract a person's name # sub _extract_name { local $_ = shift || ''; # trim whitespace s/^\s+//; s/\s+$//; s/\s+/ /; # Disregard numeric names (e.g. 123456.1234@compuserve.com) return "" if /^[\d ]+$/; # remove outermost parenthesis s/^\(|\)$//g; # remove outer quotation marks s/^"|"$//g; # remove embedded comments s/\(.*\)//g; # reverse "Last, First M." if applicable s/^([^\s]+) ?, ?(.*)$/$2 $1/; s/,.*//; # Set the case of the name to first char upper rest lower # Upcase first letter on name s/\b(\w+)/\L\u$1/igo; # Scottish names such as 'McLeod' s/\bMc(\w)/Mc\u$1/igo; # Irish names such as 'O'Malley, O'Reilly' s/\bo'(\w)/O'\u$1/igo; # Roman numerals, eg 'Level III Support' s/\b(x*(ix)?v*(iv)?i*)\b/\U$1/igo; # some cleanup s/\[[^\]]*\]//g; s/(^[\s'"]+|[\s'"]+$)//g; s/\s{2,}/ /g; return $_; } sub _tokenise { local($_) = join(',', @_); my(@words,$snippet,$field); s/\A\s+//; s/[\r\n]+/ /g; while ($_ ne '') { $field = ''; if( s/^\s*\(/(/ ) # (...) { my $depth = 0; PAREN: while(s/^(\(([^\(\)\\]|\\.)*)//) { $field .= $1; $depth++; while(s/^(([^\(\)\\]|\\.)*\)\s*)//) { $field .= $1; last PAREN unless --$depth; $field .= $1 if s/^(([^\(\)\\]|\\.)+)//; } } carp "Unmatched () '$field' '$_'" if $depth; $field =~ s/\s+\Z//; push(@words, $field); next; } s/^("([^"\\]|\\.)*")\s*// # "..." || s/^(\[([^\]\\]|\\.)*\])\s*// # [...] || s/^([^\s\Q()<>\@,;:\\".[]\E]+)\s*// || s/^([\Q()<>\@,;:\\".[]\E])\s*// and do { push(@words, $1); next; }; croak "Unrecognised line: $_"; } push(@words, ","); \@words; } sub _find_next { my $idx = shift; my $tokens = shift; my $len = shift; while($idx < $len) { my $c = $tokens->[$idx]; return $c if($c eq "," || $c eq "<"); $idx++; } return ""; } sub _complete { my $pkg = shift; my $phrase = shift; my $address = shift; my $comment = shift; my $o = undef; if(@{$phrase} || @{$comment} || @{$address}) { $o = $pkg->new(join(" ",@{$phrase}), join("", @{$address}), join(" ",@{$comment})); @{$phrase} = (); @{$address} = (); @{$comment} = (); } return $o; } sub new { my $pkg = shift; my $me = bless [@_], $pkg; return $me; } sub parse { my $pkg = shift; local $_; my @phrase = (); my @comment = (); my @address = (); my @objs = (); my $depth = 0; my $idx = 0; my $tokens = _tokenise(grep { defined $_} @_); my $len = scalar(@{$tokens}); my $next = _find_next($idx,$tokens,$len); for( ; $idx < $len ; $idx++) { $_ = $tokens->[$idx]; if(substr($_,0,1) eq "(") { push(@comment,$_); } elsif($_ eq '<') { $depth++; } elsif($_ eq '>') { $depth-- if($depth); unless($depth) { my $o = _complete($pkg,\@phrase, \@address, \@comment); push(@objs, $o) if(defined $o); $depth = 0; $next = _find_next($idx,$tokens,$len); } } elsif($_ eq ',') { warn "Unmatched '<>'" if($depth); my $o = _complete($pkg,\@phrase, \@address, \@comment); push(@objs, $o) if(defined $o); $depth = 0; $next = _find_next($idx+1,$tokens,$len); } elsif($depth) { push(@address,$_); } elsif($next eq "<") { push(@phrase,$_); } elsif($_ =~ /\A[\Q.\@:;\E]\Z/ || !scalar(@address) || $address[$#address] =~ /\A[\Q.\@:;\E]\Z/) { push(@address,$_); } else { warn "Unmatched '<>'" if($depth); my $o = _complete($pkg,\@phrase, \@address, \@comment); push(@objs, $o) if(defined $o); $depth = 0; push(@address,$_); } } @objs; } sub set_or_get { my $me = shift; my $i = shift; my $val = $me->[$i]; $me->[$i] = shift if(@_); $val; } sub phrase { set_or_get(shift,0,@_) } sub address { set_or_get(shift,1,@_) } sub comment { set_or_get(shift,2,@_) } sub format { my @fmts = (); my $me; foreach $me (@_) { my($phrase,$addr,$comment) = @{$me}; my @tmp = (); if(defined $phrase && length($phrase)) { push(@tmp, $phrase); push(@tmp, "<" . $addr . ">") if(defined $addr && length($addr)); } else { push(@tmp, $addr) if(defined $addr && length($addr)); } if(defined($comment) && $comment =~ /\S/) { $comment =~ s/^\s*\(?/(/; $comment =~ s/\)?\s*$/)/; } push(@tmp, $comment) if(defined $comment && length($comment)); push(@fmts, join(" ", @tmp)) if(scalar(@tmp)); } return join(", ", @fmts); } sub name { my $me = shift; my $phrase = $me->phrase; my $addr = $me->address; $phrase = $me->comment unless(defined($phrase) && length($phrase)); my $name = _extract_name($phrase); # first.last@domain address if($name eq '' && $addr =~ /([^\%\.\@\_]+([\.\_][^\%\.\@\_]+)+)[\@\%]/o) { ($name = $1) =~ s/[\.\_]+/ /go; $name = _extract_name($name); } if($name eq '' && $addr =~ m#/g=#oi) # X400 style address { my ($f) = $addr =~ m#g=([^/]*)#oi; my ($l) = $addr =~ m#s=([^/]*)#io; $name = _extract_name($f . " " . $l); } return length($name) ? $name : undef; } sub host { my $me = shift; my $addr = $me->address; my $i = rindex($addr,'@'); my $host = ($i >= 0) ? substr($addr,$i+1) : undef; return $host; } sub user { my $me = shift; my $addr = $me->address; my $i = index($addr,'@'); my $user = ($i >= 0) ? substr($addr,0,$i) : $addr; return $user; } sub path { return (); } sub canon { my $me = shift; return ($me->host, $me->user, $me->path); } 1; __END__ =head1 NAME Mail::Address - Parse mail addresses =head1 SYNOPSIS use Mail::Address; my @addrs = Mail::Address->parse($line); foreach $addr (@addrs) { print $addr->format,"\n"; } =head1 DESCRIPTION C extracts and manipulates RFC822 compilant email addresses. As well as being able to create C objects in the normal manner, C can extract addresses from the To and Cc lines found in an email message. =head1 CONSTRUCTORS =over 4 =item new( PHRASE, ADDRESS, [ COMMENT ]) Mail::Address->new("Perl5 Porters", "perl5-porters@africa.nicoh.com"); Create a new C object which represents an address with the elements given. In a message these 3 elements would be seen like: PHRASE
(COMMENT) ADDRESS (COMMENT) =item parse( LINE ) Mail::Address->parse($line); Parse the given line a return a list of extracted C objects. The line would normally be one taken from a To,Cc or Bcc line in a message =back =head1 METHODS =over 4 =item phrase () Return the phrase part of the object. =item address () Return the address part of the object. =item comment () Return the comment part of the object =item format () Return a string representing the address in a suitable form to be placed on a To,Cc or Bcc line of a message =item name () Using the information contained within the object attempt to identify what the person or groups name is =item host () Return the address excluding the user id and '@' =item user () Return the address excluding the '@' and the mail domain =item path () Unimplemented yet but should return the UUCP path for the message =item canon () Unimplemented yet but should return the UUCP canon for the message =back =head1 AUTHOR Graham Barr =head1 COPYRIGHT Copyright (c) 1995-8 Graham Barr. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut wm357/Mail/Filter.pm100755 312 144 7376 6700450775 13450 0ustar webmailusers# Mail::Filter.pm # # Copyright (c) 1997 Graham Barr . All rights # reserved. This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. package Mail::Filter; use Carp; use strict; use vars qw($VERSION); $VERSION = "1.01"; sub new { my $self = shift; bless { filters => [ @_ ] }, $self; } sub add { my $self = shift; push(@{$self->{'filters'}}, @_); } sub _filter { my $self = shift; my $mail = shift; my $sub; foreach $sub (@{$self->{'filters'}}) { if(ref($sub) eq "CODE") { $mail = $sub->($self,$mail); } elsif(!ref($sub)) { $mail = $self->$sub($mail); } else { carp "Cannot call filter '$sub', ignored"; } last unless ref($mail); } # the specification indicates that the result of operations on $mail # should be returned by this function return $mail; } sub filter { my $self = shift; my $obj = shift; if($obj->isa('Mail::Folder')) { $self->{'folder'} = $obj; my $m; foreach $m ($obj->message_list) { my $mail = $obj->get_message($m) || next; $self->{'msgnum'} = $m; _filter($self,$mail); } delete $self->{'folder'}; delete $self->{'msgnum'}; } elsif($obj->isa('Mail::Internet')) { return _filter($self,$obj); } else { carp "Cannot process '$obj'"; return undef; } } sub folder { my $self = shift; exists $self->{'folder'} ? $self->{'folder'} : undef; } sub msgnum { my $self = shift; exists $self->{'msgnum'} ? $self->{'msgnum'} : undef; } 1; __END__ =head1 NAME Mail::Filter - Filter mail through multiple subroutines =head1 SYNOPSIS use Mail::Filter; $filter = new Mail::Filter( \&filter1, \&filter2 ); $mail = new Mail::Internet( [<>] ); $mail = $filter->filter($mail); $folder = new Mail::Folder( .... ); $filter->filter($folder); =head1 DESCRIPTION C provides an interface to filtering Email through multiple subroutines. C filters mail by calling each filter subroutine in turn. Each filter subroutine is called with two arguments, the first is the filter object and the second is the mail or folder object being filtered. The result from each filter sub is passed to the next filter as the mail object. If a filter subroutine returns undef, then C will abort and return immediately. The function returns the result from the last subroutine to operate on the mail object. =head1 CONSTRUCTOR =over 4 =item new ( [ FILTER [, ... ]]) Create a new C object with the given filter subroutines. Each filter may be either a code reference or the name of a method to call on the object. =back =head1 METHODS =over 4 =item add ( FILTER [, FILTER ...] ) Add the given filters to the end of the fliter list. =item filter ( MAIL-OBJECT | MAIL-FOLDER ) If the first argument is a C object, then this object will be passed through the filter list. If the first argument is a C object, then each message in turn will be passed through the filter list. =item folder If the C method is called with a C object, then the filter subroutines may call this method to obtain the folder object that is being processed. =item msgnum If the C method is called with a C object, then the filter subroutines may call this method to obtain the message number of the message that is being processed. =back =head1 SEE ALSO L L =head1 AUTHOR Graham Barr EFE =head1 COPYRIGHT Copyright (c) 1997 Graham Barr. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut wm357/Mail/Mailer/ 40755 312 144 0 7245677667 13001 5ustar webmailuserswm357/Mail/Mailer/test.pm100755 312 144 370 6556413311 14350 0ustar webmailuserspackage Mail::Mailer::test; use vars qw(@ISA); require Mail::Mailer::rfc822; @ISA = qw(Mail::Mailer::rfc822); sub can_cc { 0 } sub exec { my($self, $exe, $args, $to) = @_; exec('sh', '-c', "echo to: " . join(" ",@{$to}) . "; cat"); } 1; wm357/Mail/Mailer/mail.pm100755 312 144 745 6556413311 14321 0ustar webmailuserspackage Mail::Mailer::mail; use vars qw(@ISA); @ISA = qw(Mail::Mailer); my %hdrs = qw(Cc ~c Bcc ~b Subject ~s); sub set_headers { my $self = shift; my $hdrs = shift; my($k,$v); while(($k,$v) = each %hdrs) { print $self join(" ",$v, $self->to_array($hdrs->{$k})), "\n" if defined $hdrs->{$k}; } } sub exec { open(STDOUT,">/dev/null"); # this is not portable !!!! open(STDERR,">/dev/null"); # this is not portable !!!! shift->SUPER::exec(@_); } 1; wm357/Mail/Mailer/smtp.pm100755 312 144 2605 7070226403 14373 0ustar webmailuserspackage Mail::Mailer::smtp; use vars qw(@ISA); use Net::SMTP; use Mail::Util qw(mailaddress); require Mail::Mailer::rfc822; @ISA = qw(Mail::Mailer::rfc822); sub can_cc { 0 } sub exec { my($self, $exe, $args, $to) = @_; my %opt = @$args; my $host = $opt{'Server'} || undef; # for Net::SMTP we do not really exec my $smtp = Net::SMTP->new($host, Debug => 0) or return undef; ${*$self}{'sock'} = $smtp; $smtp->mail(mailaddress()); my $u; foreach $u (@$to) { $smtp->to($u); } $smtp->data; untie(*$self) if tied *$self; tie *$self, 'Mail::Mailer::smtp::pipe',$self; $self; } sub set_headers { my($self,$hdrs) = @_; $self->SUPER::set_headers({ From => "<" . mailaddress() . ">", %$hdrs, 'X-Mailer' => "Mail::Mailer[v$Mail::Mailer::VERSION] Net::SMTP[v$Net::SMTP::VERSION]" }) } sub epilogue { my $self = shift; my $sock = ${*$self}{'sock'}; $sock->dataend; $sock->quit; delete ${*$self}{'sock'}; untie(*$self); } sub close { my($self, @to) = @_; my $sock = ${*$self}{'sock'}; if ($sock && fileno($sock)) { $self->epilogue; close($sock); } } package Mail::Mailer::smtp::pipe; sub TIEHANDLE { my $pkg = shift; my $self = shift; my $sock = ${*$self}{'sock'}; return bless \$sock; } sub PRINT { my $self = shift; my $sock = $$self; $sock->datasend( @_ ); } 1; wm357/Mail/Mailer/sendmail.pm100755 312 144 1053 7070230022 15170 0ustar webmailuserspackage Mail::Mailer::sendmail; use vars qw(@ISA); require Mail::Mailer::rfc822; @ISA = qw(Mail::Mailer::rfc822); sub exec { my($self, $exe, $args, $to) = @_; # Fork and exec the mailer (no shell involved to avoid risks) # We should always use a -t on sendmail so that Cc: and Bcc: work # Rumor: some sendmails may ignore or break with -t (AIX?) # Chopped out the @$to arguments, because -t means # they are sent in the body, and postfix complains if they # are also given on comand line. exec( $exe, '-t', @$args ); } wm357/Mail/Mailer/rfc822.pm100755 312 144 510 6556413311 14373 0ustar webmailuserspackage Mail::Mailer::rfc822; use vars qw(@ISA); @ISA = qw(Mail::Mailer); sub set_headers { my $self = shift; my $hdrs = shift; local($\)=""; foreach(keys %$hdrs) { next unless m/^[A-Z]/; print $self "$_: ", join(",", $self->to_array($hdrs->{$_})), "\n"; } print $self "\n"; # terminate headers } 1; wm357/Mail/Internet.pm100755 312 144 42250 7125655356 14025 0ustar webmailusers# Mail::Internet.pm # # Copyright (c) 1995-8 Graham Barr . All rights # reserved. This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # package Mail::Internet; use strict; require 5.002; use Carp; #use AutoLoader; # mjm use Mail::Header; use vars qw($VERSION); BEGIN { $VERSION = "1.32"; # mjm *AUTOLOAD = \&AutoLoader::AUTOLOAD; unless(defined &UNIVERSAL::isa) { *UNIVERSAL::isa = sub { my($obj,$type) = @_; my $pkg = ref($obj) || $obj; my @pkg = ($pkg); my %done; while(@pkg) { $pkg = shift @pkg; return 1 if $pkg eq $type; next if exists $done{$pkg}; $done{$pkg} = 1; no strict 'refs'; unshift @pkg,@{$pkg . "::ISA"} if(@{$pkg . "::ISA"}); } undef; } } } sub new { my $self = shift; my $type = ref($self) || $self; my $arg = @_ % 2 ? shift : undef; my %arg = @_; my $me = bless {}, $type; $me->{'mail_inet_head'} = $arg{Header} if exists $arg{Header}; $me->{'mail_inet_body'} = $arg{Body} if exists $arg{Body}; $me->head->fold_length(delete $arg{FoldLength} || 79); # Default fold length $me->head->mail_from($arg{MailFrom}) if exists $arg{MailFrom}; $me->head->modify(exists $arg{Modify} ? $arg{Modify} : 1); if(defined $arg) { if(ref($arg) eq 'ARRAY') { $me->header($arg) unless exists $arg{Header}; $me->body($arg) unless exists $arg{Body}; } elsif(defined fileno($arg)) { $me->read_header($arg) unless exists $arg{Header}; $me->read_body($arg) unless exists $arg{Body}; } } return $me; } sub read { my $me = shift; $me->read_header(@_); $me->read_body(@_); } sub read_body { my($me,$fd) = @_; $me->body( [ <$fd> ] ); } sub extract { my $me = shift; my $arg = shift; $me->head->extract($arg); $me->body($arg); } sub body { my $me = shift; my $body = $me->{'mail_inet_body'} ||= []; if(@_) { my $new = shift; $me->{'mail_inet_body'} = ref($new) eq 'ARRAY' ? $new : [ $new ]; } return $body; } sub header { shift->head->header(@_) } sub fold { shift->head->fold(@_) } sub fold_length { shift->head->fold_length(@_) } sub combine { shift->head->combine(@_) } sub print_header { shift->{'mail_inet_head'}->print(@_) } sub head { shift->{'mail_inet_head'} ||= new Mail::Header } sub read_header { my $me = shift; my $head = $me->head; $head->read(@_); $head->header(); } sub clean_header { carp "clean_header depreciated, use ->header" if $^W; shift->header(); } sub tidy_headers { carp "tidy_headers no longer required" if $^W; } sub add { my $me = shift; my $head = $me->head; my $ret; while(@_) { my ($tag,$line) = splice(@_,0,2); $ret = $head->add($tag,$line,-1) or return undef; } $ret; } sub replace { my $me = shift; my $head = $me->head; my $ret; while(@_) { my ($tag,$line) = splice(@_,0,2); $ret = $head->replace($tag,$line,0) or return undef; } $ret; } sub get { my $me = shift; my $head = $me->head; my @ret = (); my $tag; foreach $tag (@_) { last if push(@ret, $head->get($tag)) && !wantarray; } wantarray ? @ret : shift @ret; } sub delete { my $me = shift; my $head = $me->head; my @ret = (); my $tag; foreach $tag (@_) { push(@ret, $head->delete($tag)); } @ret; } sub dup { my $me = shift; my $type = ref($me); my $dup = $type->new; $dup->{'mail_inet_body'} = [@{$me->body}] if exists $me->{'mail_inet_body'}; $dup->{'mail_inet_head'} = $me->{'mail_inet_head'}->dup if exists $me->{'mail_inet_head'}; $dup; } sub empty { my $me = shift; %{*$me} = (); 1; } sub print_body { my $me = shift; my $fd = shift || \*STDOUT; my $ln; foreach $ln (@{$me->body}) { print $fd $ln or return 0; } 1; } sub print { my $me = shift; my $fd = shift || \*STDOUT; $me->print_header($fd) and print $fd "\n" and $me->print_body($fd); } sub as_string { my $me = shift; $me->head->as_string . "\n" . join '', @{ $me->body }; } sub as_mbox_string { my $me = shift->dup; my $escaped = shift; $me->head->delete('Content-Length'); $me->escape_from unless $escaped; $me->as_string . "\n"; } sub remove_sig { my $me = shift; my $nlines = shift || 10; my $body = $me->body; my($line,$i); $line = scalar(@{$body}); return unless($line); while($i++ < $nlines && $line--) { if($body->[$line] =~ /\A--\040?[\r\n]+/) { splice(@{$body},$line,$i); last; } } } sub tidy_body { my $me = shift; my $body = $me->body; my $line; if(scalar(@{$body})) { shift @$body while(scalar(@{$body}) && $body->[0] =~ /\A\s*\Z/); pop @$body while(scalar(@{$body}) && $body->[-1] =~ /\A\s*\Z/); } return $body; } sub DESTROY {} # Auto loaded methods go after __END__ __END__ sub reply; use Mail::Address; sub reply { my $me = shift; my %arg = @_; my $pkg = ref $me; my @reply = (); local *MAILHDR; if(open(MAILHDR,"$ENV{HOME}/.mailhdr")) { # User has defined a mail header template @reply = ; close(MAILHDR); } my $reply = $pkg->new(\@reply); my($to,$cc,$name,$body,$id); # The Subject line my $subject = $me->get('Subject') || ""; $subject = "Re: " . $subject if($subject =~ /\S+/ && $subject !~ /Re:/i); $reply->replace('Subject',$subject); # Locate who we are sending to $to = $me->get('Reply-To') || $me->get('From') || $me->get('Return-Path') || ""; # Mail::Address->parse returns a list of refs to a 2 element array my $sender = (Mail::Address->parse($to))[0]; $name = $sender->name; $id = $sender->address; unless(defined $name) { my $fr = $me->get('From'); $fr = (Mail::Address->parse($fr))[0] if(defined $fr); $name = $fr->name if(defined $fr); } my $indent = $arg{Indent} || ">"; if($indent =~ /%/) { my %hash = ( '%' => '%'); my @name = grep(do { length > 0 }, split(/[\n\s]+/,$name || "")); my @tmp; @name = "" unless(@name); $hash{f} = $name[0]; $hash{F} = $#name ? substr($hash{f},0,1) : $hash{f}; $hash{l} = $#name ? $name[$#name] : ""; $hash{L} = substr($hash{l},0,1) || ""; $hash{n} = $name || ""; $hash{I} = join("",grep($_ = substr($_,0,1), @tmp = @name)); $indent =~ s/%(.)/defined $hash{$1} ? $hash{$1} : $1/eg; } $reply->replace('To', $id); # Find addresses not to include my %nocc = (); my $mailaddresses = $ENV{MAILADDRESSES} || ""; my $addr; $nocc{lc $id} = 1; foreach $addr (Mail::Address->parse($reply->get('Bcc'),$mailaddresses)) { my $lc = lc $addr->address; $nocc{$lc} = 1; } if($arg{ReplyAll} || 0) { # Who shall we copy this to my %cc = (); foreach $addr (Mail::Address->parse($me->get('To'),$me->get('Cc'))) { my $lc = lc $addr->address; $cc{$lc} = $addr->format unless(defined $nocc{$lc}); } $cc = join(', ',values %cc); $reply->replace('Cc', $cc); } # References my $refs = $me->get('References') || ""; my $mid = $me->get('Message-Id'); $refs .= " " . $mid if(defined $mid); $reply->replace('References',$refs); # In-Reply-To my $date = $me->get('Date'); my $inreply = ""; if(defined $mid) { $inreply = $mid; $inreply .= " from " . $name if(defined $name); $inreply .= " on " . $date if(defined $date); } elsif(defined $name) { $inreply = $name . "'s message"; $inreply .= "of " . $date if(defined $date); } $reply->replace('In-Reply-To', $inreply); # Quote the body $body = $reply->body; @$body = @{$me->body}; # copy body $reply->remove_sig; # remove signature, if any $reply->tidy_body; # tidy up map { s/\A/$indent/ } @$body; # indent # Add references unshift @{$body}, (defined $name ? $name . " " : "") . "<$id> writes:\n"; if(defined $arg{Keep} && 'ARRAY' eq ref($arg{Keep})) { # Copy lines from the original my $keep; foreach $keep (@{$arg{Keep}}) { my $ln = $me->get($keep); $reply->replace($keep,$ln) if(defined $ln); } } if(defined $arg{Exclude} && 'ARRAY' eq ref($arg{Exclude})) { # Exclude lines $reply->delete(@{$arg{Exclude}}); } # remove empty header lins $reply->head->cleanup; $reply; } sub add_signature { my $me = shift; carp "add_signature depriciated, use ->sign" if $^W; $me->sign(File => shift || "$ENV{HOME}/.signature"); } sub sign { my $me = shift; my %arg = @_; my $sig; my @sig; if($sig = delete $arg{File}) { local *SIG; if(open(SIG,$sig)) { local $_; while() { last unless /\A(--)?\s*\Z/; } @sig = ($_,,"\n"); close(SIG); } } elsif($sig = delete $arg{Signature}) { @sig = ref($sig) ? @$sig : split(/\n/, $sig); } if(@sig) { $me->remove_sig; map(s/\n?\Z/\n/,@sig); push(@{$me->body}, "-- \n",@sig); } } sub _prephdr { use Mail::Util; my $hdr = shift; $hdr->delete('From '); # Just in case :-) # An original message should not have any Received lines $hdr->delete('Received'); $hdr->replace('X-Mailer', "Perl5 Mail::Internet v" . $Mail::Internet::VERSION); my $name = eval { (getpwuid($>))[6] } || $ENV{NAME} || ""; while($name =~ s/\([^\(\)]*\)//) { 1; } if($name =~ /[^\w\s]/) { $name =~ s/"/\"/g; $name = '"' . $name . '"'; } my $from = sprintf "%s <%s>", $name, Mail::Util::mailaddress(); $from =~ s/\s{2,}/ /g; my $tag; foreach $tag (qw(From Sender)) { $hdr->add($tag,$from) unless($hdr->get($tag)); } } sub smtpsend; use Carp; use Mail::Util qw(mailaddress); use Mail::Address; use Net::Domain qw(hostname); use Net::SMTP; use strict; sub smtpsend { my $src = shift; my %opt = @_; my $host = $opt{Host}; my $noquit = 0; my $smtp; my @hello = defined $opt{Hello} ? (Hello => $opt{Hello}) : (); unless(defined($host)) { my @hosts = qw(mailhost localhost); unshift(@hosts, split(/:/, $ENV{SMTPHOSTS})) if(defined $ENV{SMTPHOSTS}); foreach $host (@hosts) { $smtp = eval { Net::SMTP->new($host, @hello) }; last if(defined $smtp); } } elsif(ref($host) && UNIVERSAL::isa($host,'Net::SMTP')) { $smtp = $host; $noquit = 1; } else { $smtp = eval { Net::SMTP->new($host, @hello) }; } return () unless(defined $smtp); my $hdr = $src->head->dup; _prephdr($hdr); # Who is it to my @rcpt = map { ref($_) ? @$_ : $_ } grep { defined } @opt{'To','Cc','Bcc'}; @rcpt = map { $hdr->get($_) } qw(To Cc Bcc) unless @rcpt; my @addr = map($_->address, Mail::Address->parse(@rcpt)); return () unless(@addr); $hdr->delete('Bcc'); # Remove blind Cc's # Send it my $ok = $smtp->mail( mailaddress() ) && $smtp->to(@addr) && $smtp->data(join("", @{$hdr->header},"\n",@{$src->body})); $smtp->quit unless $noquit; $ok ? @addr : (); } sub send; use Mail::Mailer; use strict; sub send { my ($src, $type, @args) = @_; my $hdr = $src->head->dup; _prephdr($hdr); my $headers = $hdr->header_hashref; # Actually send it my $mailer = Mail::Mailer->new($type, @args); $mailer->open($headers); $src->print_body($mailer); $mailer->close(); } sub nntppost; use Mail::Util qw(mailaddress); use Net::NNTP; use strict; sub nntppost { my $mail = shift; my %opt = @_; my $groups = $mail->get('Newsgroups') || ""; my @groups = split(/[\s,]+/,$groups); return () unless @groups; my $hdr = $mail->head->dup; _prephdr($hdr); # Remove these incase the NNTP host decides to mail as well as me $hdr->delete(qw(To Cc Bcc)); my $news; my $noquit = 0; my $host = $opt{Host}; if(ref($host) && UNIVERSAL::isa($host,'Net::NNTP')) { $news = $host; $noquit = 1; } else { my @opt = (); push(@opt, $opt{'Host'}); push(@opt, 'Port', $opt{'Port'}) if exists $opt{'Port'}; push(@opt, 'Debug', $opt{'Debug'}) if exists $opt{'Debug'}; $news = new Net::NNTP(@opt) or return (); } $news->post(@{$hdr->header},"\n",@{$mail->body}); my $code = $news->code; $news->quit unless $noquit; return 240 == $code ? @groups : (); } sub escape_from { my $me = shift; my $body = $me->body; local $_; scalar grep { s/\A(>*From) />$1 /o } @$body; } sub unescape_from { my $me = shift; my $body = $me->body; local $_; scalar grep { s/\A>(>*From) /$1 /o } @$body; } 1; # keep require happy =head1 NAME Mail::Internet - manipulate Internet format (RFC 822) mail messages =head1 SYNOPSIS use Mail::Internet; =head1 DESCRIPTION This package provides a class object which can be used for reading, creating, manipulating and writing a message with RFC822 compliant headers. =head1 CONSTRUCTOR =over 4 =item new ( [ ARG ], [ OPTIONS ] ) C is optiona and may be either a file descriptor (reference to a GLOB) or a reference to an array. If given the new object will be initialized with headers and body either from the array of read from the file descriptor. C is a list of options given in the form of key-value pairs, just like a hash table. Valid options are =over 8 =item B
The value of this option should be a C object. If given then C will not attempt to read a mail header from C, if it was specified. =item B The value of this option should be a reference to an array which contains the lines for the body of the message. Each line should be terminated with C<\n> (LF). If Body is given then C will not attempt to read the body from C (even if it is specified). =back The Mail::Header options C, C and C may also be given. =back =head1 METHODS =over 4 =item body () Returns the body of the message. This is a reference to an array. Each entry in the array represents a single line in the message. =item print_header ( [ FILEHANDLE ] ) =item print_body ( [ FILEHANDLE ] ) =item print ( [ FILEHANDLE ] ) Print the header, body or whole message to file descriptor I. I<$fd> should be a reference to a GLOB. If I is not given the output will be sent to STDOUT. $mail->print( \*STDOUT ); # Print message to STDOUT =item as_string () Returns the message as a single string. =item as_mbox_string ( [ ALREADY_ESCAPED ] ) Returns the message as a string in mbox format. C, if given and true, indicates that ->escape_from has already been called on this object. =item head () Returns the C object which holds the headers for the current message =back =head1 UTILITY METHODS The following methods are more a utility type than a manipulation type of method. =over 4 =item remove_sig ( [ NLINES ] ) Attempts to remove a users signature from the body of a message. It does this by looking for a line equal to C<'-- '> within the last C of the message. If found then that line and all lines after it will be removed. If C is not given a default value of 10 will be used. This would be of most use in auto-reply scripts. =item tidy_body () Removes all leading and trailing lines from the body that only contain white spaces. =item reply () Create a new object with header initialised for a reply to the current object. And the body will be a copy of the current message indented. =item add_signature ( [ FILE ] ) Append a signature to the message. C is a file which contains the signature, if not given then the file "$ENV{HOME}/.signature" will be checked for. =item send ( [ type [ args.. ]] ) Send a Mail::Internet message using Mail::Mailer. Type and args are passed on to C =item smtpsend ( [ OPTIONS ] ) Send a Mail::Internet message via SMTP, requires Net::SMTP The return value will be a list of email addresses that the message was sent to. If the message was not sent the list will be empty. Options are passed as key-value pairs. Current options are =over 4 =item Host Name of the SMTP server to connect to, or a Net::SMTP object to use If C is not given then the SMTP host is found by attempting connections first to hosts specified in C<$ENV{SMTPHOSTS}>, a colon separated list, then C and C. =item To =item Cc =item Bcc Send the email to the given addresses, each can be either a string or a reference to a list of email addresses. If none of C, or C are given then the addresses are extracted from the message being sent. =item Hello Send a HELO (or EHLO) command to the server with the given name. =back =item nntppost ( [ OPTIONS ] ) Post an article via NNTP, requires Net::NNTP. Options are passed as key-value pairs. Current options are =over 4 =item Host Name of NNTP server to connect to, or a Net::NNTP object to use. =item Port Port number to connect to on remote host =item Debug Debug value to pass to Net::NNTP, see =back =item escape_from () It can cause problems with some applications if a message contains a line starting with C<`From '>, in particular when attempting to split a folder. This method inserts a leading C<`>'> on anyline that matches the regular expression C*From/> =item unescape_from () This method will remove the escaping added by escape_from =back =head1 SEE ALSO L L =head1 AUTHOR Graham Barr =head1 COPYRIGHT Copyright (c) 1995-7 Graham Barr. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut wm357/Mail/Header.pm100755 312 144 47323 7070136474 13425 0ustar webmailusers# Mail::Header.pm # # Copyright (c) 1995-7 Graham Barr . All rights reserved. # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # # The internals of this package are implemented in terms of a list of lines # and a hash indexed by the tags. The hash contains a list of references to # the actual SV's in the list. We therefore do our upmost to preserve this. # anyone who delves into these structures deserve all they get. # package Mail::Header; require 5.002; use strict; use Carp; use vars qw($VERSION $FIELD_NAME); $VERSION = "1.17"; my $MAIL_FROM = 'KEEP'; my %HDR_LENGTHS = (); # # Pattern to match a RFC822 Field name ( Extract from RFC #822) # # field = field-name ":" [ field-body ] CRLF # # field-name = 1* # # CHAR = ; ( 0-177, 0.-127.) # CTL = ; ( 177, 127.) # I have included the trailing ':' in the field-name # $FIELD_NAME = '[^\x00-\x1f\x7f-\xff :]+:'; ## ## Private functions ## sub _error { warn @_; return (wantarray ? () : undef) } # tidy up internal hash table and list sub _tidy_header { my $me = shift; my($ref,$key); my $i; my $d = 0; for($i = 0 ; $i < scalar(@{$me->{'mail_hdr_list'}}) ; $i++) { unless(defined $me->{'mail_hdr_list'}[$i]) { splice(@{$me->{'mail_hdr_list'}},$i,1); $d++; $i--; } } if($d) { local $_; my @del = (); while(($key,$ref) = each %{$me->{'mail_hdr_hash'}} ) { push(@del, $key) unless @$ref = grep { ref($_) && defined $$_ } @$ref; } map { delete $me->{'mail_hdr_hash'}{$_} } @del; } } # fold the line to the given length sub _fold_line { my($ln,$maxlen) = @_; return if $_[0] =~ /^X-Face:/io; $maxlen = 20 if($maxlen < 20); my $max = int($maxlen - 5); # 4 for leading spcs + 1 for [\,\;] my $min = int($maxlen * 4 / 5) - 4; my $ml = $maxlen; $_[0] =~ s/\s*[\r\n]+\s*/ /og; # Compress any white space around a newline $_[0] =~ s/\s*\Z/\n/so; # End line with a EOLN return if $_[0] =~ /^From\s/io; if(length($_[0]) > $ml) { #Split the line up # first bias towards splitting at a , or a ; >4/5 along the line # next split a whitespace # else we are looking at a single word and probably don't want to split my $x = ""; $x .= $2 . "\n " while($_[0] =~ s/^(\s*( [^"]{$min,$max}?[\,\;] |[^"]{1,$max}[\s\n] |[^\s"]+[\s\n] |([^\s"]+("[^"]*")?)+[\s\n] )) //x); $x .= $_[0]; $_[0] = $x; $_[0] =~ s/(\A\s+|[\t ]+\Z)//sog; $_[0] =~ s/\s+\n/\n/sog; } $_[0] =~ s/\A(\S+)\n\s*(?=\S)/$1 /so; } # attempt to change the case of a tag to that required by RFC822. That # being all characters are lowercase except the first of each word. Also # if the word is an `acronym' then all characters are uppercase. We decide # a word is an acronym if it does not contain a vowel. sub _tag_case { my $tag = shift; $tag =~ s/:\Z//o; # Change the case of the tag # eq Message-Id $tag =~ s/\b([a-z]+)/\L\u$1/gio; $tag =~ s/\b([b-df-hj-np-tv-z]+)\b/\U$1/gio if $tag =~ /-/; $tag; } # format a complete line # ensure line starts with the given tag # ensure tag is correct case # change the 'From ' tag as required # fold the line sub _fmt_line { my $me = shift; my $tag = shift; my $line = shift; my $modify = shift || $me->{'mail_hdr_modify'}; my $ctag = undef; ($tag) = $line =~ /\A($FIELD_NAME|From )/oi unless(defined $tag); if($tag =~ /\AFrom /io && $me->{'mail_hdr_mail_from'} ne 'KEEP') { if ($me->{'mail_hdr_mail_from'} eq 'COERCE') { $line =~ s/^From /Mail-From: /o; $tag = "Mail-From:"; } elsif ($me->{'mail_hdr_mail_from'} eq 'IGNORE') { return (); } elsif ($me->{'mail_hdr_mail_from'} eq 'ERROR') { return _error "unadorned 'From ' ignored: <$line>" } } if(defined $tag) { $tag = _tag_case($ctag = $tag); $ctag = $tag if($modify); $ctag =~ s/([^ :])\Z/$1:/o if defined $ctag; } croak( "Bad RFC822 field name '$tag'\n") unless(defined $ctag && $ctag =~ /\A($FIELD_NAME|From )/oi); # Ensure the line starts with tag if(defined($ctag) && ($modify || $line !~ /\A\Q$ctag\E/i)) { my $xtag; ($xtag = $ctag) =~ s/\s*\Z//o; $line =~ s/\A(\Q$ctag\E)?\s*/$xtag /i; } my $maxlen = $me->{'mail_hdr_lengths'}{$tag} || $HDR_LENGTHS{$tag} || $me->fold_length; _fold_line($line,$maxlen) if $modify && defined $maxlen; $line =~ s/\n*\Z/\n/so; ($tag, $line); } sub _insert { my($me,$tag,$line,$where) = @_; if($where < 0) { $where = scalar(@{$me->{'mail_hdr_list'}}) + $where + 1; $where = 0 if($where < 0); } elsif($where >= scalar(@{$me->{'mail_hdr_list'}})) { $where = scalar(@{$me->{'mail_hdr_list'}}); } my $atend = $where == scalar(@{$me->{'mail_hdr_list'}}); splice(@{$me->{'mail_hdr_list'}},$where,0,$line); $me->{'mail_hdr_hash'}{$tag} ||= []; my $ref = \${$me->{'mail_hdr_list'}}[$where]; if(scalar($me->{'mail_hdr_hash'}{$tag}) && $where) { if($atend) { push(@{$me->{'mail_hdr_hash'}{$tag}}, $ref); } else { my($ln,$i,$ref); $i = 0; foreach $ln (@{$me->{'mail_hdr_list'}}) { my $r = \$ln; last if($r == $ref); $i++ if($r == $me->{'mail_hdr_hash'}{$tag}[$i]); } splice(@{$me->{'mail_hdr_hash'}{$tag}},$i,0,$ref); } } else { unshift(@{$me->{'mail_hdr_hash'}{$tag}}, $ref); } } ## ## Constructor ## sub new { my $self = shift; my $type = ref($self) || $self; my $arg = @_ % 2 ? shift : undef; my %arg = @_; $arg{Modify} = delete $arg{Reformat} unless exists $arg{Modify}; my %hash = ( mail_hdr_list => [], mail_hdr_hash => {}, mail_hdr_modify => delete $arg{Modify} || 0, mail_hdr_foldlen => 79, mail_hdr_lengths => {} ); my $me = bless \%hash, $type; $me->mail_from( uc($arg{'MailFrom'} || $MAIL_FROM) ); $me->fold_length($arg{FoldLength}) if exists $arg{FoldLength}; if(ref $arg) { if(ref($arg) eq 'ARRAY') { $me->extract([ @{$arg} ]); } elsif(defined fileno($arg)) { $me->read($arg); } } $me; } sub modify { my $me = shift; my $old = $me->{'mail_hdr_modify'}; $me->{'mail_hdr_modify'} = 0 + shift if @_; $old; } sub mail_from { my $me = shift; my $choice = uc(shift); $choice =~ /^(IGNORE|ERROR|COERCE|KEEP)$/ or die "bad Mail-From choice: '$choice'"; if(ref($me)) { $me->{'mail_hdr_mail_from'} = $choice; } else { $MAIL_FROM = $choice; } $me; } sub fold { my $me = shift; my $maxlen = shift; my($tag,$list,$ln); while(($tag,$list) = each %{$me->{'mail_hdr_hash'}}) { my $len = $maxlen || $me->{'mail_hdr_lengths'}{$tag} || $HDR_LENGTHS{$tag} || $me->fold_length; foreach $ln (@$list) { _fold_line($$ln,$len) if defined $ln; } } $me; } sub unfold { my $me = shift; my($tag,$list,$ln); if(@_) { $tag = _tag_case(shift); return $me unless exists $me->{'mail_hdr_hash'}{$tag}; $list = $me->{'mail_hdr_hash'}{$tag}; foreach $ln (@$list) { $$ln =~ s/\r?\n\s+/ /sog if defined $ln && defined $$ln; } } else { while(($tag,$list) = each %{$me->{'mail_hdr_hash'}}) { foreach $ln (@$list) { $$ln =~ s/\r?\n\s+/ /sog if defined $ln && defined $$ln; } } } $me; } sub extract { my $me = shift; my $arr = shift; my $line; $me->empty; while(scalar(@{$arr}) && $arr->[0] =~ /\A($FIELD_NAME|From )/o) { my $tag = $1; $line = shift @{$arr}; $line .= shift @{$arr} while(scalar(@{$arr}) && $arr->[0] =~ /\A[ \t]+/o); ($tag,$line) = _fmt_line($me,$tag,$line); _insert($me,$tag,$line,-1) if defined $line; } shift @{$arr} if(scalar(@{$arr}) && $arr->[0] =~ /\A\s*\Z/o); $me; } sub read { my $me = shift; my $fd = shift; $me->empty; my $line = undef; my $ln = ""; my $tag = undef; while(1) { $ln = <$fd>; if(defined $ln && defined $line && $ln =~ /\A[ \t]+/o) { $line .= $ln; next; } if(defined $line) { ($tag,$line) = _fmt_line($me,$tag,$line); _insert($me,$tag,$line,-1) if defined $line; } last unless(defined $ln && $ln =~ /\A($FIELD_NAME|From )/o); $tag = $1; $line = $ln; } $me; } sub empty { my $me = shift; $me->{'mail_hdr_list'} = []; $me->{'mail_hdr_hash'} = {}; $me; } sub header { my $me = shift; $me->extract(@_) if(@_); $me->fold if $me->{'mail_hdr_modify'}; # Must protect ourself against corruption as the hash contains refs to the # SV's in the list, if the user modifies this list we are really screwed :- [ @{$me->{'mail_hdr_list'}} ]; } # Return/set headers by hash reference. This can probably be # optimized. I didn't want to mess much around with the internal # implementation as for now... # -- Tobias Brox sub header_hashref { my $me = shift; my $hashref = shift; # Extract the input data for my $hdrkey (keys %$hashref) { for (ref $hashref->{$hdrkey} ? @{$hashref->{$hdrkey}} : $hashref->{$hdrkey}) { $me->add($hdrkey, $_); } } $me->fold if $me->{'mail_hdr_modify'}; # Build a hash my $hash={ map { $_ => [ $me->get($_) ] } keys %{$me->{'mail_hdr_hash'}} }; return $hash; } sub add { my $me = shift; my($tag,$text,$where) = @_; my $line; ($tag,$line) = _fmt_line($me,$tag,$text); # Must have a tag and text to add return undef unless(defined $tag && defined $line); $where = -1 unless defined $where; _insert($me,$tag,$line,$where); $line =~ /^\S+\s(.*)/os; return $1; } sub replace { my $me = shift; my $idx = 0; my($tag,$line); $idx = pop @_ if(@_ % 2); TAG: while(@_) { ($tag,$line) = _fmt_line($me,splice(@_,0,2)); return undef unless(defined $tag && defined $line); if(exists $me->{'mail_hdr_hash'}{$tag} && defined $me->{'mail_hdr_hash'}{$tag}[$idx]) { ${$me->{'mail_hdr_hash'}{$tag}[$idx]} = $line; } else { _insert($me,$tag,$line,-1); } } $line =~ /^\S+\s*(.*)/os; return $1; } sub combine { my $me = shift; my $tag = _tag_case(shift); my $with = shift || ' '; my $line; return _error "unadorned 'From ' ignored" if($tag =~ /^From /io && $me->{'mail_hdr_mail_from'} ne 'KEEP'); return undef unless exists $me->{'mail_hdr_hash'}{$tag}; if(scalar(@{$me->{'mail_hdr_hash'}{$tag}}) > 1) { my @lines = $me->get($tag); chomp(@lines); map { $$_ = undef } @{$me->{'mail_hdr_hash'}{$tag}}; $line = ${$me->{'mail_hdr_hash'}{$tag}[0]} = (_fmt_line($me,$tag, join($with,@lines),1))[1]; _tidy_header($me); } else { return $me->{'mail_hdr_hash'}{$tag}[0]; } return $line; # post-match } sub get { my $me = shift; my $tag = _tag_case(shift); my $idx = shift; return wantarray ? () : undef unless exists $me->{'mail_hdr_hash'}{$tag}; my $l = length($tag); $l += 1 unless $tag =~ / \Z/o; $idx = 0 unless defined $idx || wantarray; if(defined $idx) { return defined $me->{'mail_hdr_hash'}{$tag}[$idx] ? eval { # why won't do work here ?? my $tmp = substr(${$me->{'mail_hdr_hash'}{$tag}[$idx]}, $l); $tmp =~ s/^\s+//; $tmp; } : undef; } return map { my $tmp = substr($$_,$l); $tmp =~ s/^\s+//; $tmp } @{$me->{'mail_hdr_hash'}{$tag}}; } sub count { my $me = shift; my $tag = _tag_case(shift); exists $me->{'mail_hdr_hash'}{$tag} ? scalar(@{$me->{'mail_hdr_hash'}{$tag}}) : 0; } sub exists { carp "Depriciated use of Mail::Header::exists, use count" if $^W; count(@_); } sub delete { my $me = shift; my $tag = _tag_case(shift); my $idx = shift; my @val = (); if(defined $me->{'mail_hdr_hash'}{$tag}) { my $l = length($tag); $l += 2 unless $tag =~ / \Z/o; if(defined $idx) { if(defined $me->{'mail_hdr_hash'}{$tag}[$idx]) { push(@val, substr(${$me->{'mail_hdr_hash'}{$tag}[$idx]},$l)); undef ${$me->{'mail_hdr_hash'}{$tag}[$idx]}; } } else { local $_; @val = map { my $x = substr($$_,$l); undef $$_; $x } @{$me->{'mail_hdr_hash'}{$tag}}; } _tidy_header($me); } return @val; } sub print { my $me = shift; my $fd = shift || \*STDOUT; my $ln; foreach $ln (@{$me->{'mail_hdr_list'}}) { next unless defined $ln; print $fd $ln or return 0; } 1; } sub as_string { my $me = shift; join('', grep { defined } @{$me->{'mail_hdr_list'}}); } sub fold_length { my $me = shift; my $old; if(@_ == 2) { my($tag,$len) = @_; my $hash = ref($me) ? $me->{'mail_hdr_lengths'} : \%HDR_LENGTHS; $tag = _tag_case($tag); $old = $hash->{$tag} || undef; $hash->{$tag} = $len > 20 ? $len : 20; } else { my $len = shift; $old = $me->{'mail_hdr_foldlen'}; if(defined $len) { $me->{'mail_hdr_foldlen'} = $len > 20 ? $len : 20; $me->fold; } } $old; } sub tags { my $me = shift; keys %{$me->{'mail_hdr_hash'}}; } sub dup { my $me = shift; my $type = ref($me) || croak "Cannot dup without an object"; my $dup = new $type; %$dup = %$me; $dup->empty; $dup->{'mail_hdr_list'} = [ @{$me->{'mail_hdr_list'}} ]; my $ln; foreach $ln ( @{$dup->{'mail_hdr_list'}} ) { my $tag = _tag_case(($ln =~ /\A($FIELD_NAME|From )/oi)[0]); $dup->{'mail_hdr_hash'}{$tag} ||= []; push(@{$dup->{'mail_hdr_hash'}{$tag}}, \$ln); } $dup; } sub cleanup { my $me = shift; my $d = 0; my $key; foreach $key (@_ ? @_ : keys %{$me->{'mail_hdr_hash'}}) { my $arr = $me->{'mail_hdr_hash'}{$key}; my $ref; foreach $ref (@$arr) { unless($$ref =~ /\A\S+\s+\S/soi) { $$ref = undef; $d++; } } } _tidy_header($me) if $d; $me; } 1; # keep require happy =head1 NAME Mail::Header - manipulate mail RFC822 compliant headers =head1 SYNOPSIS use Mail::Header; $head = new Mail::Header; $head = new Mail::Header \*STDIN; $head = new Mail::Header [<>], Modify => 0; =head1 DESCRIPTION This package provides a class object which can be used for reading, creating, manipulating and writing RFC822 compliant headers. =head1 CONSTRUCTOR =over 4 =item new ( [ ARG ], [ OPTIONS ] ) C may be either a file descriptor (reference to a GLOB) or a reference to an array. If given the new object will be initialized with headers either from the array of read from the file descriptor. C is a list of options given in the form of key-value pairs, just like a hash table. Valid options are =over 8 =item B If this value is I then the headers will be re-formatted, otherwise the format of the header lines will remain unchanged. =item B This option specifies what to do when a header in the form `From ' is encountered. Valid values are C - ignore and discard the header, C - invoke an error (call die), C - rename them as Mail-From and C - keep them. =item B The default length of line to be used when folding header lines =back =back =head1 METHODS =over 4 =item modify ( [ VALUE ] ) If C is I then C will not do any automatic reformatting of the headers, other than to ensure that the line starts with the tags given. =item mail_from ( OPTION ) C