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