First production sendmail(8) wrapper
authorshort <>
Sun, 6 Oct 2002 19:42:36 +0000 (19:42 +0000)
committershort <>
Sun, 6 Oct 2002 19:42:36 +0000 (19:42 +0000)
perlmail-sendmail [new file with mode: 0755]

diff --git a/perlmail-sendmail b/perlmail-sendmail
new file mode 100755 (executable)
index 0000000..59ea8b1
--- /dev/null
@@ -0,0 +1,282 @@
+#! /usr/bin/perl
+#
+# $Id$
+
+use vars qw($VERSION);
+$VERSION=do { my @r=(q$Revision$=~/\d+/g); sprintf "%d.".("%03d"x$#r),@r; };
+use strict;
+use warnings;
+
+require Getopt::Long;
+use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
+require Mail::Header;
+require Mail::Address;
+require File::Basename;
+
+my $sendmail_orig=(-x ($_="/usr/sbin/sendmail-orig") ? $_ : "/usr/sbin/sendmail");
+my $HOME="/home/lace";
+my $opt_F;
+sub FromAddress
+{
+my($rcpt,$iserror)=@_;
+
+       return Mail::Address->new(
+                       (defined $opt_F ? $opt_F : "Jan Kratochvil"),
+                       (!$iserror ? 'rcpt' : 'rcpterr')
+                                       .'-'
+                                       .(defined($rcpt->user()) ? $rcpt->user() : "NOUSER")
+                                       .".AT."
+                                       .(defined($rcpt->host()) ? $rcpt->host() : "LOCAL")
+                                       .'@jankratochvil.net',
+                       );
+}
+
+# RedHat sendmail-8.9.3-20/src/conf.c/HdrInfo[]/\Q/* destination fields */\E
+# FIXME: Recognize "Resent-$_" headers for -t but when we are in 'resent' mode?
+my @h_rcpt=(   # case in-sensitive!
+               "To",
+               "Cc",
+               "Bcc",
+               "Apparently-To",
+               );
+
+# ordering matters; first header found is substituted
+# last header is subsituted if no one is found
+my @h_from=(
+               "Resent-From",
+               "From",
+               );
+
+
+# FIXME: modularized unification with 'lacemail-accept'
+# BEGIN lacemail-accept
+our %muttrc_pending=();
+sub muttrc
+{
+my($muttrc)=@_;
+
+       $muttrc||="$HOME/.muttrc";
+       $muttrc=~s/^\~/$HOME/;
+       do { warn "Looping muttrc, ignoring: $muttrc"; return (); } if $muttrc_pending{$muttrc};
+       local $muttrc_pending{$muttrc}=1;
+       local *MUTTRC;
+       open MUTTRC,$muttrc or do { warn "open \"$muttrc\": $!"; return (); };
+       local $/="\n";
+       local $_;
+       my @r=();
+       # far emulation mutt/init.c/mutt_parse_rc_line()
+       while (<MUTTRC>) {
+               s/^[\s;]*//s;
+               s/[#;].*$//s;
+               s/\s*$//s;
+               next if !/^(\S+)\s*/s;
+               if ($1 eq "source") {
+                       $_=$';
+                       do { warn "Wrong 'source' parameters at $muttrc:$.: $_"; next; } if !/^\S+$/;
+                       push @r,muttrc($_);
+                       next;
+                       }
+               push @r,$_;
+               }
+       close MUTTRC or warn "close \"$muttrc\": $!";
+       return wantarray() ? @r : join("",map("$_\n",@r));
+}
+
+my %mutteval_charmap=(         # WARNING: Don't use "" or "0" here, see below for "|| warn"!
+               '\\'=>"\\",
+               'r'=>"\r",
+               'n'=>"\n",
+               't'=>"\t",
+               'f'=>"\f",
+               'e'=>"\e",
+               );
+# mutt/init.c/mutt_extract_token()
+sub mutteval
+{
+       local $_=$_[0];
+       return $_ if !s/^"//;
+       do { warn "Missing trailing quote in: $_"; return $_; } if !s/"$//;
+       s/\\(.)/$mutteval_charmap{$1} || warn "Undefined '\\$1' sequence in: $_";/ges;
+       return $_;
+}
+
+sub muttrc_get
+{
+my(@headers)=@_;
+
+       my @r=map({ (ref $_ ? $_ : qr/^\s*set\s+\Q$_\E\s*=\s*(.*?)\s*$/si); } @headers);
+       my %r=map(($_=>undef()),@r);
+       for (muttrc()) {
+               for my $ritem (@r) {
+                       /$ritem/si or next;
+                       $r{$ritem}=mutteval $1;
+                       }
+               }
+       for my $var (grep { !defined($r{$_}) } @r) {
+               warn "Variable '$var' not found in muttrc";
+               return undef();
+               }
+       return wantarray() ? %r : $r{$r[0]};
+}
+# END lacemail-accept
+
+
+sub sendmail_show { return "\"$sendmail_orig\" ".join(",",map("\"$_\"",@ARGV)); }
+
+sub sendmail_orig_exec
+{
+       exec {$sendmail_orig} $0,@ARGV or die "exec(".sendmail_show()."): $!";
+       die "NOTREACHED";
+}
+
+Getopt::Long::Configure(
+               "no_ignorecase",
+               "no_getopt_compat",
+               "bundling",
+               # FIXME: workaround: 'unknown options' are considered the same as 'arguments'
+               # None of ($REQUIRE_ORDER, $PERMUTE, $RETURN_IN_ORDER) can help us.
+               # No preprocessing possible as it is hard to find option arguments.
+               "permute",
+               "pass_through",
+               );
+
+my $opt_b;
+my $opt_t;
+our $opt_f;
+#my $opt_F;    # declared before &FromAddress already
+my $opt_lacemail_dry_run;
+my @ARGV_save=@ARGV;   # for non-bm mode
+die if !Getopt::Long::GetOptions(
+               "b=s"              ,\$opt_b,
+               "t"                ,\$opt_t,
+               "f=s"              ,\$opt_f,
+               "F=s"              ,\$opt_F,
+               "lacemail-dry-run+",\$opt_lacemail_dry_run,
+               );
+if (0
+               # RedHat sendmail-8.12.5-7/sendmail/main.c/\QDo a quick prescan of the argument list.\E
+               || grep({ File::Basename::basename($0) eq $_; } "newaliases","mailq","smtpd","hoststat","purgestat")
+               # -bm: Deliver mail in the usual way (default).
+               || (defined($opt_b) && $opt_b ne "m")
+               ) {
+       @ARGV=@ARGV_save;
+       sendmail_orig_exec();
+       die "NOTREACHED";
+       }
+
+# RedHat sendmail-8.9.3-20/src/main.c/main()/\Qif (FullName != NULL)\E
+#   for $opt_F is implemented by Mail::Address in our &FromAddress
+
+my $head=Mail::Header->new(\*STDIN);
+# We may (=will) change the contents and send it multiple times
+if (defined(my $msgid=$head->get("Message-ID"))) {
+       $head->delete("Message-ID");
+       $head->replace("X-LaceMail-sendmail-Message-ID",$msgid);
+       }
+# options leave in @ARGV, addresses to @addr:
+my @args=@ARGV;        # temporary
+@ARGV=();      # options
+my @addr=();   # addresses
+push @{(/^-./ ? \@ARGV : \@addr)},$_ for (@args);
+if ($opt_t) {
+       for my $addrobj (map({ Mail::Address->parse($_); } map({ ($head->get($_)); } @h_rcpt))) {
+               if (!$addrobj->address()) {
+                       # bogus, shouldn't happen
+                       warn "->address() not found in \"".$addrobj->format()."\"";
+                       next;
+               }
+               push @addr,$addrobj;
+               }
+       }
+
+# return: Mail::Address instance or undef()
+sub parseone
+{
+my($line)=@_;
+
+       return undef() if !defined $line;
+       my @r=Mail::Address->parse($line);
+       warn "Got ".scalar(@r)." addresses while wanting just one; when parsing: $line" if 1!=@r;
+       return $r[0];
+}
+
+sub matches
+{
+       return 
+}
+
+my $from_headername;
+{
+       my $muttrc_From=parseone(scalar muttrc_get("from"));    # may get undef()!; parseone() may be redundant
+       $muttrc_From=$muttrc_From->address() if $muttrc_From;
+       $opt_f=undef() if defined($opt_f) && $muttrc_From && lc($opt_f) eq lc($muttrc_From);
+       my @from_val;
+       for (@h_from) {
+               $from_headername=$_;    # leave last item in $from_headername
+               last if @from_val=$head->get($from_headername);
+               }
+       @from_val=map({ ($_->address()); } map({ (Mail::Address->parse($_)); } @from_val));
+       $from_headername=undef() if !(1==@from_val && $muttrc_From && lc($from_val[0]) eq lc($muttrc_From));
+       # now $from_headername contains the header name to be replaced w/substituted value
+       }
+
+my $exitcode=0;
+my @rcpts=(@addr ? @addr : (undef())); # !defined($rcpt) if we have no recipients
+my $stdin_body=(@rcpts<=1 ? undef() : do {     # store input data only if it will be used multiple times
+               local $/=undef();
+               <STDIN>;
+               });
+for my $rcpt (@rcpts) {
+       local @ARGV=@ARGV;
+       local $opt_f=$opt_f;
+
+       if (defined $rcpt) {    # !defined($rcpt) if we have no recipients
+               local $_;
+               if (!ref $rcpt) {
+                       $rcpt=parseone $rcpt;
+                       next if !defined $rcpt;
+                       }
+               $opt_f=FromAddress($rcpt,1)->address() if !defined $opt_f;
+               $head->replace($from_headername,FromAddress($rcpt,0)->format()) if $from_headername;
+               }
+
+       1;      # drop '-bm' if present as it is default anyway
+       1;      # drop '-t' if present as we are looping now for it
+       push @ARGV,"-f",$opt_f if defined $opt_f;
+       # we don't handle "Full-Name" header thus pass "-F"
+       # "From/Resent-From" should be handled by our &FromAddress
+       push @ARGV,"-F",$opt_F if defined $opt_F;
+       push @ARGV,$rcpt->address() if defined $rcpt;
+
+       local $SIG{"PIPE"}=sub { die "Got SIGPIPE from ".sendmail_show(); };
+       local *SENDMAIL;
+       if ($opt_lacemail_dry_run) {
+               print sendmail_show()."\n";
+               *SENDMAIL=\*STDOUT;
+               }
+       else {
+               defined (my $pid=open SENDMAIL,"|-") or die "Cannot fork to spawn ".sendmail_show().": $!";
+               sendmail_orig_exec() if !$pid; # child
+               }
+       $head->print(\*SENDMAIL);
+       print "\n";     # Mail::Header->print() eats the empty line but it doesn't print it
+       if (defined($stdin_body)) {
+               print SENDMAIL $stdin_body;
+               }
+       else {
+               local $_;
+               while (<STDIN>) {
+                       print SENDMAIL $_;
+                       }
+               }
+
+       next if $opt_lacemail_dry_run;  # don't close our STDOUT as it is aliased to *SENDMAIL
+       close SENDMAIL or warn "close(".sendmail_show()."): $?=".join(",",
+                       (!WIFEXITED($?)   ? () : ("EXITSTATUS(".WEXITSTATUS($?).")")),
+                       (!WIFSIGNALED($?) ? () : ("TERMSIG("   .WTERMSIG($?)   .")")),
+                       (!WIFSTOPPED($?)  ? () : ("STOPSIG("   .WSTOPSIG($?)   .")")),
+                       );
+       my $gotcode=(!WIFEXITED($?) ? 99 : WEXITSTATUS($?));
+       $exitcode=$gotcode if $gotcode>$exitcode;
+       }
+exit $exitcode;