#! /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 MIME::Head; # inherits Mail::Header require Mail::Address; require File::Basename; require Mail::Alias; my $sendmail_orig=(-x ($_="/usr/sbin/sendmail-orig") ? $_ : "/usr/sbin/sendmail"); my $HOME="/home/short"; # Mail-Alias-1.12 defaults to "/etc/mail/aliases" which does not exist on RedHat sendmail-8.12.5-7 # Mail-Alias-1.12 will clutter $_ ! my @addr_addon=(Mail::Alias->new("/etc/aliases")->exists("sentout") ? ("sentout") : ()); my $opt_F; my $is_pgp; sub FromAddress { my($rcpt,$iserror)=@_; my $phrase=(defined $opt_F ? $opt_F : "Jan Kratochvil"); { last if !$is_pgp; last if $iserror; local *F; local $_; my $filename="$HOME/.gnupg/options"; open F,$filename or do { warn "Open \"$filename\": $!"; last; }; local $/="\n"; my @keys=map((/^\s*default-key\s+(\S+)\s*$/),); @keys==1 or do { warn "Found ".scalar(@keys)." 'default-key's in your \"$filename\", ignoring"; last; }; close F or warn "Close \"$filename\": $!"; my $default_key=$keys[0]; $default_key=~/^[[:xdigit:]]{8}$/ or do { warn "Invalid 'default-key', ignoring: $default_key"; last; }; return Mail::Address->new( $phrase, 'pgp-'.uc($default_key).'@jankratochvil.net', ); } # !$is_pgp or fallback return Mail::Address->new( $phrase, (!$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 () { 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_Q; my $opt_q; 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, "Q:s" ,\$opt_Q, "q:s" ,\$opt_q, "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") || defined $opt_q # MD_QUEUERUN || defined $opt_Q # MD_QUEUERUN ) { @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=MIME::Head->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); for (@h_from) { $from_headername=$_; # leave last item in $from_headername next if !(my @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)); last; } # fallthru with $from_headername remaining set if last headername did not exist # now $from_headername contains the header name to be replaced w/substituted value } # to be utilized later by &FromAddress $is_pgp=(1 && do { local $_=$head->mime_attr("Content-Type"); $_ && ~m#^multipart/(?:signed|encrypted)$#; } && do { local $_=$head->mime_attr("Content-Type.protocol"); $_ && ~m#^application/pgp\b#; } ); my $exitcode=0; # !defined($rcpt) if we have no recipients # make the list unique to prevent dupes being normally filtered by sendmail(8) # one '{' is block-wrapper, another '{' is hash-indirection! # hash keys are just strings, never refs! # unify the list as Mail::Address instances my @rcpts=(!@addr ? (undef()) : values(%{{ map({ my $obj=$_; $obj=parseone $obj if !ref $obj; (!defined $obj ? () : (lc($obj->address())=>$obj)); } @addr) }})); my $stdin_body=(@rcpts<=1 ? undef() : do { # store input data only if it will be used multiple times local $/=undef(); ; }); for my $rcpt (@rcpts) { local @ARGV=@ARGV; local $opt_f=$opt_f; if (defined $rcpt) { # !defined($rcpt) if we have no recipients local $_; $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; push @ARGV,@addr_addon; 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 SENDMAIL "\n"; # MIME::Head->print() eats the empty line but it doesn't print it if (defined($stdin_body)) { print SENDMAIL $stdin_body; } else { local $_; while () { 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;