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