First production sendmail(8) wrapper
[PerlMail.git] / perlmail-sendmail
1 #! /usr/bin/perl
2 #
3 # $Id$
4
5 use vars qw($VERSION);
6 $VERSION=do { my @r=(q$Revision$=~/\d+/g); sprintf "%d.".("%03d"x$#r),@r; };
7 use strict;
8 use warnings;
9
10 require Getopt::Long;
11 use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG WIFSTOPPED WSTOPSIG);
12 require Mail::Header;
13 require Mail::Address;
14 require File::Basename;
15
16 my $sendmail_orig=(-x ($_="/usr/sbin/sendmail-orig") ? $_ : "/usr/sbin/sendmail");
17 my $HOME="/home/lace";
18 my $opt_F;
19 sub FromAddress
20 {
21 my($rcpt,$iserror)=@_;
22
23         return Mail::Address->new(
24                         (defined $opt_F ? $opt_F : "Jan Kratochvil"),
25                         (!$iserror ? 'rcpt' : 'rcpterr')
26                                         .'-'
27                                         .(defined($rcpt->user()) ? $rcpt->user() : "NOUSER")
28                                         .".AT."
29                                         .(defined($rcpt->host()) ? $rcpt->host() : "LOCAL")
30                                         .'@jankratochvil.net',
31                         );
32 }
33
34 # RedHat sendmail-8.9.3-20/src/conf.c/HdrInfo[]/\Q/* destination fields */\E
35 # FIXME: Recognize "Resent-$_" headers for -t but when we are in 'resent' mode?
36 my @h_rcpt=(    # case in-sensitive!
37                 "To",
38                 "Cc",
39                 "Bcc",
40                 "Apparently-To",
41                 );
42
43 # ordering matters; first header found is substituted
44 # last header is subsituted if no one is found
45 my @h_from=(
46                 "Resent-From",
47                 "From",
48                 );
49
50
51 # FIXME: modularized unification with 'lacemail-accept'
52 # BEGIN lacemail-accept
53 our %muttrc_pending=();
54 sub muttrc
55 {
56 my($muttrc)=@_;
57
58         $muttrc||="$HOME/.muttrc";
59         $muttrc=~s/^\~/$HOME/;
60         do { warn "Looping muttrc, ignoring: $muttrc"; return (); } if $muttrc_pending{$muttrc};
61         local $muttrc_pending{$muttrc}=1;
62         local *MUTTRC;
63         open MUTTRC,$muttrc or do { warn "open \"$muttrc\": $!"; return (); };
64         local $/="\n";
65         local $_;
66         my @r=();
67         # far emulation mutt/init.c/mutt_parse_rc_line()
68         while (<MUTTRC>) {
69                 s/^[\s;]*//s;
70                 s/[#;].*$//s;
71                 s/\s*$//s;
72                 next if !/^(\S+)\s*/s;
73                 if ($1 eq "source") {
74                         $_=$';
75                         do { warn "Wrong 'source' parameters at $muttrc:$.: $_"; next; } if !/^\S+$/;
76                         push @r,muttrc($_);
77                         next;
78                         }
79                 push @r,$_;
80                 }
81         close MUTTRC or warn "close \"$muttrc\": $!";
82         return wantarray() ? @r : join("",map("$_\n",@r));
83 }
84
85 my %mutteval_charmap=(          # WARNING: Don't use "" or "0" here, see below for "|| warn"!
86                 '\\'=>"\\",
87                 'r'=>"\r",
88                 'n'=>"\n",
89                 't'=>"\t",
90                 'f'=>"\f",
91                 'e'=>"\e",
92                 );
93 # mutt/init.c/mutt_extract_token()
94 sub mutteval
95 {
96         local $_=$_[0];
97         return $_ if !s/^"//;
98         do { warn "Missing trailing quote in: $_"; return $_; } if !s/"$//;
99         s/\\(.)/$mutteval_charmap{$1} || warn "Undefined '\\$1' sequence in: $_";/ges;
100         return $_;
101 }
102
103 sub muttrc_get
104 {
105 my(@headers)=@_;
106
107         my @r=map({ (ref $_ ? $_ : qr/^\s*set\s+\Q$_\E\s*=\s*(.*?)\s*$/si); } @headers);
108         my %r=map(($_=>undef()),@r);
109         for (muttrc()) {
110                 for my $ritem (@r) {
111                         /$ritem/si or next;
112                         $r{$ritem}=mutteval $1;
113                         }
114                 }
115         for my $var (grep { !defined($r{$_}) } @r) {
116                 warn "Variable '$var' not found in muttrc";
117                 return undef();
118                 }
119         return wantarray() ? %r : $r{$r[0]};
120 }
121 # END lacemail-accept
122
123
124 sub sendmail_show { return "\"$sendmail_orig\" ".join(",",map("\"$_\"",@ARGV)); }
125
126 sub sendmail_orig_exec
127 {
128         exec {$sendmail_orig} $0,@ARGV or die "exec(".sendmail_show()."): $!";
129         die "NOTREACHED";
130 }
131
132 Getopt::Long::Configure(
133                 "no_ignorecase",
134                 "no_getopt_compat",
135                 "bundling",
136                 # FIXME: workaround: 'unknown options' are considered the same as 'arguments'
137                 # None of ($REQUIRE_ORDER, $PERMUTE, $RETURN_IN_ORDER) can help us.
138                 # No preprocessing possible as it is hard to find option arguments.
139                 "permute",
140                 "pass_through",
141                 );
142
143 my $opt_b;
144 my $opt_t;
145 our $opt_f;
146 #my $opt_F;     # declared before &FromAddress already
147 my $opt_lacemail_dry_run;
148 my @ARGV_save=@ARGV;    # for non-bm mode
149 die if !Getopt::Long::GetOptions(
150                 "b=s"              ,\$opt_b,
151                 "t"                ,\$opt_t,
152                 "f=s"              ,\$opt_f,
153                 "F=s"              ,\$opt_F,
154                 "lacemail-dry-run+",\$opt_lacemail_dry_run,
155                 );
156 if (0
157                 # RedHat sendmail-8.12.5-7/sendmail/main.c/\QDo a quick prescan of the argument list.\E
158                 || grep({ File::Basename::basename($0) eq $_; } "newaliases","mailq","smtpd","hoststat","purgestat")
159                 # -bm: Deliver mail in the usual way (default).
160                 || (defined($opt_b) && $opt_b ne "m")
161                 ) {
162         @ARGV=@ARGV_save;
163         sendmail_orig_exec();
164         die "NOTREACHED";
165         }
166
167 # RedHat sendmail-8.9.3-20/src/main.c/main()/\Qif (FullName != NULL)\E
168 #   for $opt_F is implemented by Mail::Address in our &FromAddress
169
170 my $head=Mail::Header->new(\*STDIN);
171 # We may (=will) change the contents and send it multiple times
172 if (defined(my $msgid=$head->get("Message-ID"))) {
173         $head->delete("Message-ID");
174         $head->replace("X-LaceMail-sendmail-Message-ID",$msgid);
175         }
176 # options leave in @ARGV, addresses to @addr:
177 my @args=@ARGV; # temporary
178 @ARGV=();       # options
179 my @addr=();    # addresses
180 push @{(/^-./ ? \@ARGV : \@addr)},$_ for (@args);
181 if ($opt_t) {
182         for my $addrobj (map({ Mail::Address->parse($_); } map({ ($head->get($_)); } @h_rcpt))) {
183                 if (!$addrobj->address()) {
184                         # bogus, shouldn't happen
185                         warn "->address() not found in \"".$addrobj->format()."\"";
186                         next;
187                 }
188                 push @addr,$addrobj;
189                 }
190         }
191
192 # return: Mail::Address instance or undef()
193 sub parseone
194 {
195 my($line)=@_;
196
197         return undef() if !defined $line;
198         my @r=Mail::Address->parse($line);
199         warn "Got ".scalar(@r)." addresses while wanting just one; when parsing: $line" if 1!=@r;
200         return $r[0];
201 }
202
203 sub matches
204 {
205         return 
206 }
207
208 my $from_headername;
209 {
210         my $muttrc_From=parseone(scalar muttrc_get("from"));    # may get undef()!; parseone() may be redundant
211         $muttrc_From=$muttrc_From->address() if $muttrc_From;
212         $opt_f=undef() if defined($opt_f) && $muttrc_From && lc($opt_f) eq lc($muttrc_From);
213         my @from_val;
214         for (@h_from) {
215                 $from_headername=$_;    # leave last item in $from_headername
216                 last if @from_val=$head->get($from_headername);
217                 }
218         @from_val=map({ ($_->address()); } map({ (Mail::Address->parse($_)); } @from_val));
219         $from_headername=undef() if !(1==@from_val && $muttrc_From && lc($from_val[0]) eq lc($muttrc_From));
220         # now $from_headername contains the header name to be replaced w/substituted value
221         }
222
223 my $exitcode=0;
224 my @rcpts=(@addr ? @addr : (undef()));  # !defined($rcpt) if we have no recipients
225 my $stdin_body=(@rcpts<=1 ? undef() : do {      # store input data only if it will be used multiple times
226                 local $/=undef();
227                 <STDIN>;
228                 });
229 for my $rcpt (@rcpts) {
230         local @ARGV=@ARGV;
231         local $opt_f=$opt_f;
232
233         if (defined $rcpt) {    # !defined($rcpt) if we have no recipients
234                 local $_;
235                 if (!ref $rcpt) {
236                         $rcpt=parseone $rcpt;
237                         next if !defined $rcpt;
238                         }
239                 $opt_f=FromAddress($rcpt,1)->address() if !defined $opt_f;
240                 $head->replace($from_headername,FromAddress($rcpt,0)->format()) if $from_headername;
241                 }
242
243         1;      # drop '-bm' if present as it is default anyway
244         1;      # drop '-t' if present as we are looping now for it
245         push @ARGV,"-f",$opt_f if defined $opt_f;
246         # we don't handle "Full-Name" header thus pass "-F"
247         # "From/Resent-From" should be handled by our &FromAddress
248         push @ARGV,"-F",$opt_F if defined $opt_F;
249         push @ARGV,$rcpt->address() if defined $rcpt;
250
251         local $SIG{"PIPE"}=sub { die "Got SIGPIPE from ".sendmail_show(); };
252         local *SENDMAIL;
253         if ($opt_lacemail_dry_run) {
254                 print sendmail_show()."\n";
255                 *SENDMAIL=\*STDOUT;
256                 }
257         else {
258                 defined (my $pid=open SENDMAIL,"|-") or die "Cannot fork to spawn ".sendmail_show().": $!";
259                 sendmail_orig_exec() if !$pid; # child
260                 }
261         $head->print(\*SENDMAIL);
262         print "\n";     # Mail::Header->print() eats the empty line but it doesn't print it
263         if (defined($stdin_body)) {
264                 print SENDMAIL $stdin_body;
265                 }
266         else {
267                 local $_;
268                 while (<STDIN>) {
269                         print SENDMAIL $_;
270                         }
271                 }
272
273         next if $opt_lacemail_dry_run;  # don't close our STDOUT as it is aliased to *SENDMAIL
274         close SENDMAIL or warn "close(".sendmail_show()."): $?=".join(",",
275                         (!WIFEXITED($?)   ? () : ("EXITSTATUS(".WEXITSTATUS($?).")")),
276                         (!WIFSIGNALED($?) ? () : ("TERMSIG("   .WTERMSIG($?)   .")")),
277                         (!WIFSTOPPED($?)  ? () : ("STOPSIG("   .WSTOPSIG($?)   .")")),
278                         );
279         my $gotcode=(!WIFEXITED($?) ? 99 : WEXITSTATUS($?));
280         $exitcode=$gotcode if $gotcode>$exitcode;
281         }
282 exit $exitcode;