#!/usr/bin/perl -Tw # #NAME # Send - Heuristic Direct Mail Delivery # #SYNOPSIS # Send [-options] [[file] ].. [to ..] [list ..] # #DESCRIPTION # This program sends its stdin or a list of files. If the keywords # "to" or "list" are present, then an explicit list of recipients # will be used, and the files' headers will be ignored. Otherwise, # the file(s) will be examined for "To:" lines. Also, if the "list" # keyword is present, the names after it are looked up in the # $HOME/mail/list/ directory, where you can keep mailing lists, one # per file. # # No other mailer is invoked by this program; the mail is sent by # making a direct TCP connection to port 25 and talking the (E)SMTP # protocol. At verbose level 2 or greater, the entire SMTP # conversation will be written to stdout, so you can see what the # other end really said. This can help tremendously in trying to # figure out why things didn't work. # # If "list" is used, this program looks in $HOME/mail/list/ for the # list names. So to create a "foo" list, create $HOME/mail/list/foo # and fill it with email addresses. They should be one per line, in # any of the common To: line formats. As usual, lines starting with # '#' are ignored. All other lines are treated as recipients. # # As a special kludge, if the keyword "file" is used before a list # of file names, the files' headers will not be scanned. This may be # used to override the program's tendency to scan headers if no # recipients can be found on the command line or in any mailing list # files. # #OPTIONS # We have a few options, flagged by '-' or '+'. For some options, # '-' means "disable" and '+' means "enable"; for others there is no # such concept and you may use either. If a parameter is used, it # should immediately follow the option letter without a space. # # -F # From . This is the source address to report to the other # end's mailer. This is useful when forwarding messages, as well # as for spoofing someone else's email. The most useful use is for # mailing lists that restrict your email to your list address. # This lets you send messages from that address even if you're on # another machine. # # -S # Add the string to each message as a Subject: line. The # default is to send the Subject: line that's in the file. This # option adds a Subject: line to each file. # # -sig No .signature file # +sig Append $HOME/.signature (default) # $sigfl = 0; # Whether to read .signature file $sigfile = $ENV{'HOME'} . '/.signature'; # #REQUIRES # The following modules are needed. Any *.pm file listed here should # be in the same directory where you found this program. You should # make sure that @INC includes whatever directory you install things # into. # push @INC, "$ENV{HOME}/bin", "$ENV{HOME}/sh", ".", "sh"; use Socket; # Perl's TCP socket stuff require "HTTPcon.pm"; # JC's HTTP connection routine # #CONFIGURATION # We need a home directory and a place to put a few log messages: $home = $ENV{'HOME'} || '/u/guests/jc'; $logdir = "$home/log"; # # If you want this program to identify itself, this string will be # sent as the User-Agent: $myid = "~jc/sh/Send.pl"; # Will anyone notice this? # # This program has the ability to lie judiciously to the other end, # in order to satisfy some of the bizarre misimplementations of SMTP # that have been seen. This can be done by configuring any or all of # the following: # # Some hosts don't respond to SMTP connections, but another hostname # will work. Enter such hosts in this table, in lower case, to speed # up the job: %rehost = ( # 'hotmail.com' => 'mail.hotmail.com', # Are they fixed now? 'listserv.heanet.ie' => 'mail.heanet.ie', 'usa.net' => 'mxpool01.netaddress.usa.net', ); # # If we can't connect directly to a machine, we use this list of # hosts to use as relays. We connect to each in turn, and try to # hand off the message. @relay = ( # Mail relay hosts # 'ecf-guest.mit.edu', ); # # For each of the relay machines, we may need to send its mailer a # string to identify our machine. The default is our hostname, but # you can use this to specify other strings: %hostid = ( # Host names to send to specific hosts 'trillian.mit.edu' => 'ecf-guest.mit.edu', 'bigfoot.com' => 'eddie.mit.edu', 'yahoo.com' => 'ecf-guest.mit.edu', # 'yahoogroups.com' => 'trillian.mit.edu', 'mindspring.com' => 'ecf-guest.mit.edu', ); $hostid = 'trillian.mit.edu'; # For all other hosts # # You may also need to specify your user id differently for some # remote hosts. %userid = ( # User identification to send to hosts ); $userid = 'jc'; # For all other hosts # # You can also specify a full name to give in the From: lines: %fullnm = ( # Per-host names to send 'trillian.mit.edu' => 'John Chambers', 'bigfoot.com' => 'Jean Chambres', 'yahoo.com' => 'John Chambers', # 'yahoogroups.com' => 'Jean Chambres', 'mindspring.com' => 'Jean Chambres', ); $fullnm = "John Chambers"; # For all other hosts # # Some sites can't be handled this way, and you to use a different # email address depending on which recipient you are sending to. The # most common reason for this is mailing lists that only accept mail # from list members. %adrmap = ( # rcpt => sender mapping # 'tradtunes@yahoogroups.com' => 'John Chambers ', # 'scand@yahoogroups.com' => 'John Chambers ', 'rjelly@yahoogroups.com' => 'John Chambers ', 'gaybladesrapper@yahoogroups.com' => 'Jean Chambres ', # 'QueTrad@yahoogroups.com' => 'Jean Chambres ', ); # #ENVIRONMENT # We get our verbose level from the environment. If this program is # called "Send", then you might set V_Send to the verbose level. The # default is 1, which produces only serious messages. Higher numbers # will produce more output. # # We need to know about a couple of directories: $maildir = '/usr/mail'; $homedir = $ENV{HOME} || $maildir; # We will look for a 'list' subdirectory to either of these as # a place to find mailing-list files. # #EXAMPLES # #FILES # #BUGS # #KLUDGES # # We suppress Bcc: lines, after extracting the recipient info. This # was the easiest way of hiding these lines. We also hide any extra # To: header lines. The reason is that some mailers choke if they # get more than one To: line. We handle them properly, of course, # but strip them out so as not to offend sensitive email software. # Maybe they should be changed to Cc: lines? # #SEE ALSO # #AUTHOR # Copyright 1995, 1999, 2003 by John Chambers . # You are free to use this program as you wish, as long as you give # me credit (and take credit for your changes). If you come up with # any cool new features, please send me a copy. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # $| = 1; $" = ','; $exitstat = 0; # Set to nonzero for failures ($P = $0) =~ s".*/""; # Our program name, minus any directory &Vinit($ENV{"V_$P"}||2); # Our verbose level, defaults to 1 or 2 $logfile = "$logdir/$P.log"; # Where to write log messages mkdir($logdir,0755) unless -d $logdir; if (open(LOG,">>$logfile")) { ++$logopen; # Note that we have a log file } else { print STDERR "$P: Can't write \"$logfile\" ($!)\n"; } $HTTPtimeout = 30; # Timeout for connects $argis = 'msg'; # How to interpret args $CRLF = "\r\n"; # RFC 822 line terminator $hlo = 'HELO'; # Default greeting $parsehdrs = 0; # Whether to read file headers #proto = getprotobyname('tcp'); $port = getservbyname('smtp','tcp'); $hdrs = 1; # Whether to generate email headers $tryalt = 1; # Whether to try variants on host name $usehostcmd = 1; # Use host command to find mail hosts $usenslookupcmd = 1; # Use nslookup to file mail hosts chomp($hostid = `hostname`) unless $hostid; &vsend("$P: Our hostid=\"$hostid\"") if $V>2; chomp($userid = $ENV{LOGNAME} || $ENV{USER}) unless $userid; &vsend("$P: Our userid=\"$userid\"") if $V>2; chomp($fullnm = $ENV{FULLNAME}) unless $fullnm; &vsend("$P: Our fullnm=\"$fullnm\"") if $V>2; # Sort the command-line args into bins arg: for $arg (@ARGV) { if (($flg,$a) = ($arg =~ /^([-+])(.*)/)) { &vsend("$P: Option \"$a\"") if $V>3; if ($a =~ /^[DdVv]/) { push @opts, '-v'; &vsend("$P: opts=(@opts)") if $V>2; } elsif ($a =~ /^[Ff](.*)/) { $From = $1; &vsend("$P: From='$From'") if $V>2; } elsif ($a =~ /^[Tt]/) { push @opts, '-t'; &vsend("$P: opts=(@opts)") if $V>2; } elsif ($a =~ /^[Ss](.*)/) { if ($a =~ /^sig/i) { $sigfl = ($flg eq '+'); &vsend("$P: sigfl=\"$sigfl\"") if $V>2; } else { $subj = $1; &vsend("$P: subj=\"$subj\"") if $V>2; } } else { &vsend("$P: Option \"$a\" not recognized, ignored.") if $V>2; } next arg; } &vsend("$P: Arg \"$arg\"") if $V>3; if ($arg eq 'to' ) {$argis = 'rcpt'; next arg} if ($arg eq 'list') {$argis = 'list'; next arg} if ($arg eq 'rcpt') {$argis = 'rcpt'; next arg} if ($arg eq 'file') {$argis = 'file'; $hdrs = 0; next arg} if ($argis eq 'rcpt') { # Recipients push @rcpts, $arg; &vsend("$P: rcpts: @rcpts") if $V>2; $parsehdrs = 0; next arg; } elsif ($argis eq 'list') { # Mailing lists push @lists, $arg; &vsend("$P: lists: @lists") if $V>2; $parsehdrs = 0; next arg; } else { # Anything else is file name push @files, $arg; &vsend("$P: files: @files") if $V>2; next arg; } } # If we got no file names, we read from standard input into a scratch # file, and send that message to all our recipients. unless (@files) { $tmpfil = "/tmp/Mail$$"; &vsend("$P: <= STDIN") if $V>1; # Needed if there are relic /tmp/Mail* files: # system 'rm /tmp/Mail*'; unless (open(TMP,">$tmpfil")) { &vsend("$P: Can't write \"$tmpfil\" ($!)") if $V>0; exit $!; } while ($l = ) {print TMP $l;} close TMP; push @files, $tmpfil; $parsehdrs = 0; } # If we got mailing list name(s), we look for the list files and # extract all the email addresses, adding them to @rcpts. list: for $l (@lists) { $listfil = ''; if ( -f ($listfil = "$homedir/mail/list/$l")) { } elsif ( -f ($listfil = "$maildir/list/$l")) { } else { next list } push @rcpts, &parselist($listfil); } unless (@rcpts) { $parsehdrs = 1; # Look for To: lines in files $opts[$#opts+1] = '-t'; # Sendmail flag with same meaning } file: for $f (@files) { &vsend("$P: Reading mail from $f ...") if $V>2; $from = ''; # May be set in headers unless (@rcpts) { # No global recipients &vsend("$P: Scan file \"$f\" for recipients ...") if $V>2; @rcpts = &parsehdrs($f); &vsend("$P: rcpts=(@rcpts)") if $V>2; $fromhdrs = 1; # Note recipients are from file } unless (@rcpts) { # Do we have any recipients? &vsend("$P: No recipients found in file \"$f\".") if $V>2; next file; } &vsend("$P: Send file \"$f\" to all recipients ...") if $V>2; for $r (@rcpts) { # Send out one file to all recipients &vsend("$P: Send file \"$f\" to \"$r\" ...") if $V>2; &fil($f,$r); } if ($fromhdrs) { # Did recipients come from file? @rcpts = (); # If so, forget about them $fromhdrs = 0; # No known recipients now } } if ($tmpfil) { unlink $tmpfil if $V<3; } exit $exitstat; # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub SendAlarm { my $t = time - $HTTPcontime; &vsend("SendAlarm: ALARM after $t sec.") if $V>1; $errmsg = 'timeout'; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Send one mail file to one recipient. sub fil { local($file,$rcpt) = @_; local($nam,$rly,$sys,$who); local($F) = "$P/fil"; if ($listnames{$rcpt}) { # Break possible recursive loops &vsend("$F: Rcpt \"$rcpt\" is a list.") if $V>1; return 0; # List should have been expanded } &vsend("$F: Send \"$file\" to \"$rcpt\" ===========================") if $V>2; unless (($sys,$who,$nam) = &parsercpt($rcpt)) { &vsend("$F: Can't send to recipient \"$rcpt\"") if $V>1; return undef; } $sys =~ s"\.+$""; # Some mailers can't handle trailing dots on hostname return undef unless $who && $sys; unless (open(MSG,$file)) { print STDERR "$F: ### Can't read \"$file\" ($!)\n"; &vsend("$F: ### Can't read \"$file\" ($!)"); return undef; } &vsend("$F: <== \"$file\" to \"$who\" at \"$sys\" ($nam)") if $V>1; &lsend("$F: \"$file\" to \"$who\" at \"$sys\" ($nam)") if $V>2; if (&msg($sys,$who,$nam)) { &vsend("$F: Sent to \"$who\" ($nam) at \"$sys\"") if $V>2; close MSG; return 1; } &vsend("$F: Mail to \"$who\" at \"$sys\" FAILED because \"$errmsg\".") if $V>2; for $rly (@relay) { &vsend("$F: <= \"$file\" to \"$rcpt\" via \"$rly\" ...") if $V>1; unless (open(MSG,$file)) { print STDERR "Can't read \"$file\" ($!)\n"; return undef; } if (&msg($rly,$rcpt,$nam)) { &vsend("$F: Sent \"$file\" to \"$rcpt\" via \"$rly\".") if $V>2; close MSG; return 1; } &vsend("$F: Send \"$file\" to \"$rcpt\" via \"$rly\" FAILED.") if $V>2; &lsend(&isodate() . " Send \"$file\" to \"$who\" at \"$sys\" ($nam) FAILED."); } &vsend("$F: Failed for all hosts. ") if $V>2; &lsend(&isodate() . " Send \"$file\" FAILED for all hosts."); return 0; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Send one message and wait for one response. We expect the response # to start with a status code, which we return. SMTP status codes are # errors if they are 400 or greater. sub msgrsp { local($msg) = join('',@_); local($rsp,$t0,$t); $errcode = 0; $errmsg = ''; if ($msg) { &msend("$msg"); &vsend("$F: => $msg") if $V==2; } response: while (1) { $t0 = time; $rsp = ; &vsend("$F: ### $t-sec delay.") if (($t = time - $t0) > 1); $rsp =~ s/[\s\r]+$//; &vsend("$F: <= $rsp") if $V>1; if ($rsp =~ /\bESMTP\b/) { $hlo = 'EHLO'; # Kludge for servers that speak ESMTP &vsend("$F: === Switch to ESMTP.") if $V>1; } if (($errcode,$errmsg) = ($rsp =~ /^(\d+)-(.*)\s*$/)) { &vsend("$F: Rsp $errcode-$errmsg") if $V>5; next response; } if (($errcode,$errmsg) = ($rsp =~ /^(\d+)\s+(.*)\s*$/)) { &vsend("$F: ERR $errcode $errmsg\n$F: ERR from \"$sys\"") if $V>0 && $errcode >= 400; } return $errcode; } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub connects { local($thathst, $port) = @_; local($tail,$try,$t0,$t1,$ti); &vwrite("$F: === Connect to $thathst:$port ... ") if $V>1; $t0 = time; if (&HTTPcon(*SCK,$try="$thathst:$port")) { $ti = time - $t0; &vwrite("connected in $ti sec.\n"); return $try; } $ti = time - $t0; &vwrite("failed in $ti sec ($!)\n") if $V>1; &vwrite("$F: === connect to mail.$thathst:$port ...") if $V>1; $t1 = time; if (&HTTPcon(*SCK,$try="mail.$thathst:$port")) { $ti = time - $t1; &vwrite("connected in $ti sec.\n"); return $try; } $ti = time - $t1; &vwrite("failed in $ti sec ($!)\n") if $V>1; if ($tryalt) { $tail = $thathst; while ($tail =~ /^([^.]*)\.(.*)\.(.*)/) { $tail = "$2.$3"; &vwrite("$F: === connect to $tail:$port ...") if $V>1; $t1 = time; if (&HTTPcon(*SCK,$try="$tail:$port")) { $ti = time - $t1; &vwrite("connected in $ti sec.\n"); return $try; } $ti = time - $t1; &vwrite("failed in $ti sec ($!)\n") if $V>1; &vwrite("$F: === connect to mail.$tail:$port ...") if $V>1; $t1 = time; if (&HTTPcon(*SCK,$try="mail.$tail:$port")) { $ti = time - $t1; &vwrite("connected in $ti sec.\n"); return $try; } $ti = time - $t1; &vwrite("failed in $ti sec ($!)\n") if $V>1; } } &vsend("$F: Can't find mail server for \"$thathst\"") if $V>1; return undef; # Failure } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Here's the routine that handles sending the current $file to one # recipient on one host. Note that the caller has opened the message # file, and we just read from the MSG file handle. We connect to the # host, and if that succeeds, we do a simple SMTP handshake. If any # of this fails, we return 0 for failure. If it succeeds, we send the # MSG file's contents, a "." line, and a QUIT, and then return 1 to # indicate success. sub msg { local($thathst,$thatusr,$thatnm) = @_; local($thishst,$thisusr,$symnam,$rcpt,$mchars,$mlines,$orighst,$sndr); local($hostport,$isots,$l,@mx,$mxcmd,@nslookup,$mxhost,$tralt,$ToCnt); local($F) = "$P/msg"; $mchars = $mlines = 0; $symnam = " ($thatnm)" if $thatnm; $orighst = $thathst; &vsend("$F: Send \"$file\" to host \"$thathst\" user \"$thatusr\" name \"$symnam\"...") if $V>2; if ($substhost = $rehost{lc($thathst)}) { &vsend("$F: === rehost $thathst to $substhost.") if $V>1; $thathst = $substhost; push @mx, $substhost; } &vsend("$F: MX lookup for $thathst ...") if $V>2; # # host formats: # lycos.com mail is handled (pri=20) by mx1.mail.lycos.com # lycos.com mail is handled by 10 mx.mail.lycos.com. # if ($usehostcmd) { $mxcmd = "host -t mx $thathst"; &vsend("$F: cmd \"$mxcmd\"") if $V>1; if (open(MX,"$mxcmd |")) { while ($l = ) { $l =~ s/[\s\r]+$//; # foo.com mail is handled (pri=1) by mta1.grp.scd.yahoo.com if ($l =~ / is handled \(*pri=(\d+)\)* by (.*)/) { # Format on FreeBSD &vsend("$F: === MX $thathst => $2.") if $V>1; push @mx, $2; } elsif ($l =~ / is handled by (\d+) (.*)/) { # Format on RH 8.0 linux &vsend("$F: === MX $thathst => $2.") if $V>1; push @mx, $2; } } close MX; } } if (!@mx && $usenslookupcmd) { $mxcmd = "nslookup -sil -querytype=mx $thathst"; &vsend("$F: cmd \"$mxcmd\"") if $V>1; if (open(MX,"$mxcmd |")) { while ($l = ) { $l =~ s/[\s\r]+$//; if ($l =~ /preference = (\d+), mail exchanger = (.*)$/) { &vsend("$F: === MX $thathst => $2.") if $V>1; push @mx, $2; } } close MX; } } unless (@mx) { &vsend("$F: Lookups failed; using host \"$thathst\"") if $V>2; push @mx, $thathst; } MX: for $mxhost (@mx) { &vsend("$F: Try MX host \"$mxhost\" ...") if $V>2; $tralt = $tryalt; # Disable tryalt mode for mx hosts $tryalt = 0; if ($hostport = &connects($mxhost, $port)) { last MX; } $tryalt = $tralt; # Restory tryalt mode } unless ($hostport) { &vsend("$F: Giving up on $thathst.") if $V>0; return undef; } &vsend("$F: === Connected to $hostport.") if $V>5; $thishst = $hostid{$thathst} || $hostid; &vsend("$F: thishst=\"$thishst\"") if $V>3; $thisusr = $userid{$thathst} || $userid; &vsend("$F: thisusr=\"$thisusr\"") if $V>3; $thisnam = $fullnm{$thathst} || $fullnm; &vsend("$F: thisnam=\"$thisnam\"") if $V>3; $sndr = $From || "<$thisusr\@$thishst>"; if ($thatusr =~ /\@/) { $rcpt = "$thatusr"; } else { $rcpt = "$thatusr\@$orighst"; } if ($adrmap{$rcpt}) { $sndr = $rpto = $adrmap{$rcpt}; &vsend("$F: Pretend to be '$sndr' for '$rcpt'") if $V>1; } else { $rpto = $ENV{'REPLYTO'} || "$thisnam $sndr"; } $hlo = 'HELO'; # Use RFC 821 greeting first return 0 if &msgrsp("") >= 400; return 0 if &msgrsp("$hlo $thishst") >= 400; return 0 if &msgrsp("MAIL From: $sndr") >= 400; return 0 if &msgrsp("RCPT To: <$rcpt>") >= 400; return 0 if &msgrsp("DATA") >= 400; unless ($msid) { # Generate a message ID string &gmdate(); $msid = "<$isodt.$$.$userid\@$hostid>"; } if ($hdrs) { print V "To: <$rcpt>\n" if $V>2; print V "Subject: $subj\n" if $V>2; &msend("To: <$rcpt>"); ++$ToCnt; &msend("X-Sent-To: <$rcpt>"); unless($date) {$date = &gmdate()} if ($subj) {&msend("Subject: $subj")} unless($from) {&msend("From: $thisnam $sndr"); &msend("X-Sent-From: $thisnam $sndr")} if ($date) {&msend("Date: $date")} if ($rpto) {&msend("Reply-to: $rpto")} if ($myid) {&msend("User-Agent: $myid")} if ($msid) {&msend("Message-ID: $msid")} if ($inrpto) {&msend("In-Reply-To: $inrpto")} # &msend(""); # Blank line to terminate our headers print V "$F: MSG " if $V==2; } while ($l = ) { if ($hdrs) { # Suppress Bcc lines next if ($l =~ /^Bcc:/i); # $l =~ s/^To:/Cc:/i if $ToCnt>1; # Convert extra To lines to Cc } $l =~ s"[\s\r]+$""; # Trim trailing whitespace $l =~ s"^\.".."; # RFC 821 transparency &msend($l); print V '+' if $V==2; # One + per line # sleep 1 if $V==2; # Slow it down to verify output } print V "\n" if $V==2; if ($sigfl) { if (open(SIG,$sigfile)) { print V "$F: SIG " if $V==2; &msend("$CRLF--"); # We need the \r for 822bis/2822 compliance while ($l = ) { $l =~ s"[\s\r]+$""; # Trim trailing whitespace $l =~ s"^\.".."; # RFC 821 transparency &msend($l); print V '+' if $V==2; # One + per line } close SIG; print V "\n" if $V==2; } else { &vsend("$F: Can't read \"$sigfile\" ($!)") if $V>0; $sigfl = 0; } } &msend("."); &vsend("$F: => . (sent $mlines lines $mchars chars)") if $V==2; chomp($rsp = ); &vsend("$F: <= $rsp") if $V>1; &lsend(&isodate() . " Sent \"$file\" to \"$who\" at \"$sys\" ($nam)"); return 1; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# # Send a message to the log output stream, adding a newline. # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# sub lsend { for (@_) {print LOG $_ . "\n"} } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# # Send a message to the log/verbose output stream, adding a newline. # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# sub vsend { for (@_) {print V $_ . "\n"} } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Send a message to the log/verbose output stream, with no newline. # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub vwrite { local($m); for $m (@_) {syswrite V,$m,length($m)} } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Send one or more messages to the mail output socket, with added newlines. # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub msend { local($m,$savsig); if ($HTTPtimeout > 0) { alarm $HTTPtimeout; $savsig = $SIG{ALRM}; $SIG{ALRM} = 'SendAlarm'; print V "HTTPcon: Set alarm after $HTTPtimeout sec.\n" if $V>4; } for $m (@_) { print SCK "$m$CRLF"; &vsend("$F: => $m") if $V>2; $mchars += length($m); $mlines ++; } alarm 0; $SIG{ALRM} = $savsig; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Read a file and extract recipient info from its headers. Return the # # list of recipients. We also note and Date: line and save its value. # # A complication here is that SMTP header lines may be continued by a # # line that starts with whitespace. This means we have to read them # # into a buffer (@h), appending continuation lines until we hit # # another header or null line. When we run out of headers, we then # # read thru @h and extract the headers we find interesting. # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub parsehdrs { local($file); # File(s) to scan local($F) = "$P/parsehdrs"; local(@h,$n,$r,@r); for $file (@_) { unless (open(FIL,$file)) { &vsend("$F: Can't read \"$file\" ($!)") if $V>0; next; } line: while ($line = ) { $line =~ s/[\s\r]+$//; $line =~ s/\(([^)]+)\s*[;,]\s*([^)]+)\)/($1 $2)/g; if ($line eq '') { &vsend("$F: NULL line ends headers.") if $V>2; last line; } elsif ($line =~ /^-----/) { &vsend("$F: Dashed line ends headers.") if $V>2; last line; } elsif ($line =~ /^(Date|From|To|Cc|Bcc):/i) { # Wanted push @h, $line; } elsif ($line =~ /^\s+(.*)/) { # Continuation $h[$#h] .= ", $line"; # Comma to get rcpt lists right } else { push @h, $line; # Unused, but may have continuation } } for $line (@h) { if ($line =~ /^(From):\s*(.*)<(.*)>$/i) { &vsend("$F: From \"$1\" <$2>") if $V>2; $from = "$2 <$3>"; push @r, $3; # } elsif ($line =~ /^(To):\s*(.*)<(.*)>$/i) { # &vsend("$F: RCPT 1 ($3)") if $V>2; # push @r, $3; } elsif ($line =~ /^(To|Cc|Bcc):\s*(.*)$/i) { for $r (split(/\s*[;,]\s*/,$2)) { # We look for ';' and ',' - both happen $r =~ s/^\s*(.*?)\s*$/$1/; # Trim the address &vsend("$F: RCPT 2 \"$r\"") if $V>2; push @r, $r if $r; } } elsif ($line =~ /^(Date):\s*(.*)\s*$/i) { &vsend("$F: Date \"$2\"") if $V>2; $date = $2; } elsif ($line =~ /^(Message-ID):\s*(.*)\s*$/i) { $inrpto = $2; &vsend("$F: Message-ID \"$inrpto\"") if $V>2; } else { &vsend("$F: Drop \"$line\"") if $V>4; } } } $n = int(@r); &vsend("$F: Found $n recipients in $f file.") if $V>2; return @r; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Mailing-list files should have one recipient per line. We trim away # initial and trailing whitespace, and anything after a # or comma. # The remaining strings are added to the list that we return. # sub parselist { local($f) = @_; local($r,@r); local($F) = "$P/parselist"; unless (open(LST,"<$f")) { print STDERR "Can't read \"$f\"\n"; next list; } while ($r = ) { $r =~ s/^\s*(.*)[\s\r]*$/$1/; # Trim white space $r =~ s/\s*[#;,].*$/$1/; # Trim comments next if !$r; # Ignore empty lines &vsend("$F: Rcpt: \"$r\"") if $V>2; push @r, $r; } close LST; return @r; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Parse a recipient's email address into a host and remainder string. # The return value is a list of three values: system, user, and any # symbolic user name that is found. sub parsercpt { local($x) = @_; local($f,$h,$n,$r,@r); local($F) = "$P/parsercpt"; if ($x =~ /^\s*<*([^@%]+)[@%]([-.\w]+)>*\s*\((.*)\)\s*$/) { &vsend("$F: Matched @%-style address with rcpt \"$1\" host \"$2\" name ($3).") if $V>2; return ($2,$1,$3); } if (($n,$r,$h) = ($x =~ /^(.*)<([^@%]+)[@%]([-.\w]+)>$/)) { $n =~ s/^[\s"]+//; $n =~ s/[\s"]+$//; &vsend("$F: Matched @%-style address with rcpt \"$r\" host \"$h\" name \"$n\".") if $V>2; return ($h,$r,$n); } if ($x =~ /^([^@%]+)[@%]([-.\w]+)$/) { &vsend("$F: Matched @%-style address with rcpt \"$1\" host \"$2\" (no name).") if $V>2; return ($2,$1,''); } if ($x =~ /^([-.\w]+)[!:]+([^@%]+)$/) { &vsend("$F: Matched !:-style address with rcpt \"$2\" host \"$1\" (no name).") if $V>2; return ($2,$1); } if ($x =~ /^\w+$/) { &vsend("$F: Matched user name \"$x\" alone.") if $V>2; if (-f ($f = "$homedir/mail/list/$x")) { &vsend("$F: Found $f file.") if $V>2; if (@r = &parselist($f)) { $n = int(@r); if ($n > 0) { &vsend("$F: Found $n recipients in $f file, to be handled later.") if $V>2; push @rcpts, @r; $listnames{$x} = $f; # Note that this is a list } else { &vsend("$F: Found no recipients in $f file.") if $V>2; } return undef; } } return ('localhost',$x); } &vsend("$F: Can't make sense of email address \"$x\".") if $V>1; return undef; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Verbose initialization. We make sure that $V is set to a number and # the V file handle is open as a place to write messages. sub Vinit { local($opt) = @_; ($V,$Vfil) = ($opt =~ /^(\d*)(.*)/); $V = 1 unless $V; if ($Vfil) { unless (open(V,">$Vfil")) { print STDERR "$P: Can't write \"$Vfil\" ($!)\n"; open(V,">&STDERR"); $Vfil = 'STDERR'; } } else { open(V,">&STDERR"); $Vfil = 'STDERR'; } select V; $| = 1; select STDOUT; &vsend("$P: V=$V \"$Vfil\"") if $V>3; &vsend("\n$P " . join(' ',@ARGV)) if $V>1; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Get the date and time, in the UTC/GMT time zone. We return a # # human-readable date, and also leave the date and time behind in the # # global $isodt variable. # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub gmdate { local($sec,$min,$hour,$mday,$mon,$year,$wday) = gmtime(time); @wkday = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat','Sun'); @month = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'); $Mon = $month[$mon]; $Day = $wkday[$wday]; $isodt = sprintf("%04d%02d%02d%02d%02d%02d",1900+$year,$mon,$mday,$hour,$min,$sec); return sprintf("%s, %02d %s %04d %02d:%02d:%02d UTC",$Day,$mday,$Mon,1900+$year,$hour,$min,$sec); } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # sub isodate { local($sec,$min,$hour,$mday,$mon,$year,$wday) = gmtime(time); @wkday = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat','Sun'); @month = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'); $Mon = $month[$mon]; $Day = $wkday[$wday]; return sprintf("%04d-%02d-%02d %02d:%02d:%02d UTC",1900+$year,1+$mon,$mday,$hour,$min,$sec); }