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