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: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";
212 unless(defined($$Config[$i]{'mid-format'}) && $$Config[$i]{'mid-format'} =~ /^<\S+\@\S{2,}\.\S{2,}>$/) {
213 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n";
216 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
217 die $Error if $Error;
220 ################################# calcdelta #################################
221 # Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...)
222 # and adds the latter to the former
225 my ($Year, $Month, $Day, $Period) = @_;
226 my ($NYear, $NMonth, $NDay);
228 if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days.
229 ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1));
230 } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM
231 ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0)));
233 return ($NYear, $NMonth, $NDay);
236 ################################## postfaq ##################################
237 # Takes a filename and many other vars.
239 # It reads the data-file $File and then posts the article.
242 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
243 my (@Header,@Body,$MID,$InRealBody,$LastModified);
245 print "$$ActName: Preparing to post.\n" if($Options{'v'});
248 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
249 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
252 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID);
253 $MID =~ s/\%n/$$ActName/g;
254 $MID =~ s/\%d/$$TDD/g;
255 $MID =~ s/\%m/$$TDM/g;
256 $MID =~ s/\%y/$$TDY/g;
259 open (FH, "<$$File");
262 push (@Body, $_), next if $InRealBody;
263 $InRealBody++ if /^$/;
264 $LastModified = $1 if /^Last-modified: (\S+)$/i;
268 push @Body, "\n" if ($Body[-1] ne "\n");
270 #Create Date- and Expires-Header:
271 my @time = localtime;
272 my $ss = ($time[0]<10) ? "0" . $time[0] : $time[0];
273 my $mm = ($time[1]<10) ? "0" . $time[1] : $time[1];
274 my $hh = ($time[2]<10) ? "0" . $time[2] : $time[2];
276 my $month = ($time[4]+1<10) ? "0" . ($time[4]+1) : $time[4]+1;
277 my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
278 my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
279 my $year = (1900 + $time[5]);
280 my $tz = $time[8] ? " +0200" : " +0100";
282 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
284 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
285 my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1];
287 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
288 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
290 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
292 $$Subject =~ s/\%LM/$LastModified/;
296 if($Options{'t'} and $Options{'t'} !~ /console/i) {
297 $$NG = $Options{'t'};
300 #Now create the complete Header:
301 push @Header, "From: $$From\n";
302 push @Header, "Newsgroups: $$NG\n";
303 push @Header, "Followup-To: $$Fup2\n" if $$Fup2;
304 push @Header, "Subject: $$Subject\n";
305 push @Header, "Message-ID: $MID\n";
306 push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes;
307 push @Header, "Date: $date\n";
308 push @Header, "Expires: $expdate\n";
309 push @Header, "Sender: $$Sender\n" if $$Sender;
310 push @Header, "Mime-Version: 1.0\n";
311 push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
312 push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
313 push @Header, "Content-Transfer-Encoding: 8bit\n";
314 push @Header, "User-Agent: yapfaq/$Version\n";
315 if ($$ExtraHeaders) {
316 push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
319 # sign article if $UsePGP is true
320 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
323 print "$$ActName: Posting article ...\n" if($Options{'v'});
327 return if($Options{'t'});
329 # otherwise: update status data
330 print "$$ActName: Save status information.\n" if($Options{'v'});
332 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
333 print FH "##;; Lastpost: $day.$month.$year\n";
334 print FH "##;; LastMID: $MID\n";
338 ################################## post ##################################
339 # Takes a complete article (Header and Body).
341 # It opens a connection to $NNTPServer and posts the message.
347 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
348 print "-----BEGIN--------------------------------------------------\n";
350 print "------END---------------------------------------------------\n";
355 if(defined($Options{'s'})) {
356 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
357 print POST @$ArticleR;
362 my $NewsConnection = Net::NNTP->new($Config{'NNTPServer'}, Reader => 1) or die "$0: E: Can't connect to news server '$Config{'NNTPServer'}'!\n";
363 $NewsConnection->authinfo ($Config{'NNTPUser'}, $Config{'NNTPPass'}) if (defined($Config{'NNTPUser'}));
364 $NewsConnection->post();
365 $NewsConnection->datasend (@$ArticleR);
366 $NewsConnection->dataend();
368 # Posting failed? Save to ERROR.dat
369 if (!$NewsConnection->ok()) {
370 open FH, ">>ERROR.dat";
371 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
372 print FH $NewsConnection->code();
373 print FH $NewsConnection->message();
376 print FH "-" x 80, "\n";
380 $NewsConnection->quit();
383 #-------- sub getpgpcommand
384 # getpgpcommand generates the command to sign the message and returns it.
387 # - $PGPVersion: A scalar holding the PGPVersion
389 my ($PGPVersion) = @_;
392 if ($PGPVersion eq '2') {
393 if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
394 open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
395 Config{'$PGPPass'} = <PGPPW>;
399 if (Config{'$PGPPass'}) {
400 $PGPCommand = "PGPPASS=\"".$Config{'PGPPass'}."\" ".$Config{'pgp'}." -u \"".$Config{'PGPSigner'}."\" +verbose=0 language='en' -saft <".$Config{'pgptmpf'}.".txt >".$Config{'pgptmpf'}.".txt.asc";
402 die "$0: E: PGP-Passphrase is unknown!\n";
404 } elsif ($PGPVersion eq '5') {
405 if ($Config{'PathtoPGPPass'}) {
406 $PGPCommand = "PGPPASSFD=2 ".$Config{'pgp'}."s -u \"".$Config{'PGPSigner'}."\" -t --armor -o ".$Config{'pgptmpf'}.".txt.asc -z -f < ".$Config{'pgptmpf'}.".txt 2<".$Config{'PathtoPGPPass'};
408 die "$0: E: PGP-Passphrase is unknown!\n";
410 } elsif ($PGPVersion =~ m/GPG/io) {
411 if (Config{'$PathtoPGPPass'}) {
412 $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";
414 die "$0: E: Passphrase is unknown!\n";
417 die "$0: E: Unknown PGP-Version $PGPVersion!";
423 #-------- sub signarticle
424 # signarticle signs an articel and returns a reference to an array
425 # containing the whole signed Message.
428 # - $HeaderAR: A reference to a array containing the articles headers.
429 # - $BodyR: A reference to an array containing the body.
432 # - $MessageRef: A reference to an array containing the whole message.
434 my ($HeaderAR, $BodyR) = @_;
435 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
437 foreach my $line (@$HeaderAR) {
438 if ($line =~ /^(\S+):\s+(.*)$/s) {
440 $$HeaderR{lc($currentheader)} = "$1: $2";
442 $$HeaderR{lc($currentheader)} .= $line;
446 foreach (@PGPSignHeaders) {
447 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
448 push @signheaders, $_;
452 $pgpbody = join ("", @$BodyR);
454 # Delete and create the temporary pgp-Files
455 unlink "$Config{'pgptmpf'}.txt";
456 unlink "$Config{'pgptmpf'}.txt.asc";
457 $signheaders = join(",", @signheaders);
459 $pgphead = "X-Signed-Headers: $signheaders\n";
460 foreach $header (@signheaders) {
461 if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
462 $pgphead .= $header.": ".$1."\n";
466 open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
467 print FH $pgphead, "\n", $pgpbody;
468 print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
469 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
471 # Start PGP, then read the signature;
472 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
475 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
476 $/ = "$Config{'pgpbegin'}\n";
478 unless (m/\Q$Config{'pgpbegin'}\E$/o) {
479 # unlink $Config{'pgptmpf'} . ".txt";
480 # unlink $Config{'pgptmpf'} . ".txt.asc";
481 die "$0: E: $Config{'pgpbegin'} not found in ".$Config{'pgptmpf'}.".txt.asc\n"
483 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
487 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
488 unlink $Config{'pgptmpf'} . ".txt";
489 unlink $Config{'pgptmpf'} . ".txt.asc";
490 die "$0: E: didn't find PGP Version line where expected.\n";
494 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
496 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
499 do { # skip other pgp headers like
500 $_ = <FH>; # "charset:"||"comment:" until empty line
505 last if /^\Q$Config{'pgpend'}\E$/;
506 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
509 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
513 unlink $Config{'pgptmpf'} . ".txt";
514 unlink $Config{'pgptmpf'} . ".txt.asc";
515 die "$0: E: unexpected data following $Config{'pgpend'}\n";
518 unlink "$Config{'pgptmpf'}.txt.asc";
520 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
521 delete $$HeaderR{$Config{'pgpheader'}};
524 foreach $header (@PGPorderheaders) {
525 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
526 push(@pgphead, "$$HeaderR{$header}");
527 delete $$HeaderR{$header};
531 foreach $header (keys %$HeaderR) {
532 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
533 push(@pgphead, "$$HeaderR{$header}");
534 delete $$HeaderR{$header};
538 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
541 @pgpbody = split /$/m, $pgpbody;
542 my @pgpmessage = (@pgphead, "\n", @pgpbody);
548 ################################ Documentation #################################
552 yapfaq - Post Usenet FAQs I<(yet another postfaq)>
556 B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>] [B<-c> I<.rc file>]
580 Furthermore you need access to a news server to actually post FAQs.
584 B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
585 frequency (every n days, weeks, months or years), adding all necessary
586 headers as defined in its config file (by default F<yapfaq.cfg>).
590 F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
591 a single line, each containing the configuration for one FAQ as a set
592 of definitions in the form of I<param = value>. Everything after a "#"
593 sign is ignored so you may comment your configuration file.
597 =item B<Name> = I<project name>
599 A name referring to your FAQ, also used for generation of a Message-ID.
601 This value must be set.
603 =item B<File> = I<file name>
605 A file containing the message body of your FAQ and all pseudo headers
606 (subheaders in the news.answers style).
608 This value must be set.
610 =item B<Posting-frequency> = I<time period>
612 The posting frequency defines how often your FAQ will be posted.
613 B<yapfaq> will only post your FAQ if this period of time has passed
614 since the last posting.
616 You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
617 or I<B<m>onths> or I<B<y>ears>.
619 This value must be set.
621 =item B<Expires> = I<time period>
623 The period of time after which your message will expire. An Expires
624 header will be calculated adding this time period to today's date.
626 You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
627 or I<B<m>onths> or I<B<y>ears>.
629 This setting is optional; the default is 3 months.
631 =item B<From> = I<author>
633 The author of your FAQ as it will appear in the From header of the
636 This value must be set.
638 =item B<Subject> = I<subject>
640 The title of your FAQ as it will appear in the Subject header of the
643 You may use the special string C<%LM> which will be replaced with
644 the contents of the Last-Modified subheader in your I<File>.
646 This value must be set.
648 =item B<NGs> = I<newsgroups>
650 A comma-separated list of newsgroup(s) to post your FAQ to as it will
651 appear in the Newsgroups header of the message.
653 This value must be set.
655 =item B<Fup2> = I<newsgroup | poster>
657 A comma-separated list of newsgroup(s) or the special string I<poster>
658 as it will appear in the Followup-To header of the message.
660 This setting is optional.
662 =item B<MID-Format> = I<pattern>
664 A pattern from which the message ID is generated as it will appear in
665 the Message-ID header of the message.
667 You may use the special strings C<%n> for the I<Name> of your project,
668 C<%d> for the date the message is posted, C<%m> for the month and
669 C<%y> for the year, respectively.
671 This value must be set.
673 =item B<Supersede> = I<yes>
675 Add Supersedes header to the message containing the Message-ID header
678 This setting is optional; you should set it to yes or leave it out.
680 =item B<ExtraHeader> = I<additional headers>
682 The contents of I<ExtraHeader> is added verbatim to the headers of
683 your message so you can add custom headers like Approved.
685 This setting is optional.
689 =head3 Example configuration file
691 # name of your project
694 # file to post (complete body and pseudo-headers)
695 # ($File.cfg contains data on last posting and last MID)
698 # how often your project should be posted
699 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
700 Posting-frequency = '1d'
702 # time period after which the posting should expire
703 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
707 From = 'test@domain.invalid'
710 # (may contain "%LM" which will be replaced by the contents of the
711 # Last-Modified pseudo header).
712 Subject = 'test noreply ignore'
714 # comma-separated list of newsgroup(s) to post to
715 # (header "Newsgroups:")
718 # header "Followup-To:"
721 # Message-ID ("%n" is $Name)
722 MID-Format = '<%n-%d.%m.%y@domain.invalid>'
724 # Supersede last posting?
727 # extra headers (appended verbatim)
728 # use this for custom headers like "Approved:"
729 ExtraHeader = 'Approved: moderator@domain.invalid
732 # other projects may follow separated with "====="
737 Posting-frequency = '2m'
738 From = 'My Name <my.name@domain.invalid>'
739 Subject = 'Test of yapfag <%LM>'
740 NGs = 'de.test,de.alt.test'
742 MID-Format = '<%n-%m.%y@domain.invalid>'
745 =head3 Status Information
747 Information about the last post and about how to form message IDs for
748 posts is stored in a file named F<I<project name>.cfg> which will be
749 generated if it does not exist. Each of those status files will
750 contain two lines, the first being the date of the last time the FAQ
751 was posted and the second being the message ID of that incarnation.
753 =head2 Runtime Configuration
755 Apart from configuring which FAQ(s) to post you may (re)set some
756 runtime configuration variables via the .rcfile (by default
757 F<.yapfaqrc>). F<.yapfaqrc> must contain one definition in the form of
758 I<param = value> on each line; everything after a "#" sign is ignored.
760 If you omit some settings they will be set to default values hardcoded
763 B<Please note that all parameter names are case-sensitive!>
767 =item B<NNTPServer> = I<NNTP server> (mandatory)
769 Host name of the NNTP server to post to. Must be set (or omitted; the
770 default is "localhost"); if set to en empty string, B<yapfaq> falls
771 back to Perl's build-in defaults (contents of environment variables
772 NNTPSERVER and NEWSHOST; if not set, default from Net::Config; if not
773 set, "news" is used).
775 =item B<NNTPUser> = I<user name> (optional)
777 User name used for authentication with the NNTP server (I<AUTHINFO
780 This setting is optional; if it is not set, I<NNTPPass> is ignored and
781 no authentication is tried.
783 =item B<NNTPPass> = I<password> (optional)
785 Password used for authentication with the NNTP server (I<AUTHINFO
788 This setting is optional; it must be set if I<NNTPUser> is present.
790 =item B<Sender> = I<Sender header> (optional)
792 The Sender header that will be added to every posted message.
794 This setting is optional.
796 =item B<ConfigFile> = I<configuration file> (mandatory)
798 The configuration file defining the FAQ(s) to post. Must be set (or
799 omitted; the default is "yapfaq.cfg").
801 =item B<UsePGP> = I<whether to add a digital signature> (optional)
803 Boolean value (0 or 1) controlling whether the FAQs will get digitally
804 signed via an X-PGP-Sig header.
806 This setting is optional; the default is 0.
808 If you have set I<UsePGP> to 1, you must also supply the necessary
809 information on your PGP oder GPG installation; please refer to the
810 sample F<.yapfaqrc> file (see below) for more information on this
815 =head3 Example runtime configuration file
817 NNTPServer = 'localhost'
821 ConfigFile = 'yapfaq.cfg'
824 ################################## PGP-Config #################################
825 pgp = '/usr/bin/pgp' # path to pgp
826 PGPVersion = '2' # Use 2 for 2.X 5 for PGP > 2.X and GPG for GPG
827 PGPSigner = '' # sign as who?
828 PGPPass = '' # pgp2 only
829 PathtoPGPPass = '' # pgp2 pgp5 and gpg
830 pgpbegin = '-----BEGIN PGP SIGNATURE-----' # Begin of PGP-Signature
831 pgpend = '-----END PGP SIGNATURE-----' # End of PGP-Signature
832 pgptmpf = 'pgptmp' # temporary file for PGP.
833 pgpheader = 'X-PGP-Sig'
835 =head3 Using more than one runtime configuration
837 You may use more than one runtime configuration file with the B<-c>
844 =item B<-V> (version)
846 Print out version and copyright information on B<yapfaq> and exit.
850 Print this man page and exit.
852 =item B<-v> (verbose)
854 Print out status information while running to STDOUT.
856 =item B<-p> (post unconditionally)
858 Post (all) FAQs unconditionally ignoring the posting frequency setting.
860 You may want to use this with the B<-f> option (see below).
862 =item B<-d> (dry run)
864 Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
865 update any status information.
867 =item B<-t> I<newsgroup(s) | CONSOLE> (test)
869 Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
870 newsgroups given after B<-t> as a comma-separated list or print the
871 FAQs to STDOUT separated by lines of dashes if the special string
872 C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
873 do without embarassing yourself on Usenet. The status files are not
874 updated when this option is given.
876 You may want to use this with the B<-f> option (see below).
878 =item B<-f> I<project name>
880 Just deal with one FAQ only.
882 By default B<yapfaq> will work on all FAQs that are defined in
883 F<yapfaq.cfg>, check whether they are due for posting and - if they
884 are - post them. Consequently when the B<-p> option is set all FAQs
885 will be posted unconditionally. That may not be what you want to
886 achieve, so you can limit the operation of B<yapfaq> to the named FAQ
889 =item B<-s> I<program> (pipe to script)
891 Instead of posting the article(s) to Usenet pipe them to the external
892 I<program> on STDIN (which may post the article(s) then). A return
893 value of 0 will be considered success.
895 =item B<-c> I<.rc file>
897 Load another runtime configuration file (.rc file) than F<.yaofaq.rc>.
899 You may for example define another usenet server to post your FAQ(s)
900 to or load another configuration file defining (an)other FAQ(s).
906 Post all FAQs that are due for posting:
910 Do a dry run, showing which FAQs would be posted:
914 Do a test run and print on STDOUT what the FAQ I<myfaq> would look
915 like when posted, regardless whether it is due for posting or not:
917 yapfaq -pt CONSOLE -f myfaq
919 Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
922 yapfaq -t de.test -f myfaq
926 There are no special environment variables used by B<yapfaq>.
938 Runtime configuration file for B<yapfaq>.
942 Configuration file for B<yapfaq>.
948 The status files will be created on successful posting if they don't
949 already exist. The first line of the file will be the date of the last
950 time the FAQ was posted and the second line will be the message ID of
951 the last post of that FAQ.
961 L<http://th-h.de/download/scripts.php> will have the current
962 version of this program.
966 Thomas Hochstein <thh@inter.net>
968 Original author (up to version 0.5b, dating from 2003):
969 Marc Brockschmidt <marc@marcbrockschmidt.de>
971 =head1 COPYRIGHT AND LICENSE
973 Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
975 Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
977 This program is free software; you may redistribute it and/or modify it
978 under the same terms as Perl itself.