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