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