chiark / gitweb /
cdb: General drive to eliminate freecdb and libfile-cdb-perl.
[newsgate] / bin / mailpost.newsgate
CommitLineData
a682e5d7
MW
1#! /usr/bin/perl
2require '/usr/lib/news/innshellvars.pl';
3
4# mailpost - yet another mail-to-news filter
5# 21feb00 [added "lc" to duplicate header fixer stmt to make it case-insensitive]
6# doka 11may99 [fixed duplicate headers problem]
7# brister 19oct98 cleaned up somewhat for perl v. 5. and made a little more robust.
8# vixie 29jan95 RCS'd [$Id: mailpost.in,v 1.3.2.1 2000/08/13 02:03:59 rra Exp $]
9# vixie 15jun93 [added -m]
10# vixie 30jun92 [added -a and -d]
11# vixie 17jun92 [attempt simple-minded fixup to $path]
12# vixie 14jun92 [original]
13
14use Getopt::Std ;
15use IPC::Open3;
16use IO::Select;
17use Sys::Syslog;
18use strict ;
19
20my $debugging = 0 ;
21my $tmpfile ;
22my $msg ;
23
24END {
25 unlink ($tmpfile) if $tmpfile ; # incase we die()
26}
27
28my $LOCK_SH = 1;
29my $LOCK_EX = 2;
30my $LOCK_NB = 4;
31my $LOCK_UN = 8;
32
33my $usage = $0 ;
34$usage =~ s!.*/!! ;
35my $prog = $usage ;
36
37openlog $usage, "pid", $inn::syslog_facility ;
38
39$usage .= "[ -r addr ][ -f addr ][ -a approved ][ -d distribution ]" .
40 " [ -m mailing-list ][ -b database ][ -o output-path ] newsgroups" ;
41
42use vars qw($opt_r $opt_f $opt_a $opt_d $opt_m $opt_b $opt_n $opt_o $opt_h) ;
43getopts("hr:f:a:d:m:b:no:") || die "usage: $usage\n" ;
44die "usage: $usage\n" if $opt_h ;
45
46#
47# $Submit is a program which takes no arguments and whose stdin is supposed
48# to be a news article (without the #!rnews header but with the news hdr).
49#
50
51my $Sendmail = $inn::mta ;
52my $Submit = $inn::inews . " -S -h";
53my $Database = ($opt_b || $inn::pathtmp) . "/mailpost-msgid" ;
54my $Maintainer = $inn::newsmaster || "usenet" ;
55my $WhereTo = $opt_o || $Submit ;
56my $Mailname = $inn::fromhost ;
57
58# can't use $inn::tmpdir as we're usually not running as news
59my $Tmpdir = "/var/tmp" ;
60
61if ($debugging || $opt_n) {
62 $Sendmail = "cat" ;
63 $WhereTo = "cat" ;
64}
65
66chop ($Mailname = `/bin/hostname`) if ! $Mailname ;
67
68
69#
70# our command-line argument(s) are the list of newsgroups to post to.
71#
72# there may be a "-r sender" or "-f sender" which becomes the $path
73# (which is in turn overridden below by various optional headers.)
74#
75# -d (distribution) and -a (approved) are also supported to supply
76# or override the mail headers by those names.
77#
78
79my $path = 'nobody';
80my $newsgroups = undef;
81my $approved = undef;
82my $distribution = undef;
83my $mailing_list = undef;
84my $references = undef;
85my @errorText = ();
86
87if ($opt_r || $opt_f) {
88 $path = $opt_r || $opt_f ;
89 push @errorText, "((path: $path))\n" ;
90}
91
92if ($opt_a) {
93 $approved = &fix_sender_addr($opt_a);
94 push @errorText, "((approved: $approved))\n";
95}
96
97if ($opt_d) {
98 $distribution = $opt_d ;
99 push @errorText, "((distribution: $distribution))\n";
100}
101
102if ($opt_m) {
103 $mailing_list = "<" . $opt_m . "> /dev/null";
104 push @errorText, "((mailing_list: $mailing_list))\n";
105}
106
107$newsgroups = join ", ", @ARGV ;
108
109die "usage: $0 newsgroup [newsgroup]\n" unless $newsgroups;
110
111
112#
113# do the header. our input is a mail message, with or without the From_
114#
115
116#$message_id = sprintf("<mailpost.%d.%d@%s>", time, $$, $Hostname);
117my $real_news_hdrs = '';
118my $weird_mail_hdrs = '';
119my $fromHdr = "MAILPOST-UNKNOWN-FROM" ;
120my $dateHdr= "MAILPOST-UNKNOWN-DATE" ;
121my $msgIdHdr = "MAILPOST-UNKNOWN-MESSAGE-ID" ;
122my $from = undef;
123my $date = undef;
124my $hdr = undef;
125my $txt = undef;
126my $message_id ;
127my $subject = "(NONE)";
128
129$_ = <STDIN>;
130if (!$_) {
131 if ( $debugging || -t STDERR ) {
132 die "empty input" ;
133 } else {
134 syslog "err", "empty input" ;
135 exit (0) ;
136 }
137}
138
139chomp $_;
140
141my $line = undef;
142if (/^From\s+([^\s]+)\s+/) {
143 $path = $1;
144 push @errorText, "((path: $path))\n";
145 $_ = $';
146 if (/ remote from /) {
147 $path = $' . '!' . $path;
148 $_ = $`;
149 }
150 $date = $_;
151} else {
152 $line = $_;
153}
154
155for (;;) {
156 last if defined($line) && ($line =~ /^$/) ;
157
158 $_ = <STDIN> ;
159 chomp ;
160
161 # gather up a single header with possible continuation lines into $line
162 if (/^\s+/) {
163 if (! $line) {
164 $msg = "First line with leading whitespace!" ;
165 syslog "err", $msg unless -t STDERR ;
166 die "$msg\n" ;
167 }
168
169 $line .= "\n" . $_ ;
170 next ;
171 }
172
173 # On the first header $line will be undefined.
174 ($_, $line) = ($line, $_) ; # swap $line and $_ ;
175
176 last if defined($_) && /^$/ ;
177 next if /^$/ ; # only on first header will this happen
178
179 push @errorText, "($_)\n";
180
181 next if /^Approved:\s/sio && defined($approved);
182 next if /^Distribution:\s/sio && defined($distribution);
183
184 if (/^(Organization|Distribution):\s*/sio) {
185 $real_news_hdrs .= "$_\n";
186 next;
187 }
188
189 if (/^Subject:\s*/sio) {
190 $subject = $';
191 next;
192 }
193
194 if (/^Message-ID:\s*/sio) {
195 $message_id = $';
196 next;
197 }
198
199 if (/^Mailing-List:\s*/sio) {
200 $mailing_list = $';
201 next;
202 }
203
204 if (/^(Sender|Approved):\s*/sio) {
205 $real_news_hdrs .= "$&" . fix_sender_addr($') . "\n";
206 next;
207 }
208
209 if (/^Return-Path:\s*/sio) {
210 $path = $';
211 $path = $1 if ($path =~ /\<([^\>]*)\>/);
212 push@errorText, "((path: $path))\n";
213 next;
214 }
215
216 if (/^Date:\s*/sio) {
217 $date = $';
218 next;
219 }
220
221 if (/^From:\s*/sio) {
222 $from = &fix_sender_addr($');
223 next;
224 }
225
226 if (/^References:\s*/sio) {
227 $references = $';
228 next;
229 }
230
231 if (!defined($references) && /^In-Reply-To:[^\<]*\<([^\>]+)\>/sio) {
232 $references = "<$1>";
233 # FALLTHROUGH
234 }
235
236 if (/^(MIME|Content)-[^:]+:\s*/sio) {
237 $real_news_hdrs .= $_ . "\n" ;
238 next ;
239 }
240
241 # strip out news trace headers since otherwise posting may fail. other
242 # trace headers will be renamed to add 'X-' so we don't have to worry
243 # about them.
244 if (/^X-(Trace|Complaints-To):\s*/sio) {
245 next ;
246 }
247
248 # strip out Received headers since otherwise posting may fail
249 # due to too large header size.
250 if (/^(Received):\s*/sio) {
251 next ;
252 }
253
254 # random unknown header. prepend 'X-' if it's not already there.
255 $_ = "X-$_" unless /^X-/sio ;
256 $weird_mail_hdrs .= "$_\n";
257}
258
259
260$msgIdHdr = $message_id if $message_id ;
261$fromHdr = $from if $from ;
262$dateHdr = $date if $date ;
263
264if ($path !~ /\!/) {
265 $path = "$'!$`" if ($path =~ /\@/);
266}
267
268$real_news_hdrs .= "Subject: ${subject}\n";
269$real_news_hdrs .= "Message-ID: ${msgIdHdr}\n" if defined($message_id);
270$real_news_hdrs .= "Mailing-List: ${mailing_list}\n" if defined($mailing_list);
271$real_news_hdrs .= "Distribution: ${distribution}\n" if defined($distribution);
272$real_news_hdrs .= "Approved: ${approved}\n" if defined($approved);
273$real_news_hdrs .= "References: ${references}\n" if defined($references);
274
275# Remove duplicate headers.
276my %headers = ();
277$real_news_hdrs =~ s/(.*?:)[ \t].*?($|\n)([ \t]+.*?($|\n))*/$headers{lc$1}++?"":$&/ge;
278
279# Inews writes error messages to stdout. We want to capture those and mail
280# them back to the newsmaster. Trying to write and read from a subprocess is
281# ugly and prone to deadlock, so we use a temp file.
282$tmpfile = sprintf "%s/mailpost.%d.%d", $Tmpdir, time, $$ ;
283
284if (!open TMPFILE,">$tmpfile") {
285 $msg = "cant open temp file ($tmpfile): $!" ;
286 $tmpfile = undef ;
287 syslog "err", "$msg\n" unless $debugging || -t STDERR ;
288 open TMPFILE, "|" . sprintf ($Sendmail, $Maintainer) ||
289 die "die(no tmpfile): sendmail: $!\n" ;
290 print TMPFILE <<"EOF";
291To: $Maintainer
292Subject: mailpost failure ($newsgroups): $msg
293
294-------- Article Contents
295
296EOF
297}
298
299print TMPFILE <<"EOF";
300Path: ${path}
301From: ${fromHdr}
302Newsgroups: ${newsgroups}
303${real_news_hdrs}Date: ${dateHdr}
304${weird_mail_hdrs}
305EOF
306
307my $rest;
308$rest .= $_ while (<STDIN>);
309$rest =~ s/\n*$/\n/g; # Remove trailing \n except very last
310
311print TMPFILE $rest;
312close TMPFILE ;
313
314if ( ! $tmpfile ) {
315 # we had to bail and mail the article to the admin.
316 exit (0) ;
317}
318
319
320##
321## We've got the article in a temp file and now we validate some of the
322## data we found and update our message-id database.
323##
324
325mailArtAndDie ("no From: found") unless $from;
326mailArtAndDie ("no Date: found") unless $date;
327mailArtAndDie ("no Message-ID: found") unless $message_id;
328mailArtAndDie ("Malformed message ID ($message_id)")
329 if ($message_id !~ /\<(\S+)\@(\S+)\>/);
330
331
332# update (with locking) our message-id database. this is used to make sure we
333# don't loop our own gatewayed articles back through the mailing list.
334
335my ($lhs, $rhs) = ($1, $2); # of message_id match above.
336$rhs =~ tr/A-Z/a-z/;
337
338$message_id = "${lhs}\@${rhs}";
339
340push @errorText, "(TAS message-id database for $message_id)\n";
341
342my $lockfile = sprintf("%s.lock", $Database);
343
344open LOCKFILE, "<$lockfile" ||
345 open LOCKFILE, ">$lockfile" ||
346 mailArtAndDie ("can't open $lockfile: $!") ;
347
348my $i ;
349for ($i = 0 ; $i < 5 ; $i++) {
350 flock LOCKFILE, $LOCK_EX && last ;
351 sleep 1 ;
352}
353
354mailArtAndDie ("can't lock $lockfile: $!") if ($i == 5) ;
355
356my %DATABASE ;
357dbmopen %DATABASE, $Database, 0666 || mailArtAndDie ("can't dbmopen $lockfile: $!");
358
359exit 0 if defined $DATABASE{$message_id}; # already seen.
360
361$DATABASE{$message_id} = sprintf "%d.%s", time, 'mailpost' ;
362
363mailArtAndDie ("TAS didn't set $message_id") unless defined $DATABASE{$message_id};
364
365dbmclose %DATABASE || mailArtAndDie ("can't dbmclose $lockfile: $!") ;
366
367flock LOCKFILE, $LOCK_UN || mailArtAndDie ("can't unlock $lockfile: $!");
368close LOCKFILE ;
369
370if (!open INEWS, "$WhereTo < $tmpfile 2>&1 |") {
371 mailArtAndDie ("cant start: $WhereTo: $!") ;
372}
373
374my @inews = <INEWS> ;
375close INEWS ;
376my $status = $? ;
377
378if (@inews) {
379 chomp @inews ;
380 mailArtAndDie ("inews failed: @inews") ;
381}
382
383unlink $tmpfile ;
384
385exit $status;
386
387sub mailArtAndDie {
388 my ($msg) = @_ ;
389
390 print STDERR $msg,"\n" if -t STDERR ;
391
392 open SENDMAIL, "|" . sprintf ($Sendmail,$Maintainer) ||
393 die "die($msg): sendmail: $!\n" ;
394 print SENDMAIL <<"EOF" ;
395To: $Maintainer
396Subject: mailpost failure ($newsgroups): $msg
397
398$msg
399EOF
400
401 if ($tmpfile && -f $tmpfile) {
402 print SENDMAIL "\n-------- Article Contents\n\n" ;
403 open FILE, "<$tmpfile" || die "open($tmpfile): $!\n" ;
404 print SENDMAIL while <FILE> ;
405 close FILE ;
406 } else {
407 print "No article left to send back.\n" ;
408 }
409 close SENDMAIL ;
410
411# unlink $tmpfile ;
412
413 exit (0) ; # using a non-zero exit may cause problems.
414}
415
416
417#
418# take 822-format name (either "comment <addr> comment" or "addr (comment)")
419# and return in always-qualified 974-format ("addr (comment)").
420#
421sub fix_sender_addr {
422 my ($address) = @_;
423 my ($lcomment, $addr, $rcomment, $comment);
424 local ($',$`,$_) ;
425
426 if ($address =~ /\<([^\>]*)\>/) {
427 ($lcomment, $addr, $rcomment) = (&dltb($`), &dltb($1), &dltb($'));
428 } elsif ($address =~ /\(([^\)]*)\)/) {
429 ($lcomment, $addr, $rcomment) = ('', &dltb($`.$'), &dltb($1));
430 } else {
431 ($lcomment, $addr, $rcomment) = ('', &dltb($address), '');
432 }
433
434 #print STDERR "fix_sender_addr($address) == ($lcomment, $addr, $rcomment)\n";
435
436 $addr .= "\@$Mailname" unless ($addr =~ /\@/);
437
438 if ($lcomment && $rcomment) {
439 $comment = $lcomment . ' ' . $rcomment;
440 } else {
441 $comment = $lcomment . $rcomment;
442 }
443
444 $_ = $addr;
445 $_ .= " ($comment)" if $comment;
446
447 #print STDERR "\t-> $_\n";
448
449 return $_;
450}
451
452#
453# delete leading and trailing blanks
454#
455
456sub dltb {
457 my ($str) = @_;
458
459 $str =~ s/^\s+//o;
460 $str =~ s/\s+$//o;
461
462 return $str;
463}
464