3 # yapfaq Version 0.6 by Thomas Hochstein
4 # (Original author: Marc Brockschmidt)
6 # This script posts any project described in its config-file. Most people
7 # will use it in combination with cron(8).
9 # Copyright (C) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
10 # Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
12 # It can be redistributed and/or modified under the same terms under
13 # which Perl itself is published.
15 my $Version = "0.6.2";
17 my $RCFile = '.yapfaqrc';
18 my @ValidConfVars = ('NNTPServer','NNTPUser','NNTPPass','Sender','ConfigFile',
19 'UsePGP','pgp','PGPVersion','PGPSigner','PGPPass',
20 'PathtoPGPPass','pgpbegin','pgpend','pgptmpf','pgpheader');
22 ################################### Defaults ##################################
23 my %Config = (NNTPServer => "localhost",
27 ConfigFile => "yapfaq.cfg",
30 ################################## PGP-Config #################################
31 pgp => '/usr/bin/pgp', # path to pgp
32 PGPVersion => '2', # Use 2 for 2.X, 5 for PGP > 2.X and GPG for GPG
33 PGPSigner => '', # sign as who?
34 PGPPass => '', # pgp2 only
35 PathtoPGPPass => '', # pgp2, pgp5 and gpg
36 pgpbegin => '-----BEGIN PGP SIGNATURE-----', # Begin of PGP-Signature
37 pgpend => '-----END PGP SIGNATURE-----', # End of PGP-Signature
38 pgptmpf => 'pgptmp', # temporary file for PGP.
39 pgpheader => 'X-PGP-Sig');
41 my @PGPSignHeaders = ('From', 'Newsgroups', 'Subject', 'Control',
42 'Supersedes', 'Followup-To', 'Date', 'Sender', 'Approved',
43 'Message-ID', 'Reply-To', 'Cancel-Lock', 'Cancel-Key',
44 'Also-Control', 'Distribution');
46 my @PGPorderheaders = ('from', 'newsgroups', 'subject', 'control',
47 'supersedes', 'followup-To', 'date', 'organization', 'lines',
48 'sender', 'approved', 'distribution', 'message-id',
49 'references', 'reply-to', 'mime-version', 'content-type',
50 'content-transfer-encoding', 'summary', 'keywords', 'cancel-lock',
51 'cancel-key', 'also-control', 'x-pgp', 'user-agent');
53 ############################# End of Configuration #############################
57 use Net::Domain qw(hostfqdn);
58 use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
59 use Fcntl ':flock'; # import LOCK_* constants
61 my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
63 # read commandline options
65 getopts('Vhvpdt:f:s:', \%Options);
66 # -V: print version / copyright information
68 print "$0 v $Version\nCopyright (c) 2003 Marc Brockschmidt <marc\@marcbrockschmidt.de>\nCopyright (c) 2010 Thomas Hochstein <thh\@inter.net>\n";
69 print "This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.\n";
72 # -h: feed myself to perldoc
78 my ($Faq) = $Options{'f'} if ($Options{'f'});
80 # read runtime configuration (configuration variables)
81 readrc (\$RCFile,\%Config) if -f $RCFile;
83 # read configuration (configured FAQs)
85 readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq);
88 # - parse configuration
90 # - if FAQ is due: call postfaq()
92 my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date
93 my ($NPY,$NPM,$NPD); #NP: Next posting-date
96 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
97 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
98 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
99 my ($Supersede) =($$_{'supersede'});
101 # -f: loop if not FAQ to post
102 next if (defined($Faq) && $ActName ne $Faq);
105 if (open (FH, "<$File.cfg")) {
107 if (/##;; Lastpost:\s*(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/){
108 ($LPD, $LPM, $LPY) = ($1, $2, $3);
109 } elsif (/^##;;\s*LastMID:\s*(<\S+@\S+>)\s*$/) {
115 warn "$0: W: Couldn't open $File.cfg: $!\n";
118 $SupersedeMID = "" unless $Supersede;
120 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
122 # if FAQ is due: get it out
123 if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) {
125 print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'};
127 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
129 } elsif($Options{'v'}) {
130 print "$ActName: Nothing to do.\n";
136 #################################### readrc ####################################
137 # Takes a filename and the reference to an array which contains the valid options
140 my ($File, $Config) = @_;
142 print "Reading $$File.\n" if($Options{'v'});
144 open FH, "<$$File" or die "$0: Can't open $$File: $!";
146 if (/^\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)/) {
147 if (grep(/$1/,@ValidConfVars)) {
148 $$Config{$1} = $2 if $2 ne '';
150 warn "$0: W: $1 is not a valid configuration variable (reading from $$File)\n";
156 ################################## readconfig ##################################
157 # Takes a filename, a reference to an array, which will hold hashes with
158 # the data from $File, and - optionally - the name of the (single) FAQ to post
161 my ($File, $Config, $Faq) = @_;
162 my ($LastEntry, $Error, $i) = ('','',0);
164 print "Reading configuration.\n" if($Options{'v'});
166 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
168 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
169 if (/^(\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)|^(.*?)'?\s*(#.*$|$))/ && not /^\s*$/) {
170 $LastEntry = lc($2) if $2;
171 $$Config[$i]{$LastEntry} .= $3 if $3;
172 $$Config[$i]{$LastEntry} .= "\n$5" if $5 && $5;
174 if (/^\s*=====\s*$/) {
182 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
183 unless(defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} =~ /^\S+$/) {
184 $Error .= "E: The name of your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
186 unless(defined($$Config[$i]{'file'}) && -f $$Config[$i]{'file'}) {
187 $Error .= "E: The file to post for your project \"$$Config[$i]{'name'}\" is not defined or does not exist.\n"
189 unless(defined($$Config[$i]{'from'}) && $$Config[$i]{'from'} =~ /\S+\@(\S+\.)?\S{2,}\.\S{2,}/) {
190 $Error .= "E: The From header for your project \"$$Config[$i]{'name'}\" seems to be incorrect.\n"
192 unless(defined($$Config[$i]{'ngs'}) && $$Config[$i]{'ngs'} =~ /^\S+$/) {
193 $Error .= "E: The Newsgroups header for your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
195 unless(defined($$Config[$i]{'subject'})) {
196 $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n"
198 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
199 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
201 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
202 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
204 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
205 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
207 unless(defined($$Config[$i]{'mid-format'}) && $$Config[$i]{'mid-format'} =~ /^<\S+\@\S{2,}\.\S{2,}>$/) {
208 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n";
211 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
212 die $Error if $Error;
215 ################################# calcdelta #################################
216 # Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...)
217 # and adds the latter to the former
220 my ($Year, $Month, $Day, $Period) = @_;
221 my ($NYear, $NMonth, $NDay);
223 if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days.
224 ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1));
225 } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM
226 ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0)));
228 return ($NYear, $NMonth, $NDay);
231 ################################## postfaq ##################################
232 # Takes a filename and many other vars.
234 # It reads the data-file $File and then posts the article.
237 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
238 my (@Header,@Body,$MID,$InRealBody,$LastModified);
240 print "$$ActName: Preparing to post.\n" if($Options{'v'});
243 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
244 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
247 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID);
248 $MID =~ s/\%n/$$ActName/g;
249 $MID =~ s/\%d/$$TDD/g;
250 $MID =~ s/\%m/$$TDM/g;
251 $MID =~ s/\%y/$$TDY/g;
254 open (FH, "<$$File");
257 push (@Body, $_), next if $InRealBody;
258 $InRealBody++ if /^$/;
259 $LastModified = $1 if /^Last-modified: (\S+)$/i;
263 push @Body, "\n" if ($Body[-1] ne "\n");
265 #Create Date- and Expires-Header:
266 my @time = localtime;
267 my $ss = ($time[0]<10) ? "0" . $time[0] : $time[0];
268 my $mm = ($time[1]<10) ? "0" . $time[1] : $time[1];
269 my $hh = ($time[2]<10) ? "0" . $time[2] : $time[2];
271 my $month = ($time[4]+1<10) ? "0" . ($time[4]+1) : $time[4]+1;
272 my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
273 my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
274 my $year = (1900 + $time[5]);
275 my $tz = $time[8] ? " +0200" : " +0100";
277 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
279 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
280 my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1];
282 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
283 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
285 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
287 $$Subject =~ s/\%LM/$LastModified/;
291 if($Options{'t'} and $Options{'t'} !~ /console/i) {
292 $$NG = $Options{'t'};
295 #Now create the complete Header:
296 push @Header, "From: $$From\n";
297 push @Header, "Newsgroups: $$NG\n";
298 push @Header, "Followup-To: $$Fup2\n" if $$Fup2;
299 push @Header, "Subject: $$Subject\n";
300 push @Header, "Message-ID: $MID\n";
301 push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes;
302 push @Header, "Date: $date\n";
303 push @Header, "Expires: $expdate\n";
304 push @Header, "Sender: $$Sender\n" if $$Sender;
305 push @Header, "Mime-Version: 1.0\n";
306 push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
307 push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
308 push @Header, "Content-Transfer-Encoding: 8bit\n";
309 push @Header, "User-Agent: yapfaq/$Version\n";
310 if ($$ExtraHeaders) {
311 push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
314 # sign article if $UsePGP is true
315 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
318 print "$$ActName: Posting article ...\n" if($Options{'v'});
322 return if($Options{'t'});
324 # otherwise: update status data
325 print "$$ActName: Save status information.\n" if($Options{'v'});
327 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
328 print FH "##;; Lastpost: $day.$month.$year\n";
329 print FH "##;; LastMID: $MID\n";
333 ################################## post ##################################
334 # Takes a complete article (Header and Body).
336 # It opens a connection to $NNTPServer and posts the message.
342 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
343 print "-----BEGIN--------------------------------------------------\n";
345 print "------END---------------------------------------------------\n";
350 if(defined($Options{'s'})) {
351 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
352 print POST @$ArticleR;
357 my $NewsConnection = Net::NNTP->new($Config{'NNTPServer'}, Reader => 1) or die "$0: E: Can't connect to news server '$Config{'NNTPServer'}'!\n";
358 $NewsConnection->authinfo ($Config{'NNTPUser'}, $Config{'NNTPPass'}) if (defined($Config{'NNTPUser'}));
359 $NewsConnection->post();
360 $NewsConnection->datasend (@$ArticleR);
361 $NewsConnection->dataend();
363 # Posting failed? Save to ERROR.dat
364 if (!$NewsConnection->ok()) {
365 open FH, ">>ERROR.dat";
366 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
367 print FH $NewsConnection->code();
368 print FH $NewsConnection->message();
371 print FH "-" x 80, "\n";
375 $NewsConnection->quit();
378 #-------- sub getpgpcommand
379 # getpgpcommand generates the command to sign the message and returns it.
382 # - $PGPVersion: A scalar holding the PGPVersion
384 my ($PGPVersion) = @_;
387 if ($PGPVersion eq '2') {
388 if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
389 open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
390 Config{'$PGPPass'} = <PGPPW>;
394 if (Config{'$PGPPass'}) {
395 $PGPCommand = "PGPPASS=\"".$Config{'PGPPass'}."\" ".$Config{'pgp'}." -u \"".$Config{'PGPSigner'}."\" +verbose=0 language='en' -saft <".$Config{'pgptmpf'}.".txt >".$Config{'pgptmpf'}.".txt.asc";
397 die "$0: E: PGP-Passphrase is unknown!\n";
399 } elsif ($PGPVersion eq '5') {
400 if ($Config{'PathtoPGPPass'}) {
401 $PGPCommand = "PGPPASSFD=2 ".$Config{'pgp'}."s -u \"".$Config{'PGPSigner'}."\" -t --armor -o ".$Config{'pgptmpf'}.".txt.asc -z -f < ".$Config{'pgptmpf'}.".txt 2<".$Config{'PathtoPGPPass'};
403 die "$0: E: PGP-Passphrase is unknown!\n";
405 } elsif ($PGPVersion =~ m/GPG/io) {
406 if (Config{'$PathtoPGPPass'}) {
407 $PGPCommand = $Config{'pgp'}." --digest-algo MD5 -a -u \"".$Config{'PGPSigner'}."\" -o ".$Config{'pgptmpf'}.".txt.asc --no-tty --batch --passphrase-fd 2 2<".$Config{'PathtoPGPPass'}." --clearsign ".$Config{'pgptmpf'}.".txt";
409 die "$0: E: Passphrase is unknown!\n";
412 die "$0: E: Unknown PGP-Version $PGPVersion!";
418 #-------- sub signarticle
419 # signarticle signs an articel and returns a reference to an array
420 # containing the whole signed Message.
423 # - $HeaderAR: A reference to a array containing the articles headers.
424 # - $BodyR: A reference to an array containing the body.
427 # - $MessageRef: A reference to an array containing the whole message.
429 my ($HeaderAR, $BodyR) = @_;
430 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
432 foreach my $line (@$HeaderAR) {
433 if ($line =~ /^(\S+):\s+(.*)$/s) {
435 $$HeaderR{lc($currentheader)} = "$1: $2";
437 $$HeaderR{lc($currentheader)} .= $line;
441 foreach (@PGPSignHeaders) {
442 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
443 push @signheaders, $_;
447 $pgpbody = join ("", @$BodyR);
449 # Delete and create the temporary pgp-Files
450 unlink "$Config{'pgptmpf'}.txt";
451 unlink "$Config{'pgptmpf'}.txt.asc";
452 $signheaders = join(",", @signheaders);
454 $pgphead = "X-Signed-Headers: $signheaders\n";
455 foreach $header (@signheaders) {
456 if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
457 $pgphead .= $header.": ".$1."\n";
461 open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
462 print FH $pgphead, "\n", $pgpbody;
463 print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
464 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
466 # Start PGP, then read the signature;
467 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
470 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
471 $/ = "$Config{'pgpbegin'}\n";
473 unless (m/\Q$Config{'pgpbegin'}\E$/o) {
474 # unlink $Config{'pgptmpf'} . ".txt";
475 # unlink $Config{'pgptmpf'} . ".txt.asc";
476 die "$0: E: $Config{'pgpbegin'} not found in ".$Config{'pgptmpf'}.".txt.asc\n"
478 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
482 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
483 unlink $Config{'pgptmpf'} . ".txt";
484 unlink $Config{'pgptmpf'} . ".txt.asc";
485 die "$0: E: didn't find PGP Version line where expected.\n";
489 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
491 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
494 do { # skip other pgp headers like
495 $_ = <FH>; # "charset:"||"comment:" until empty line
500 last if /^\Q$Config{'pgpend'}\E$/;
501 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
504 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
508 unlink $Config{'pgptmpf'} . ".txt";
509 unlink $Config{'pgptmpf'} . ".txt.asc";
510 die "$0: E: unexpected data following $Config{'pgpend'}\n";
513 unlink "$Config{'pgptmpf'}.txt.asc";
515 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
516 delete $$HeaderR{$Config{'pgpheader'}};
519 foreach $header (@PGPorderheaders) {
520 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
521 push(@pgphead, "$$HeaderR{$header}");
522 delete $$HeaderR{$header};
526 foreach $header (keys %$HeaderR) {
527 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
528 push(@pgphead, "$$HeaderR{$header}");
529 delete $$HeaderR{$header};
533 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
536 @pgpbody = split /$/m, $pgpbody;
537 my @pgpmessage = (@pgphead, "\n", @pgpbody);
543 ################################ Documentation #################################
547 yapfaq - Post Usenet FAQs I<(yet another postfaq)>
551 B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>]
575 Furthermore you need access to a news server to actually post FAQs.
579 B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
580 frequency (every n days, weeks, months or years), adding all necessary
581 headers as defined in its config file (by default F<yapfaq.cfg>).
585 F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
586 a single line, each containing the configuration for one FAQ as a set
587 of definitions in the form of I<param = value>.
591 =item B<Name> = I<project name>
593 A name referring to your FAQ, also used for generation of a Message-ID.
595 This value must be set.
597 =item B<File> = I<file name>
599 A file containing the message body of your FAQ and all pseudo headers
600 (subheaders in the news.answers style).
602 This value must be set.
604 =item B<Posting-frequency> = I<time period>
606 The posting frequency defines how often your FAQ will be posted.
607 B<yapfaq> will only post your FAQ if this period of time has passed
608 since the last posting.
610 You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
611 or I<B<m>onths> or I<B<y>ears>.
613 This value must be set.
615 =item B<Expires> = I<time period>
617 The period of time after which your message will expire. An Expires
618 header will be calculated adding this time period to today's date.
620 You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
621 or I<B<m>onths> or I<B<y>ears>.
623 This setting is optional; the default is 3 months.
625 =item B<From> = I<author>
627 The author of your FAQ as it will appear in the From header of the
630 This value must be set.
632 =item B<Subject> = I<subject>
634 The title of your FAQ as it will appear in the Subject header of the
637 You may use the special string C<%LM> which will be replaced with
638 the contents of the Last-Modified subheader in your I<File>.
640 This value must be set.
642 =item B<NGs> = I<newsgroups>
644 A comma-separated list of newsgroup(s) to post your FAQ to as it will
645 appear in the Newsgroups header of the message.
647 This value must be set.
649 =item B<Fup2> = I<newsgroup | poster>
651 A comma-separated list of newsgroup(s) or the special string I<poster>
652 as it will appear in the Followup-To header of the message.
654 This setting is optional.
656 =item B<MID-Format> = I<pattern>
658 A pattern from which the message ID is generated as it will appear in
659 the Message-ID header of the message.
661 You may use the special strings C<%n> for the I<Name> of your project,
662 C<%d> for the date the message is posted, C<%m> for the month and
663 C<%y> for the year, respectively.
665 This value must be set.
667 =item B<Supersede> = I<yes>
669 Add Supersedes header to the message containing the Message-ID header
672 This setting is optional; you should set it to yes or leave it out.
674 =item B<ExtraHeader> = I<additional headers>
676 The contents of I<ExtraHeader> is added verbatim to the headers of
677 your message so you can add custom headers like Approved.
679 This setting is optional.
683 =head2 Example configuration file
685 # name of your project
688 # file to post (complete body and pseudo-headers)
689 # ($File.cfg contains data on last posting and last MID)
692 # how often your project should be posted
693 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
694 Posting-frequency = '1d'
696 # time period after which the posting should expire
697 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
701 From = 'test@domain.invalid'
704 # (may contain "%LM" which will be replaced by the contents of the
705 # Last-Modified pseudo header).
706 Subject = 'test noreply ignore'
708 # comma-separated list of newsgroup(s) to post to
709 # (header "Newsgroups:")
712 # header "Followup-To:"
715 # Message-ID ("%n" is $Name)
716 MID-Format = '<%n-%d.%m.%y@domain.invalid>'
718 # Supersede last posting?
721 # extra headers (appended verbatim)
722 # use this for custom headers like "Approved:"
723 ExtraHeader = 'Approved: moderator@domain.invalid
726 # other projects may follow separated with "====="
731 Posting-frequency = '2m'
732 From = 'My Name <my.name@domain.invalid>'
733 Subject = 'Test of yapfag <%LM>'
734 NGs = 'de.test,de.alt.test'
736 MID-Format = '<%n-%m.%y@domain.invalid>'
739 Information about the last post and about how to form message IDs for
740 posts is stored in a file named F<I<project name>.cfg> which will be
741 generated if it does not exist. Each of those status files will
742 contain two lines, the first being the date of the last time the FAQ
743 was posted and the second being the message ID of that incarnation.
749 =item B<-V> (version)
751 Print out version and copyright information on B<yapfaq> and exit.
755 Print this man page and exit.
757 =item B<-v> (verbose)
759 Print out status information while running to STDOUT.
761 =item B<-p> (post unconditionally)
763 Post (all) FAQs unconditionally ignoring the posting frequency setting.
765 You may want to use this with the B<-f> option (see below).
767 =item B<-d> (dry run)
769 Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
770 update any status information.
772 =item B<-t> I<newsgroup(s) | CONSOLE> (test)
774 Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
775 newsgroups given after B<-t> as a comma-separated list or print the
776 FAQs to STDOUT separated by lines of dashes if the special string
777 C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
778 do without embarassing yourself on Usenet. The status files are not
779 updated when this option is given.
781 You may want to use this with the B<-f> option (see below).
783 =item B<-f> I<project name>
785 Just deal with one FAQ only.
787 By default B<yapfaq> will work on all FAQs that are defined in
788 F<yapfaq.cfg>, check whether they are due for posting and - if they
789 are - post them. Consequently when the B<-p> option is set all FAQs
790 will be posted unconditionally. That may not be what you want to
791 achieve, so you can limit the operation of B<yapfaq> to the named FAQ
794 =item B<-s> I<program> (pipe to script)
796 Instead of posting the article(s) to Usenet pipe them to the external
797 I<program> on STDIN (which may post the article(s) then). A return
798 value of 0 will be considered success.
804 Post all FAQs that are due for posting:
808 Do a dry run, showing which FAQs would be posted:
812 Do a test run and print on STDOUT what the FAQ I<myfaq> would look
813 like when posted, regardless whether it is due for posting or not:
815 yapfaq -pt CONSOLE -f myfaq
817 Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
820 yapfaq -t de.test -f myfaq
824 There are no special environment variables used by B<yapfaq>.
836 Configuration file for B<yapfaq>.
842 The status files will be created on successful posting if they don't
843 already exist. The first line of the file will be the date of the last
844 time the FAQ was posted and the second line will be the message ID of
845 the last post of that FAQ.
855 L<http://th-h.de/download/scripts.php> will have the current
856 version of this program.
860 Thomas Hochstein <thh@inter.net>
862 Original author (until version 0.5b from 2003):
863 Marc Brockschmidt <marc@marcbrockschmidt.de>
866 =head1 COPYRIGHT AND LICENSE
868 Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
870 Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
872 This program is free software; you may redistribute it and/or modify it
873 under the same terms as Perl itself.