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