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