Fix: Test mode must not update status information.
[usenet/yapfaq.git] / yapfaq.pl
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
15 my $Version = "0.6";
16
17 my $NNTPServer = "localhost";
18 my $NNTPUser = "";
19 my $NNTPPass = "";
20 my $Sender = "";
21 my $ConfigFile = "yapfaq.cfg";
22 my $UsePGP = 0;
23
24 ################################## PGP-Config #################################
25
26 my $pgp           = '/usr/bin/pgp';            # path to pgp
27 my $PGPVersion    = '2';                       # Use 2 for 2.X, 5 for PGP > 2.X and GPG for GPG
28
29 my $PGPSigner     = '';                        # sign as who?
30 my $PGPPass       = '';                        # pgp2 only
31 my $PathtoPGPPass = '';                        # pgp2, pgp5 and gpg
32
33
34 my $pgpbegin  ='-----BEGIN PGP SIGNATURE-----';# Begin of PGP-Signature
35 my $pgpend    ='-----END PGP SIGNATURE-----';  # End of PGP-Signature
36 my $pgptmpf   ='pgptmp';                       # temporary file for PGP.
37 my $pgpheader ='X-PGP-Sig';
38
39 my @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
44 my @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
53 use strict;
54 use Net::NNTP;
55 use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
56 use Fcntl ':flock'; # import LOCK_* constants
57 use Getopt::Std;
58 my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
59
60 my %Options;
61 getopts('hvpdt:f:', \%Options);
62 if ($Options{'h'}) {
63   print "$0 v $Version\nUsage: $0 [-hvpd] [-t <newsgroups>] [-f <faq>]\n";
64   exit(0);
65 };
66 my ($Faq) = $Options{'f'} if ($Options{'f'});
67
68 my @Config;
69 readconfig (\$ConfigFile, \@Config, \$Faq);
70
71 foreach (@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
111 exit;
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
117 sub 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
167 sub 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
184 sub 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
287 sub 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
325 sub 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.
370 sub 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
489 yapfaq - Post Usenet FAQs I<(yet another postfaq)>
490
491 =head1 SYNOPSIS
492
493 B<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
501 Perl 5.8 or later
502
503 =item -
504
505 Net::NNTP
506
507 =item -
508
509 Date::Calc
510
511 =item -
512
513 Getopt::Std
514
515 =back
516
517 Furthermore you need access to a news server to actually post FAQs.
518
519 =head1 DESCRIPTION
520
521 B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
522 frequency (every n days, weeks, months or years), adding all necessary
523 headers as defined in its config file (by default F<yapfaq.cfg>).
524
525 =head2 Configuration
526
527 F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
528 a single line, each containing the configuration for one FAQ as a set
529 of definitions in the form of I<param = value>.
530
531 =over 4
532
533 =item B<Name> = I<project name>
534
535 A name referring to your FAQ, also used for generation of a Message-ID.
536
537 This value must be set.
538
539 =item B<File> = I<file name>
540
541 A file containing the message body of your FAQ and all pseudo headers
542 (subheaders in the news.answers style).
543
544 This value must be set.
545
546 =item B<Posting-frequency> = I<time period>
547
548 The posting frequency defines how often your FAQ will be posted.
549 B<yapfaq> will only post your FAQ if this period of time has passed
550 since the last posting.
551
552 You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
553 or I<B<m>onths> or I<B<y>ears>.
554
555 This value must be set.
556
557 =item B<Expires> = I<time period>
558
559 The period of time after which your message will expire. An Expires
560 header will be calculated adding this time period to today's date.
561
562 You can declare this  time period either in I<B<d>ays> or I<B<w>weeks>
563 or I<B<m>onths> or I<B<y>ears>.
564
565 This setting is optional; the default  is 3 months.
566
567 =item B<From> = I<author>
568
569 The author of your FAQ as it will appear in the From header of the
570 message.
571
572 This value must be set.
573
574 =item B<Subject> = I<subject>
575
576 The title of your FAQ as it will appear in the Subject header of the
577 message.
578
579 You may use the special string C<%LM> which will be replaced with
580 the contents of the Last-Modified subheader in your I<File>.
581
582 This value must be set.
583
584 =item B<NGs> = I<newsgroups>
585
586 A comma-separated list of newsgroup(s) to post your FAQ to as it will
587 appear in the Newsgroups header of the message.
588
589 This value must be set.
590
591 =item B<Fup2> = I<newsgroup | poster>
592
593 A comma-separated list of newsgroup(s) or the special string I<poster>
594 as it will appear in the Followup-To header of the message.
595
596 This setting is optional.
597
598 =item B<MID-Format> = I<pattern>
599
600 A pattern from which the message ID is generated as it will appear in
601 the Message-ID header of the message.
602
603 You may use the special strings C<%n> for the I<Name> of your project,
604 C<%d> for the date the message is posted, C<%m> for the month and
605 C<%y> for the year, respectively.
606
607 This value must be set.
608
609 =item B<Supersede> = I<yes>
610
611 Add Supersedes header to the message containing the Message-ID header
612 of the last posting.
613
614 This setting is optional; you should set it to yes or leave it out.
615
616 =item B<ExtraHeader> = I<additional headers>
617
618 The contents of I<ExtraHeader> is added verbatim to the headers of
619 your message so you can add custom headers like Approved.
620
621 This 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
681 Information about the last post and about how to form message IDs for
682 posts is stored in a file named F<I<project name>.cfg> which will be
683 generated if it does not exist. Each of those status files will
684 contain two lines, the first being the date of the last time the FAQ
685 was 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
693 Print out version and usage information on B<yapfaq> and exit.
694
695 =item B<-v> (verbose)
696
697 Print out status information while running to STDOUT.
698
699 =item B<-p> (post unconditionally)
700
701 Post (all) FAQs unconditionally ignoring the posting frequency setting.
702
703 You may want to use this with the B<-f> option (see below).
704
705 =item B<-d> (dry run)
706
707 Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
708 update any status information.
709
710 =item B<-t> I<newsgroup(s) | CONSOLE> (test)
711
712 Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
713 newsgroups given after B<-t> as a comma-separated list or print the
714 FAQs to STDOUT separated by lines of dashes if the special string
715 C<CONSOLE> is given.  This can be used to preview what B<yapfaq> would
716 do without embarassing yourself on Usenet.  The status files are not
717 updated when this option is given.
718
719 You may want to use this with the B<-f> option (see below).
720
721 =item B<-f> I<project name>
722
723 Just deal with one FAQ only.
724
725 By default B<yapfaq> will work on all FAQs that are defined in
726 F<yapfaq.cfg>, check whether they are due for posting and - if they
727 are - post them. Consequently when the B<-p> option is set all FAQs
728 will be posted unconditionally. That may not be what you want to
729 achieve, so you can limit the operation of B<yapfaq> to the named FAQ
730 only.
731
732 =back
733
734 =head1 EXAMPLES
735
736 Post all FAQs that are due for posting:
737
738     yapfaq
739
740 Do a dry run, showing which FAQs would be posted:
741
742     yapfaq -dv
743
744 Do a test run and print on STDOUT what the FAQ I<myfaq> would look
745 like when posted, regardless whether it is due for posting or not:
746
747     yapfaq -pt CONSOLE -f myfaq
748
749 Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
750 if it is due:
751
752     yapfaq -t de.test -f myfaq
753
754 =head1 ENVIRONMENT
755
756 There are no special environment variables used by B<yapfaq>.
757
758 =head1 FILES
759
760 =over 4
761
762 =item F<yapfaq.pl>
763
764 The script itself.
765
766 =item F<yapfaq.cfg>
767
768 Configuration file for B<yapfaq>.
769
770 =item F<*.cfg>
771
772 Status data on FAQs.
773
774 The status files will be created on successful posting if they don't
775 already exist. The first line of the file will be the date of the last
776 time the FAQ was posted and the second line will be the message ID of
777 the last post of that FAQ.
778
779 =back
780
781 =head1 BUGS
782
783 Many, I'm sure.
784
785 =head1 SEE ALSO
786
787 L<http://th-h.de/download/scripts.php> will have the current
788 version of this program.
789
790 =head1 AUTHOR
791
792 Thomas Hochstein <thh@inter.net>
793
794 Original author (until version 0.5b from 2003):
795 Marc Brockschmidt <marc@marcbrockschmidt.de>
796
797
798 =head1 COPYRIGHT AND LICENSE
799
800 Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
801
802 Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
803
804 This program is free software; you may redistribute it and/or modify it
805 under the same terms as Perl itself.
806
807 =cut
This page took 0.032456 seconds and 3 git commands to generate.