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