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