3 # yapfaq Version 0.7 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.
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:c: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 $RCFile = $Options{'c'} if ($Options{'c'});
83 readrc (\$RCFile,\%Config);
85 warn "$0: W: .rc file $RCFile does not exist!\n";
88 # read configuration (configured FAQs)
90 readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq);
93 # - parse configuration
95 # - if FAQ is due: call postfaq()
97 my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date
98 my ($NPY,$NPM,$NPD); #NP: Next posting-date
101 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
102 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
103 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
104 my ($Supersede) =($$_{'supersede'});
106 # -f: loop if not FAQ to post
107 next if (defined($Faq) && $ActName ne $Faq);
110 if (open (FH, "<$File.cfg")) {
112 if (/##;; Lastpost:\s*(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/){
113 ($LPD, $LPM, $LPY) = ($1, $2, $3);
114 } elsif (/^##;;\s*LastMID:\s*(<\S+@\S+>)\s*$/) {
120 warn "$0: W: Couldn't open $File.cfg: $!\n";
123 $SupersedeMID = "" unless $Supersede;
125 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
127 # if FAQ is due: get it out
128 if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) {
130 print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'};
132 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
134 } elsif($Options{'v'}) {
135 print "$ActName: Nothing to do.\n";
141 #################################### readrc ####################################
142 # Takes a filename and the reference to an array which contains the valid options
145 my ($File, $Config) = @_;
147 print "Reading $$File.\n" if($Options{'v'});
149 open FH, "<$$File" or die "$0: Can't open $$File: $!";
151 if (/^\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)/) {
152 if (grep(/$1/,@ValidConfVars)) {
153 $$Config{$1} = $2 if $2 ne '';
155 warn "$0: W: $1 is not a valid configuration variable (reading from $$File)\n";
161 ################################## readconfig ##################################
162 # Takes a filename, a reference to an array, which will hold hashes with
163 # the data from $File, and - optionally - the name of the (single) FAQ to post
166 my ($File, $Config, $Faq) = @_;
167 my ($LastEntry, $Error, $i) = ('','',0);
169 print "Reading configuration.\n" if($Options{'v'});
171 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
173 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
174 if (/^(\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)|^(.*?)'?\s*(#.*$|$))/ && not /^\s*$/) {
175 $LastEntry = lc($2) if $2;
176 $$Config[$i]{$LastEntry} .= $3 if $3;
177 $$Config[$i]{$LastEntry} .= "\n$5" if $5 && $5;
179 if (/^\s*=====\s*$/) {
187 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
188 unless(defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} =~ /^\S+$/) {
189 $Error .= "E: The name of your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
191 unless(defined($$Config[$i]{'file'}) && -f $$Config[$i]{'file'}) {
192 $Error .= "E: The file to post for your project \"$$Config[$i]{'name'}\" is not defined or does not exist.\n"
194 unless(defined($$Config[$i]{'from'}) && $$Config[$i]{'from'} =~ /\S+\@(\S+\.)?\S{2,}\.\S{2,}/) {
195 $Error .= "E: The From header for your project \"$$Config[$i]{'name'}\" seems to be incorrect.\n"
197 unless(defined($$Config[$i]{'ngs'}) && $$Config[$i]{'ngs'} =~ /^\S+$/) {
198 $Error .= "E: The Newsgroups header for your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
200 unless(defined($$Config[$i]{'subject'})) {
201 $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n"
203 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
204 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
206 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
207 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
209 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
210 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
211 $$Config[$i]{'expires'} = '3m'; # set default (3 month) if expires is unset or invalid
213 unless(!$$Config[$i]{'mid-format'} || $$Config[$i]{'mid-format'} =~ /^<\S+\@\S{2,}\.\S{2,}>$/) {
214 warn "$0: W: The Message-ID format for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n";
215 $$Config[$i]{'mid-format'} = '<%n-%d.%m.%y@'.hostfqdn.'>'; # set default if mid-format is invalid
218 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
219 die $Error if $Error;
222 ################################# calcdelta #################################
223 # Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...)
224 # and adds the latter to the former
227 my ($Year, $Month, $Day, $Period) = @_;
228 my ($NYear, $NMonth, $NDay);
230 if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days.
231 ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1));
232 } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM
233 ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0)));
235 return ($NYear, $NMonth, $NDay);
238 ################################ updatestatus ###############################
239 # Takes a MID and a status file name
240 # and writes status information to disk
243 my ($ActName, $File, $date, $MID) = @_;
245 print "$$ActName: Save status information.\n" if($Options{'v'});
247 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
248 print FH "##;; Lastpost: $date\n";
249 print FH "##;; LastMID: $MID\n";
253 ################################## postfaq ##################################
254 # Takes a filename and many other vars.
256 # It reads the data-file $File and then posts the article.
259 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
260 my (@Header,@Body,$MID,$InRealBody,$LastModified);
262 print "$$ActName: Preparing to post.\n" if($Options{'v'});
265 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
266 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
269 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID); # set to default if unset
270 $MID =~ s/\%n/$$ActName/g;
271 $MID =~ s/\%d/$$TDD/g;
272 $MID =~ s/\%m/$$TDM/g;
273 $MID =~ s/\%y/$$TDY/g;
276 open (FH, "<$$File");
279 push (@Body, $_), next if $InRealBody;
280 $InRealBody++ if /^$/;
281 $LastModified = $1 if /^Last-modified: (\S+)$/i;
285 push @Body, "\n" if ($Body[-1] ne "\n");
287 #Create Date- and Expires-Header:
288 my @time = localtime;
289 my $ss = ($time[0]<10) ? "0" . $time[0] : $time[0];
290 my $mm = ($time[1]<10) ? "0" . $time[1] : $time[1];
291 my $hh = ($time[2]<10) ? "0" . $time[2] : $time[2];
293 my $month = ($time[4]+1<10) ? "0" . ($time[4]+1) : $time[4]+1;
294 my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
295 my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
296 my $year = (1900 + $time[5]);
297 my $tz = $time[8] ? " +0200" : " +0100";
299 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
301 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
302 my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1];
304 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
305 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
307 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
309 $$Subject =~ s/\%LM/$LastModified/;
313 if($Options{'t'} and $Options{'t'} !~ /console/i) {
314 $$NG = $Options{'t'};
317 #Now create the complete Header:
318 push @Header, "From: $$From\n";
319 push @Header, "Newsgroups: $$NG\n";
320 push @Header, "Followup-To: $$Fup2\n" if $$Fup2;
321 push @Header, "Subject: $$Subject\n";
322 push @Header, "Message-ID: $MID\n";
323 push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes;
324 push @Header, "Date: $date\n";
325 push @Header, "Expires: $expdate\n";
326 push @Header, "Sender: $$Sender\n" if $$Sender;
327 push @Header, "Mime-Version: 1.0\n";
328 push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
329 push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
330 push @Header, "Content-Transfer-Encoding: 8bit\n";
331 push @Header, "User-Agent: yapfaq/$Version\n";
332 if ($$ExtraHeaders) {
333 push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
336 # sign article if $UsePGP is true
337 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
340 print "$$ActName: Posting article ...\n" if($Options{'v'});
341 my $failure = post(\@Article);
344 print "$$ActName: Posting failed, ERROR.dat may have more information.\n" if($Options{'v'} && (!defined($Options{'t'}) || $Options{'t'} !~ /console/i));
346 updatestatus($ActName, $File, "$day.$month.$year", $MID) if !defined($Options{'t'});
350 ################################## post ##################################
351 # Takes a complete article (Header and Body).
353 # It opens a connection to $NNTPServer and posts the message.
359 # test mode - print article to console
360 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
361 print "-----BEGIN--------------------------------------------------\n";
363 print "------END---------------------------------------------------\n";
364 # pipe article to script
365 } elsif(defined($Options{'s'})) {
366 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
367 print POST @$ArticleR;
372 warn "$0: W: $Options{'s'} exited with status ", ($? >> 8), "\n";
377 my $NewsConnection = Net::NNTP->new($Config{'NNTPServer'}, Reader => 1) or die "$0: E: Can't connect to news server '$Config{'NNTPServer'}'!\n";
378 $NewsConnection->authinfo ($Config{'NNTPUser'}, $Config{'NNTPPass'}) if (defined($Config{'NNTPUser'}));
379 $NewsConnection->post();
380 $NewsConnection->datasend (@$ArticleR);
381 $NewsConnection->dataend();
383 if ($NewsConnection->ok()) {
385 # Posting failed? Save to ERROR.dat
387 warn "$0: W: Posting failed!\n";
388 open FH, ">>ERROR.dat";
389 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
390 print FH $NewsConnection->code();
391 print FH $NewsConnection->message();
394 print FH "-" x 80, "\n";
397 $NewsConnection->quit();
402 #-------- sub getpgpcommand
403 # getpgpcommand generates the command to sign the message and returns it.
406 # - $PGPVersion: A scalar holding the PGPVersion
408 my ($PGPVersion) = @_;
411 if ($PGPVersion eq '2') {
412 if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
413 open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
414 Config{'$PGPPass'} = <PGPPW>;
418 if (Config{'$PGPPass'}) {
419 $PGPCommand = "PGPPASS=\"".$Config{'PGPPass'}."\" ".$Config{'pgp'}." -u \"".$Config{'PGPSigner'}."\" +verbose=0 language='en' -saft <".$Config{'pgptmpf'}.".txt >".$Config{'pgptmpf'}.".txt.asc";
421 die "$0: E: PGP-Passphrase is unknown!\n";
423 } elsif ($PGPVersion eq '5') {
424 if ($Config{'PathtoPGPPass'}) {
425 $PGPCommand = "PGPPASSFD=2 ".$Config{'pgp'}."s -u \"".$Config{'PGPSigner'}."\" -t --armor -o ".$Config{'pgptmpf'}.".txt.asc -z -f < ".$Config{'pgptmpf'}.".txt 2<".$Config{'PathtoPGPPass'};
427 die "$0: E: PGP-Passphrase is unknown!\n";
429 } elsif ($PGPVersion =~ m/GPG/io) {
430 if (Config{'$PathtoPGPPass'}) {
431 $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";
433 die "$0: E: Passphrase is unknown!\n";
436 die "$0: E: Unknown PGP-Version $PGPVersion!";
442 #-------- sub signarticle
443 # signarticle signs an articel and returns a reference to an array
444 # containing the whole signed Message.
447 # - $HeaderAR: A reference to a array containing the articles headers.
448 # - $BodyR: A reference to an array containing the body.
451 # - $MessageRef: A reference to an array containing the whole message.
453 my ($HeaderAR, $BodyR) = @_;
454 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
456 foreach my $line (@$HeaderAR) {
457 if ($line =~ /^(\S+):\s+(.*)$/s) {
459 $$HeaderR{lc($currentheader)} = "$1: $2";
461 $$HeaderR{lc($currentheader)} .= $line;
465 foreach (@PGPSignHeaders) {
466 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
467 push @signheaders, $_;
471 $pgpbody = join ("", @$BodyR);
473 # Delete and create the temporary pgp-Files
474 unlink "$Config{'pgptmpf'}.txt";
475 unlink "$Config{'pgptmpf'}.txt.asc";
476 $signheaders = join(",", @signheaders);
478 $pgphead = "X-Signed-Headers: $signheaders\n";
479 foreach $header (@signheaders) {
480 if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
481 $pgphead .= $header.": ".$1."\n";
485 open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
486 print FH $pgphead, "\n", $pgpbody;
487 print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
488 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
490 # Start PGP, then read the signature;
491 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
494 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
495 $/ = "$Config{'pgpbegin'}\n";
497 unless (m/\Q$Config{'pgpbegin'}\E$/o) {
498 # unlink $Config{'pgptmpf'} . ".txt";
499 # unlink $Config{'pgptmpf'} . ".txt.asc";
500 die "$0: E: $Config{'pgpbegin'} not found in ".$Config{'pgptmpf'}.".txt.asc\n"
502 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
506 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
507 unlink $Config{'pgptmpf'} . ".txt";
508 unlink $Config{'pgptmpf'} . ".txt.asc";
509 die "$0: E: didn't find PGP Version line where expected.\n";
513 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
515 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
518 do { # skip other pgp headers like
519 $_ = <FH>; # "charset:"||"comment:" until empty line
524 last if /^\Q$Config{'pgpend'}\E$/;
525 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
528 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
532 unlink $Config{'pgptmpf'} . ".txt";
533 unlink $Config{'pgptmpf'} . ".txt.asc";
534 die "$0: E: unexpected data following $Config{'pgpend'}\n";
537 unlink "$Config{'pgptmpf'}.txt.asc";
539 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
540 delete $$HeaderR{$Config{'pgpheader'}};
543 foreach $header (@PGPorderheaders) {
544 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
545 push(@pgphead, "$$HeaderR{$header}");
546 delete $$HeaderR{$header};
550 foreach $header (keys %$HeaderR) {
551 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
552 push(@pgphead, "$$HeaderR{$header}");
553 delete $$HeaderR{$header};
557 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
560 @pgpbody = split /$/m, $pgpbody;
561 my @pgpmessage = (@pgphead, "\n", @pgpbody);
567 ################################ Documentation #################################
571 yapfaq - Post Usenet FAQs I<(yet another postfaq)>
575 B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>] [B<-c> I<.rc file>]
599 Furthermore you need access to a news server to actually post FAQs.
603 B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
604 frequency (every n days, weeks, months or years), adding all necessary
605 headers as defined in its config file (by default F<yapfaq.cfg>).
609 F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
610 a single line, each containing the configuration for one FAQ as a set
611 of definitions in the form of I<param = value>. Everything after a "#"
612 sign is ignored so you may comment your configuration file.
616 =item B<Name> = I<project name>
618 A name referring to your FAQ, also used for generation of a Message-ID.
620 This value must be set.
622 =item B<File> = I<file name>
624 A file containing the message body of your FAQ and all pseudo headers
625 (subheaders in the news.answers style).
627 This value must be set.
629 =item B<Posting-frequency> = I<time period>
631 The posting frequency defines how often your FAQ will be posted.
632 B<yapfaq> will only post your FAQ if this period of time has passed
633 since the last posting.
635 You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
636 or I<B<m>onths> or I<B<y>ears>.
638 This value must be set.
640 =item B<Expires> = I<time period> (optional)
642 The period of time after which your message will expire. An Expires
643 header will be calculated adding this time period to today's date.
645 You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
646 or I<B<m>onths> or I<B<y>ears>.
648 This setting is optional; the default is 3 months.
650 =item B<From> = I<author>
652 The author of your FAQ as it will appear in the From header of the
655 This value must be set.
657 =item B<Subject> = I<subject>
659 The title of your FAQ as it will appear in the Subject header of the
662 You may use the special string C<%LM> which will be replaced with
663 the contents of the Last-Modified subheader in your I<File>.
665 This value must be set.
667 =item B<NGs> = I<newsgroups>
669 A comma-separated list of newsgroup(s) to post your FAQ to as it will
670 appear in the Newsgroups header of the message.
672 This value must be set.
674 =item B<Fup2> = I<newsgroup | poster> (optional)
676 A comma-separated list of newsgroup(s) or the special string I<poster>
677 as it will appear in the Followup-To header of the message.
679 This setting is optional.
681 =item B<MID-Format> = I<pattern> (optional)
683 A pattern from which the message ID is generated as it will appear in
684 the Message-ID header of the message.
686 You may use the special strings C<%n> for the I<Name> of your project,
687 C<%d> for the date the message is posted, C<%m> for the month and
688 C<%y> for the year, respectively.
690 This setting is optional; the default is '<%n-%d.%m.%y@I<YOURHOST>>'
691 where I<YOURHOST> is the fully qualified domain name (FQDN) of the
692 host B<yapfaq> is running on. Obviously that will only work if you
693 have defined a reasonable hostname that the hostfqdn() function of
694 Net::Domain can return.
696 =item B<Supersede> = I<yes> (optional)
698 Add Supersedes header to the message containing the Message-ID header
701 This setting is optional; you should set it to yes or leave it out.
703 =item B<ExtraHeader> = I<additional headers> (optional)
705 The contents of I<ExtraHeader> is added verbatim to the headers of
706 your message so you can add custom headers like Approved.
708 This setting is optional.
712 =head3 Example configuration file
714 # name of your project
717 # file to post (complete body and pseudo-headers)
718 # ($File.cfg contains data on last posting and last MID)
721 # how often your project should be posted
722 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
723 Posting-frequency = '1d'
725 # time period after which the posting should expire
726 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
730 From = 'test@domain.invalid'
733 # (may contain "%LM" which will be replaced by the contents of the
734 # Last-Modified pseudo header).
735 Subject = 'test noreply ignore'
737 # comma-separated list of newsgroup(s) to post to
738 # (header "Newsgroups:")
741 # header "Followup-To:"
744 # Message-ID ("%n" is $Name)
745 # MID-Format = '<%n-%d.%m.%y@domain.invalid>'
747 # Supersede last posting?
750 # extra headers (appended verbatim)
751 # use this for custom headers like "Approved:"
752 ExtraHeader = 'Approved: moderator@domain.invalid
755 # other projects may follow separated with "====="
760 Posting-frequency = '2m'
761 From = 'My Name <my.name@domain.invalid>'
762 Subject = 'Test of yapfag <%LM>'
763 NGs = 'de.test,de.alt.test'
765 MID-Format = '<%n-%m.%y@domain.invalid>'
768 =head3 Status Information
770 Information about the last post and about how to form message IDs for
771 posts is stored in a file named F<I<project name>.cfg> which will be
772 generated if it does not exist. Each of those status files will
773 contain two lines, the first being the date of the last time the FAQ
774 was posted and the second being the message ID of that incarnation.
776 =head2 Runtime Configuration
778 Apart from configuring which FAQ(s) to post you may (re)set some
779 runtime configuration variables via the .rcfile (by default
780 F<.yapfaqrc>). F<.yapfaqrc> must contain one definition in the form of
781 I<param = value> on each line; everything after a "#" sign is ignored.
783 If you omit some settings they will be set to default values hardcoded
786 B<Please note that all parameter names are case-sensitive!>
790 =item B<NNTPServer> = I<NNTP server> (mandatory)
792 Host name of the NNTP server to post to. Must be set (or omitted; the
793 default is "localhost"); if set to en empty string, B<yapfaq> falls
794 back to Perl's build-in defaults (contents of environment variables
795 NNTPSERVER and NEWSHOST; if not set, default from Net::Config; if not
796 set, "news" is used).
798 =item B<NNTPUser> = I<user name> (optional)
800 User name used for authentication with the NNTP server (I<AUTHINFO
803 This setting is optional; if it is not set, I<NNTPPass> is ignored and
804 no authentication is tried.
806 =item B<NNTPPass> = I<password> (optional)
808 Password used for authentication with the NNTP server (I<AUTHINFO
811 This setting is optional; it must be set if I<NNTPUser> is present.
813 =item B<Sender> = I<Sender header> (optional)
815 The Sender header that will be added to every posted message.
817 This setting is optional.
819 =item B<ConfigFile> = I<configuration file> (mandatory)
821 The configuration file defining the FAQ(s) to post. Must be set (or
822 omitted; the default is "yapfaq.cfg").
824 =item B<UsePGP> = I<whether to add a digital signature> (optional)
826 Boolean value (0 or 1) controlling whether the FAQs will get digitally
827 signed via an X-PGP-Sig header.
829 This setting is optional; the default is 0.
831 If you have set I<UsePGP> to 1, you must also supply the necessary
832 information on your PGP oder GPG installation; please refer to the
833 sample F<.yapfaqrc> file (see below) for more information on this
838 =head3 Example runtime configuration file
840 NNTPServer = 'localhost'
844 ConfigFile = 'yapfaq.cfg'
847 ################################## PGP-Config #################################
848 pgp = '/usr/bin/pgp' # path to pgp
849 PGPVersion = '2' # Use 2 for 2.X 5 for PGP > 2.X and GPG for GPG
850 PGPSigner = '' # sign as who?
851 PGPPass = '' # pgp2 only
852 PathtoPGPPass = '' # pgp2 pgp5 and gpg
853 pgpbegin = '-----BEGIN PGP SIGNATURE-----' # Begin of PGP-Signature
854 pgpend = '-----END PGP SIGNATURE-----' # End of PGP-Signature
855 pgptmpf = 'pgptmp' # temporary file for PGP.
856 pgpheader = 'X-PGP-Sig'
858 =head3 Using more than one runtime configuration
860 You may use more than one runtime configuration file with the B<-c>
867 =item B<-V> (version)
869 Print out version and copyright information on B<yapfaq> and exit.
873 Print this man page and exit.
875 =item B<-v> (verbose)
877 Print out status information while running to STDOUT.
879 =item B<-p> (post unconditionally)
881 Post (all) FAQs unconditionally ignoring the posting frequency setting.
883 You may want to use this with the B<-f> option (see below).
885 =item B<-d> (dry run)
887 Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
888 update any status information.
890 =item B<-t> I<newsgroup(s) | CONSOLE> (test)
892 Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
893 newsgroups given after B<-t> as a comma-separated list or print the
894 FAQs to STDOUT separated by lines of dashes if the special string
895 C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
896 do without embarassing yourself on Usenet. The status files are not
897 updated when this option is given.
899 You may want to use this with the B<-f> option (see below).
901 =item B<-f> I<project name>
903 Just deal with one FAQ only.
905 By default B<yapfaq> will work on all FAQs that are defined in
906 F<yapfaq.cfg>, check whether they are due for posting and - if they
907 are - post them. Consequently when the B<-p> option is set all FAQs
908 will be posted unconditionally. That may not be what you want to
909 achieve, so you can limit the operation of B<yapfaq> to the named FAQ
912 =item B<-s> I<program> (pipe to script)
914 Instead of posting the article(s) to Usenet pipe them to the external
915 I<program> on STDIN (which may post the article(s) then). A return
916 value of 0 will be considered success.
918 =item B<-c> I<.rc file>
920 Load another runtime configuration file (.rc file) than F<.yaofaq.rc>.
922 You may for example define another usenet server to post your FAQ(s)
923 to or load another configuration file defining (an)other FAQ(s).
929 Post all FAQs that are due for posting:
933 Do a dry run, showing which FAQs would be posted:
937 Do a test run and print on STDOUT what the FAQ I<myfaq> would look
938 like when posted, regardless whether it is due for posting or not:
940 yapfaq -pt CONSOLE -f myfaq
942 Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
945 yapfaq -t de.test -f myfaq
949 There are no special environment variables used by B<yapfaq>.
961 Runtime configuration file for B<yapfaq>.
965 Configuration file for B<yapfaq>.
971 The status files will be created on successful posting if they don't
972 already exist. The first line of the file will be the date of the last
973 time the FAQ was posted and the second line will be the message ID of
974 the last post of that FAQ.
984 L<http://th-h.de/download/scripts.php> will have the current
985 version of this program.
989 Thomas Hochstein <thh@inter.net>
991 Original author (up to version 0.5b, dating from 2003):
992 Marc Brockschmidt <marc@marcbrockschmidt.de>
994 =head1 COPYRIGHT AND LICENSE
996 Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
998 Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
1000 This program is free software; you may redistribute it and/or modify it
1001 under the same terms as Perl itself.