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