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