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