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