Add option '-s': pipe article to script.
[usenet/yapfaq.git] / yapfaq.pl
... / ...
CommitLineData
1#! /usr/bin/perl -W
2#
3# yapfaq Version 0.6 by Thomas Hochstein
4# (Original author: Marc Brockschmidt)
5#
6# This script posts any project described in its config-file. Most people
7# will use it in combination with cron(8).
8#
9# Copyright (C) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
10# Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
11#
12# It can be redistributed and/or modified under the same terms under
13# which Perl itself is published.
14
15my $Version = "0.6.2";
16
17my $NNTPServer = "localhost";
18my $NNTPUser = "";
19my $NNTPPass = "";
20my $Sender = "";
21my $ConfigFile = "yapfaq.cfg";
22my $UsePGP = 0;
23
24################################## PGP-Config #################################
25
26my $pgp = '/usr/bin/pgp'; # path to pgp
27my $PGPVersion = '2'; # Use 2 for 2.X, 5 for PGP > 2.X and GPG for GPG
28
29my $PGPSigner = ''; # sign as who?
30my $PGPPass = ''; # pgp2 only
31my $PathtoPGPPass = ''; # pgp2, pgp5 and gpg
32
33
34my $pgpbegin ='-----BEGIN PGP SIGNATURE-----';# Begin of PGP-Signature
35my $pgpend ='-----END PGP SIGNATURE-----'; # End of PGP-Signature
36my $pgptmpf ='pgptmp'; # temporary file for PGP.
37my $pgpheader ='X-PGP-Sig';
38
39my @PGPSignHeaders = ('From', 'Newsgroups', 'Subject', 'Control',
40 'Supersedes', 'Followup-To', 'Date', 'Sender', 'Approved',
41 'Message-ID', 'Reply-To', 'Cancel-Lock', 'Cancel-Key',
42 'Also-Control', 'Distribution');
43
44my @PGPorderheaders = ('from', 'newsgroups', 'subject', 'control',
45 'supersedes', 'followup-To', 'date', 'organization', 'lines',
46 'sender', 'approved', 'distribution', 'message-id',
47 'references', 'reply-to', 'mime-version', 'content-type',
48 'content-transfer-encoding', 'summary', 'keywords', 'cancel-lock',
49 'cancel-key', 'also-control', 'x-pgp', 'user-agent');
50
51############################# End of Configuration #############################
52
53use strict;
54use Net::NNTP;
55use Net::Domain qw(hostfqdn);
56use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
57use Fcntl ':flock'; # import LOCK_* constants
58use Getopt::Std;
59my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
60
61# read commandline options
62my %Options;
63getopts('Vhvpdt:f:s:', \%Options);
64# -V: print version / copyright information
65if ($Options{'V'}) {
66 print "$0 v $Version\nCopyright (c) 2003 Marc Brockschmidt <marc\@marcbrockschmidt.de>\nCopyright (c) 2010 Thomas Hochstein <thh\@inter.net>\n";
67 print "This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.\n";
68 exit(0);
69}
70# -h: feed myself to perldoc
71if ($Options{'h'}) {
72 exec ('perldoc', $0);
73 exit(0);
74};
75# -f: set $Faq
76my ($Faq) = $Options{'f'} if ($Options{'f'});
77
78# read configuration (configured FAQs)
79my @Config;
80readconfig (\$ConfigFile, \@Config, \$Faq);
81
82# for each FAQ:
83# - parse configuration
84# - read status data
85# - if FAQ is due: call postfaq()
86foreach (@Config) {
87 my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date
88 my ($NPY,$NPM,$NPD); #NP: Next posting-date
89 my $SupersedeMID;
90
91 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
92 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
93 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
94 my ($Supersede) =($$_{'supersede'});
95
96 # -f: loop if not FAQ to post
97 next if (defined($Faq) && $ActName ne $Faq);
98
99 # read status data
100 if (open (FH, "<$File.cfg")) {
101 while(<FH>){
102 if (/##;; Lastpost:\s*(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/){
103 ($LPD, $LPM, $LPY) = ($1, $2, $3);
104 } elsif (/^##;;\s*LastMID:\s*(<\S+@\S+>)\s*$/) {
105 $SupersedeMID = $1;
106 }
107 }
108 close FH;
109 } else {
110 warn "$0: W: Couldn't open $File.cfg: $!\n";
111 }
112
113 $SupersedeMID = "" unless $Supersede;
114
115 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
116
117 # if FAQ is due: get it out
118 if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) {
119 if($Options{'d'}) {
120 print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'};
121 } else {
122 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Sender,\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
123 }
124 } elsif($Options{'v'}) {
125 print "$ActName: Nothing to do.\n";
126 }
127}
128
129exit;
130
131################################## readconfig ##################################
132# Takes a filename, a reference to an array, which will hold hashes with
133# the data from $File, and - optionally - the name of the (single) FAQ to post
134
135sub readconfig{
136 my ($File, $Config, $Faq) = @_;
137 my ($LastEntry, $Error, $i) = ('','',0);
138
139 print "Reading configuration.\n" if($Options{'v'});
140
141 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
142 while (<FH>) {
143 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
144 if (/^(\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)|^(.*?)'?\s*(#.*$|$))/ && not /^\s*$/) {
145 $LastEntry = lc($2) if $2;
146 $$Config[$i]{$LastEntry} .= $3 if $3;
147 $$Config[$i]{$LastEntry} .= "\n$5" if $5 && $5;
148 }
149 if (/^\s*=====\s*$/) {
150 $i++;
151 }
152 }
153 close FH;
154
155 #Check saved values:
156 for $i (0..$i){
157 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
158 unless(defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} =~ /^\S+$/) {
159 $Error .= "E: The name of your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
160 }
161 unless(defined($$Config[$i]{'file'}) && -f $$Config[$i]{'file'}) {
162 $Error .= "E: The file to post for your project \"$$Config[$i]{'name'}\" is not defined or does not exist.\n"
163 }
164 unless(defined($$Config[$i]{'from'}) && $$Config[$i]{'from'} =~ /\S+\@(\S+\.)?\S{2,}\.\S{2,}/) {
165 $Error .= "E: The From header for your project \"$$Config[$i]{'name'}\" seems to be incorrect.\n"
166 }
167 unless(defined($$Config[$i]{'ngs'}) && $$Config[$i]{'ngs'} =~ /^\S+$/) {
168 $Error .= "E: The Newsgroups header for your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
169 }
170 unless(defined($$Config[$i]{'subject'})) {
171 $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n"
172 }
173 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
174 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
175 }
176 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
177 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
178 }
179 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
180 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
181 }
182 unless(defined($$Config[$i]{'mid-format'}) && $$Config[$i]{'mid-format'} =~ /^<\S+\@\S{2,}\.\S{2,}>$/) {
183 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n";
184 }
185 }
186 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
187 die $Error if $Error;
188}
189
190################################# calcdelta #################################
191# Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...)
192# and adds the latter to the former
193
194sub calcdelta {
195 my ($Year, $Month, $Day, $Period) = @_;
196 my ($NYear, $NMonth, $NDay);
197
198 if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days.
199 ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1));
200 } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM
201 ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0)));
202 }
203 return ($NYear, $NMonth, $NDay);
204}
205
206################################## postfaq ##################################
207# Takes a filename and many other vars.
208#
209# It reads the data-file $File and then posts the article.
210
211sub postfaq {
212 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
213 my (@Header,@Body,$MID,$InRealBody,$LastModified);
214
215 print "$$ActName: Preparing to post.\n" if($Options{'v'});
216
217 #Prepare MID:
218 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
219 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
220
221 $MID = $$MIDF;
222 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID);
223 $MID =~ s/\%n/$$ActName/g;
224 $MID =~ s/\%d/$$TDD/g;
225 $MID =~ s/\%m/$$TDM/g;
226 $MID =~ s/\%y/$$TDY/g;
227
228 #Now get the body:
229 open (FH, "<$$File");
230 while (<FH>){
231 s/\r//;
232 push (@Body, $_), next if $InRealBody;
233 $InRealBody++ if /^$/;
234 $LastModified = $1 if /^Last-modified: (\S+)$/i;
235 push @Body, $_;
236 }
237 close FH;
238 push @Body, "\n" if ($Body[-1] ne "\n");
239
240 #Create Date- and Expires-Header:
241 my @time = localtime;
242 my $ss = ($time[0]<10) ? "0" . $time[0] : $time[0];
243 my $mm = ($time[1]<10) ? "0" . $time[1] : $time[1];
244 my $hh = ($time[2]<10) ? "0" . $time[2] : $time[2];
245 my $day = $time[3];
246 my $month = ($time[4]+1<10) ? "0" . ($time[4]+1) : $time[4]+1;
247 my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
248 my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
249 my $year = (1900 + $time[5]);
250 my $tz = $time[8] ? " +0200" : " +0100";
251
252 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
253
254 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
255 my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1];
256
257 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
258 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
259
260 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
261 if ($LastModified) {
262 $$Subject =~ s/\%LM/$LastModified/;
263 }
264
265 # Test mode?
266 if($Options{'t'} and $Options{'t'} !~ /console/i) {
267 $$NG = $Options{'t'};
268 }
269
270 #Now create the complete Header:
271 push @Header, "From: $$From\n";
272 push @Header, "Newsgroups: $$NG\n";
273 push @Header, "Followup-To: $$Fup2\n" if $$Fup2;
274 push @Header, "Subject: $$Subject\n";
275 push @Header, "Message-ID: $MID\n";
276 push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes;
277 push @Header, "Date: $date\n";
278 push @Header, "Expires: $expdate\n";
279 push @Header, "Sender: $$Sender\n" if $$Sender;
280 push @Header, "Mime-Version: 1.0\n";
281 push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
282 push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
283 push @Header, "Content-Transfer-Encoding: 8bit\n";
284 push @Header, "User-Agent: yapfaq/$Version\n";
285 if ($$ExtraHeaders) {
286 push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
287 }
288
289 # sign article if $UsePGP is true
290 my @Article = ($UsePGP)?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
291
292 # post article
293 print "$$ActName: Posting article ...\n" if($Options{'v'});
294 post(\@Article);
295
296 # Test mode?
297 return if($Options{'t'});
298
299 # otherwise: update status data
300 print "$$ActName: Save status information.\n" if($Options{'v'});
301
302 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
303 print FH "##;; Lastpost: $day.$month.$year\n";
304 print FH "##;; LastMID: $MID\n";
305 close FH;
306}
307
308################################## post ##################################
309# Takes a complete article (Header and Body).
310#
311# It opens a connection to $NNTPServer and posts the message.
312
313sub post {
314 my ($ArticleR) = @_;
315
316 # Test mode?
317 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
318 print "-----BEGIN--------------------------------------------------\n";
319 print @$ArticleR;
320 print "------END---------------------------------------------------\n";
321 return;
322 }
323
324 # pipe to script?
325 if(defined($Options{'s'})) {
326 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
327 print POST @$ArticleR;
328 close POST;
329 return;
330 }
331
332 my $NewsConnection = Net::NNTP->new($NNTPServer, Reader => 1)
333 or die "$0: E: Can't connect to news server '$NNTPServer'!\n";
334
335 $NewsConnection->authinfo ($NNTPUser, $NNTPPass) if (defined($NNTPUser));
336 $NewsConnection->post();
337 $NewsConnection->datasend (@$ArticleR);
338 $NewsConnection->dataend();
339
340 # Posting failed? Save to ERROR.dat
341 if (!$NewsConnection->ok()) {
342 open FH, ">>ERROR.dat";
343 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
344 print FH $NewsConnection->code();
345 print FH $NewsConnection->message();
346 print FH "\n";
347 print FH @$ArticleR;
348 print FH "-" x 80, "\n";
349 close FH;
350 }
351
352 $NewsConnection->quit();
353}
354
355#-------- sub getpgpcommand
356# getpgpcommand generates the command to sign the message and returns it.
357#
358# Receives:
359# - $PGPVersion: A scalar holding the PGPVersion
360sub getpgpcommand {
361 my ($PGPVersion) = @_;
362 my $PGPCommand;
363
364 if ($PGPVersion eq '2') {
365 if ($PathtoPGPPass && !$PGPPass) {
366 open (PGPPW, $PathtoPGPPass) or die "$0: E: Can't open $PathtoPGPPass: $!";
367 $PGPPass = <PGPPW>;
368 close PGPPW;
369 }
370
371 if ($PGPPass) {
372 $PGPCommand = "PGPPASS=\"".$PGPPass."\" ".$pgp." -u \"".$PGPSigner."\" +verbose=0 language='en' -saft <".$pgptmpf.".txt >".$pgptmpf.".txt.asc";
373 } else {
374 die "$0: E: PGP-Passphrase is unknown!\n";
375 }
376 } elsif ($PGPVersion eq '5') {
377 if ($PathtoPGPPass) {
378 $PGPCommand = "PGPPASSFD=2 ".$pgp."s -u \"".$PGPSigner."\" -t --armor -o ".$pgptmpf.".txt.asc -z -f < ".$pgptmpf.".txt 2<".$PathtoPGPPass;
379 } else {
380 die "$0: E: PGP-Passphrase is unknown!\n";
381 }
382 } elsif ($PGPVersion =~ m/GPG/io) {
383 if ($PathtoPGPPass) {
384 $PGPCommand = $pgp." --digest-algo MD5 -a -u \"".$PGPSigner."\" -o ".$pgptmpf.".txt.asc --no-tty --batch --passphrase-fd 2 2<".$PathtoPGPPass." --clearsign ".$pgptmpf.".txt";
385 } else {
386 die "$0: E: Passphrase is unknown!\n";
387 }
388 } else {
389 die "$0: E: Unknown PGP-Version $PGPVersion!";
390 }
391 return $PGPCommand;
392}
393
394
395#-------- sub signarticle
396# signarticle signs an articel and returns a reference to an array
397# containing the whole signed Message.
398#
399# Receives:
400# - $HeaderAR: A reference to a array containing the articles headers.
401# - $BodyR: A reference to an array containing the body.
402#
403# Returns:
404# - $MessageRef: A reference to an array containing the whole message.
405sub signpgp {
406 my ($HeaderAR, $BodyR) = @_;
407 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
408
409 foreach my $line (@$HeaderAR) {
410 if ($line =~ /^(\S+):\s+(.*)$/s) {
411 $currentheader = $1;
412 $$HeaderR{lc($currentheader)} = "$1: $2";
413 } else {
414 $$HeaderR{lc($currentheader)} .= $line;
415 }
416 }
417
418 foreach (@PGPSignHeaders) {
419 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
420 push @signheaders, $_;
421 }
422 }
423
424 $pgpbody = join ("", @$BodyR);
425
426 # Delete and create the temporary pgp-Files
427 unlink "$pgptmpf.txt";
428 unlink "$pgptmpf.txt.asc";
429 $signheaders = join(",", @signheaders);
430
431 $pgphead = "X-Signed-Headers: $signheaders\n";
432 foreach $header (@signheaders) {
433 if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
434 $pgphead .= $header.": ".$1."\n";
435 }
436 }
437
438 open(FH, ">" . $pgptmpf . ".txt") or die "$0: E: can't open $pgptmpf: $!\n";
439 print FH $pgphead, "\n", $pgpbody;
440 print FH "\n" if ($PGPVersion =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
441 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
442
443 # Start PGP, then read the signature;
444 my $PGPCommand = getpgpcommand($PGPVersion);
445 `$PGPCommand`;
446
447 open (FH, "<" . $pgptmpf . ".txt.asc") or die "$0: E: can't open ".$pgptmpf.".txt.asc: $!\n";
448 $/ = "$pgpbegin\n";
449 $_ = <FH>;
450 unless (m/\Q$pgpbegin\E$/o) {
451# unlink $pgptmpf . ".txt";
452# unlink $pgptmpf . ".txt.asc";
453 die "$0: E: $pgpbegin not found in ".$pgptmpf.".txt.asc\n"
454 }
455 unlink($pgptmpf . ".txt") or warn "$0: W: Couldn't unlink $pgptmpf.txt: $!\n";
456
457 $/ = "\n";
458 $_ = <FH>;
459 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
460 unlink $pgptmpf . ".txt";
461 unlink $pgptmpf . ".txt.asc";
462 die "$0: E: didn't find PGP Version line where expected.\n";
463 }
464
465 if (defined($2)) {
466 $$HeaderR{$pgpheader} = $1."-".$2." ".$signheaders;
467 } else {
468 $$HeaderR{$pgpheader} = $1." ".$signheaders;
469 }
470
471 do { # skip other pgp headers like
472 $_ = <FH>; # "charset:"||"comment:" until empty line
473 } while ! /^$/;
474
475 while (<FH>) {
476 chomp;
477 last if /^\Q$pgpend\E$/;
478 $$HeaderR{$pgpheader} .= "\n\t$_";
479 }
480
481 $$HeaderR{$pgpheader} .= "\n" unless ($$HeaderR{$pgpheader} =~ /\n$/s);
482
483 $_ = <FH>;
484 unless (eof(FH)) {
485 unlink $pgptmpf . ".txt";
486 unlink $pgptmpf . ".txt.asc";
487 die "$0: E: unexpected data following $pgpend\n";
488 }
489 close(FH);
490 unlink "$pgptmpf.txt.asc";
491
492 my $tmppgpheader = $pgpheader . ": " . $$HeaderR{$pgpheader};
493 delete $$HeaderR{$pgpheader};
494
495 @pgphead = ();
496 foreach $header (@PGPorderheaders) {
497 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
498 push(@pgphead, "$$HeaderR{$header}");
499 delete $$HeaderR{$header};
500 }
501 }
502
503 foreach $header (keys %$HeaderR) {
504 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
505 push(@pgphead, "$$HeaderR{$header}");
506 delete $$HeaderR{$header};
507 }
508 }
509
510 push @pgphead, ("X-PGP-Key: " . $PGPSigner . "\n"), $tmppgpheader;
511 undef $tmppgpheader;
512
513 @pgpbody = split /$/m, $pgpbody;
514 my @pgpmessage = (@pgphead, "\n", @pgpbody);
515 return \@pgpmessage;
516}
517
518__END__
519
520################################ Documentation #################################
521
522=head1 NAME
523
524yapfaq - Post Usenet FAQs I<(yet another postfaq)>
525
526=head1 SYNOPSIS
527
528B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>]
529
530=head1 REQUIREMENTS
531
532=over 2
533
534=item -
535
536Perl 5.8 or later
537
538=item -
539
540Net::NNTP
541
542=item -
543
544Date::Calc
545
546=item -
547
548Getopt::Std
549
550=back
551
552Furthermore you need access to a news server to actually post FAQs.
553
554=head1 DESCRIPTION
555
556B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
557frequency (every n days, weeks, months or years), adding all necessary
558headers as defined in its config file (by default F<yapfaq.cfg>).
559
560=head2 Configuration
561
562F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
563a single line, each containing the configuration for one FAQ as a set
564of definitions in the form of I<param = value>.
565
566=over 4
567
568=item B<Name> = I<project name>
569
570A name referring to your FAQ, also used for generation of a Message-ID.
571
572This value must be set.
573
574=item B<File> = I<file name>
575
576A file containing the message body of your FAQ and all pseudo headers
577(subheaders in the news.answers style).
578
579This value must be set.
580
581=item B<Posting-frequency> = I<time period>
582
583The posting frequency defines how often your FAQ will be posted.
584B<yapfaq> will only post your FAQ if this period of time has passed
585since the last posting.
586
587You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
588or I<B<m>onths> or I<B<y>ears>.
589
590This value must be set.
591
592=item B<Expires> = I<time period>
593
594The period of time after which your message will expire. An Expires
595header will be calculated adding this time period to today's date.
596
597You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
598or I<B<m>onths> or I<B<y>ears>.
599
600This setting is optional; the default is 3 months.
601
602=item B<From> = I<author>
603
604The author of your FAQ as it will appear in the From header of the
605message.
606
607This value must be set.
608
609=item B<Subject> = I<subject>
610
611The title of your FAQ as it will appear in the Subject header of the
612message.
613
614You may use the special string C<%LM> which will be replaced with
615the contents of the Last-Modified subheader in your I<File>.
616
617This value must be set.
618
619=item B<NGs> = I<newsgroups>
620
621A comma-separated list of newsgroup(s) to post your FAQ to as it will
622appear in the Newsgroups header of the message.
623
624This value must be set.
625
626=item B<Fup2> = I<newsgroup | poster>
627
628A comma-separated list of newsgroup(s) or the special string I<poster>
629as it will appear in the Followup-To header of the message.
630
631This setting is optional.
632
633=item B<MID-Format> = I<pattern>
634
635A pattern from which the message ID is generated as it will appear in
636the Message-ID header of the message.
637
638You may use the special strings C<%n> for the I<Name> of your project,
639C<%d> for the date the message is posted, C<%m> for the month and
640C<%y> for the year, respectively.
641
642This value must be set.
643
644=item B<Supersede> = I<yes>
645
646Add Supersedes header to the message containing the Message-ID header
647of the last posting.
648
649This setting is optional; you should set it to yes or leave it out.
650
651=item B<ExtraHeader> = I<additional headers>
652
653The contents of I<ExtraHeader> is added verbatim to the headers of
654your message so you can add custom headers like Approved.
655
656This setting is optional.
657
658=back
659
660=head2 Example configuration file
661
662 # name of your project
663 Name = 'testpost'
664
665 # file to post (complete body and pseudo-headers)
666 # ($File.cfg contains data on last posting and last MID)
667 File = 'test.txt'
668
669 # how often your project should be posted
670 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
671 Posting-frequency = '1d'
672
673 # time period after which the posting should expire
674 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
675 Expires = '3m'
676
677 # header "From:"
678 From = 'test@domain.invalid'
679
680 # header "Subject:"
681 # (may contain "%LM" which will be replaced by the contents of the
682 # Last-Modified pseudo header).
683 Subject = 'test noreply ignore'
684
685 # comma-separated list of newsgroup(s) to post to
686 # (header "Newsgroups:")
687 NGs = 'de.test'
688
689 # header "Followup-To:"
690 Fup2 = 'poster'
691
692 # Message-ID ("%n" is $Name)
693 MID-Format = '<%n-%d.%m.%y@domain.invalid>'
694
695 # Supersede last posting?
696 Supersede = yes
697
698 # extra headers (appended verbatim)
699 # use this for custom headers like "Approved:"
700 ExtraHeader = 'Approved: moderator@domain.invalid
701 X-Header: Some text'
702
703 # other projects may follow separated with "====="
704 =====
705
706 Name = 'othertest'
707 File = 'test.txt'
708 Posting-frequency = '2m'
709 From = 'My Name <my.name@domain.invalid>'
710 Subject = 'Test of yapfag <%LM>'
711 NGs = 'de.test,de.alt.test'
712 Fup2 = 'de.test'
713 MID-Format = '<%n-%m.%y@domain.invalid>'
714 Supersede = yes
715
716Information about the last post and about how to form message IDs for
717posts is stored in a file named F<I<project name>.cfg> which will be
718generated if it does not exist. Each of those status files will
719contain two lines, the first being the date of the last time the FAQ
720was posted and the second being the message ID of that incarnation.
721
722=head1 OPTIONS
723
724=over 3
725
726=item B<-V> (version)
727
728Print out version and copyright information on B<yapfaq> and exit.
729
730=item B<-h> (help)
731
732Print this man page and exit.
733
734=item B<-v> (verbose)
735
736Print out status information while running to STDOUT.
737
738=item B<-p> (post unconditionally)
739
740Post (all) FAQs unconditionally ignoring the posting frequency setting.
741
742You may want to use this with the B<-f> option (see below).
743
744=item B<-d> (dry run)
745
746Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
747update any status information.
748
749=item B<-t> I<newsgroup(s) | CONSOLE> (test)
750
751Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
752newsgroups given after B<-t> as a comma-separated list or print the
753FAQs to STDOUT separated by lines of dashes if the special string
754C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
755do without embarassing yourself on Usenet. The status files are not
756updated when this option is given.
757
758You may want to use this with the B<-f> option (see below).
759
760=item B<-f> I<project name>
761
762Just deal with one FAQ only.
763
764By default B<yapfaq> will work on all FAQs that are defined in
765F<yapfaq.cfg>, check whether they are due for posting and - if they
766are - post them. Consequently when the B<-p> option is set all FAQs
767will be posted unconditionally. That may not be what you want to
768achieve, so you can limit the operation of B<yapfaq> to the named FAQ
769only.
770
771=item B<-s> I<program> (pipe to script)
772
773Instead of posting the article(s) to Usenet pipe them to the external
774I<program> on STDIN (which may post the article(s) then). A return
775value of 0 will be considered success.
776
777=back
778
779=head1 EXAMPLES
780
781Post all FAQs that are due for posting:
782
783 yapfaq
784
785Do a dry run, showing which FAQs would be posted:
786
787 yapfaq -dv
788
789Do a test run and print on STDOUT what the FAQ I<myfaq> would look
790like when posted, regardless whether it is due for posting or not:
791
792 yapfaq -pt CONSOLE -f myfaq
793
794Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
795if it is due:
796
797 yapfaq -t de.test -f myfaq
798
799=head1 ENVIRONMENT
800
801There are no special environment variables used by B<yapfaq>.
802
803=head1 FILES
804
805=over 4
806
807=item F<yapfaq.pl>
808
809The script itself.
810
811=item F<yapfaq.cfg>
812
813Configuration file for B<yapfaq>.
814
815=item F<*.cfg>
816
817Status data on FAQs.
818
819The status files will be created on successful posting if they don't
820already exist. The first line of the file will be the date of the last
821time the FAQ was posted and the second line will be the message ID of
822the last post of that FAQ.
823
824=back
825
826=head1 BUGS
827
828Many, I'm sure.
829
830=head1 SEE ALSO
831
832L<http://th-h.de/download/scripts.php> will have the current
833version of this program.
834
835=head1 AUTHOR
836
837Thomas Hochstein <thh@inter.net>
838
839Original author (until version 0.5b from 2003):
840Marc Brockschmidt <marc@marcbrockschmidt.de>
841
842
843=head1 COPYRIGHT AND LICENSE
844
845Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
846
847Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
848
849This program is free software; you may redistribute it and/or modify it
850under the same terms as Perl itself.
851
852=cut
This page took 0.012556 seconds and 4 git commands to generate.