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