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 %Config = (NNTPServer => "localhost",
21 ConfigFile => "yapfaq.cfg",
24 ################################## PGP-Config #################################
25 pgp => '/usr/bin/pgp', # path to pgp
26 PGPVersion => '2', # Use 2 for 2.X, 5 for PGP > 2.X and GPG for GPG
27 PGPSigner => '', # sign as who?
28 PGPPass => '', # pgp2 only
29 PathtoPGPPass => '', # pgp2, pgp5 and gpg
30 pgpbegin => '-----BEGIN PGP SIGNATURE-----', # Begin of PGP-Signature
31 pgpend => '-----END PGP SIGNATURE-----', # End of PGP-Signature
32 pgptmpf => 'pgptmp', # temporary file for PGP.
33 pgpheader => 'X-PGP-Sig');
35 my @PGPSignHeaders = ('From', 'Newsgroups', 'Subject', 'Control',
36 'Supersedes', 'Followup-To', 'Date', 'Sender', 'Approved',
37 'Message-ID', 'Reply-To', 'Cancel-Lock', 'Cancel-Key',
38 'Also-Control', 'Distribution');
40 my @PGPorderheaders = ('from', 'newsgroups', 'subject', 'control',
41 'supersedes', 'followup-To', 'date', 'organization', 'lines',
42 'sender', 'approved', 'distribution', 'message-id',
43 'references', 'reply-to', 'mime-version', 'content-type',
44 'content-transfer-encoding', 'summary', 'keywords', 'cancel-lock',
45 'cancel-key', 'also-control', 'x-pgp', 'user-agent');
47 ############################# End of Configuration #############################
51 use Net::Domain qw(hostfqdn);
52 use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
53 use Fcntl ':flock'; # import LOCK_* constants
55 my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
57 # read commandline options
59 getopts('Vhvpdt:f:s:', \%Options);
60 # -V: print version / copyright information
62 print "$0 v $Version\nCopyright (c) 2003 Marc Brockschmidt <marc\@marcbrockschmidt.de>\nCopyright (c) 2010 Thomas Hochstein <thh\@inter.net>\n";
63 print "This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.\n";
66 # -h: feed myself to perldoc
72 my ($Faq) = $Options{'f'} if ($Options{'f'});
74 # read configuration (configured FAQs)
76 readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq);
79 # - parse configuration
81 # - if FAQ is due: call postfaq()
83 my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date
84 my ($NPY,$NPM,$NPD); #NP: Next posting-date
87 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
88 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
89 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
90 my ($Supersede) =($$_{'supersede'});
92 # -f: loop if not FAQ to post
93 next if (defined($Faq) && $ActName ne $Faq);
96 if (open (FH, "<$File.cfg")) {
98 if (/##;; Lastpost:\s*(\d{1,2})\.(\d{1,2})\.(\d{2}(\d{2})?)/){
99 ($LPD, $LPM, $LPY) = ($1, $2, $3);
100 } elsif (/^##;;\s*LastMID:\s*(<\S+@\S+>)\s*$/) {
106 warn "$0: W: Couldn't open $File.cfg: $!\n";
109 $SupersedeMID = "" unless $Supersede;
111 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
113 # if FAQ is due: get it out
114 if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) {
116 print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'};
118 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
120 } elsif($Options{'v'}) {
121 print "$ActName: Nothing to do.\n";
127 ################################## readconfig ##################################
128 # Takes a filename, a reference to an array, which will hold hashes with
129 # the data from $File, and - optionally - the name of the (single) FAQ to post
132 my ($File, $Config, $Faq) = @_;
133 my ($LastEntry, $Error, $i) = ('','',0);
135 print "Reading configuration.\n" if($Options{'v'});
137 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
139 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
140 if (/^(\s*(\S+)\s*=\s*'?(.*?)'?\s*(#.*$|$)|^(.*?)'?\s*(#.*$|$))/ && not /^\s*$/) {
141 $LastEntry = lc($2) if $2;
142 $$Config[$i]{$LastEntry} .= $3 if $3;
143 $$Config[$i]{$LastEntry} .= "\n$5" if $5 && $5;
145 if (/^\s*=====\s*$/) {
153 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
154 unless(defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} =~ /^\S+$/) {
155 $Error .= "E: The name of your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
157 unless(defined($$Config[$i]{'file'}) && -f $$Config[$i]{'file'}) {
158 $Error .= "E: The file to post for your project \"$$Config[$i]{'name'}\" is not defined or does not exist.\n"
160 unless(defined($$Config[$i]{'from'}) && $$Config[$i]{'from'} =~ /\S+\@(\S+\.)?\S{2,}\.\S{2,}/) {
161 $Error .= "E: The From header for your project \"$$Config[$i]{'name'}\" seems to be incorrect.\n"
163 unless(defined($$Config[$i]{'ngs'}) && $$Config[$i]{'ngs'} =~ /^\S+$/) {
164 $Error .= "E: The Newsgroups header for your project \"$$Config[$i]{'name'}\" is not defined or contains whitespaces.\n"
166 unless(defined($$Config[$i]{'subject'})) {
167 $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n"
169 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
170 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
172 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
173 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
175 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
176 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
178 unless(defined($$Config[$i]{'mid-format'}) && $$Config[$i]{'mid-format'} =~ /^<\S+\@\S{2,}\.\S{2,}>$/) {
179 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" seems to be invalid - set to default.\n";
182 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
183 die $Error if $Error;
186 ################################# calcdelta #################################
187 # Takes a date (year, month and day) and a time period (1d, 1w, 1m, 1y, ...)
188 # and adds the latter to the former
191 my ($Year, $Month, $Day, $Period) = @_;
192 my ($NYear, $NMonth, $NDay);
194 if ($Period =~ /(\d+)\s*([dw])/) { # Is counted in days or weeks: Use Add_Delta_Days.
195 ($NYear, $NMonth, $NDay) = Add_Delta_Days($Year, $Month, $Day, (($2 eq "w")?$1 * 7: $1 * 1));
196 } elsif ($Period =~ /(\d+)\s*([my])/) { #Is counted in months or years: Use Add_Delta_YM
197 ($NYear, $NMonth, $NDay) = Add_Delta_YM($Year, $Month, $Day, (($2 eq "m")?(0,$1):($1,0)));
199 return ($NYear, $NMonth, $NDay);
202 ################################## postfaq ##################################
203 # Takes a filename and many other vars.
205 # It reads the data-file $File and then posts the article.
208 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
209 my (@Header,@Body,$MID,$InRealBody,$LastModified);
211 print "$$ActName: Preparing to post.\n" if($Options{'v'});
214 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
215 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
218 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID);
219 $MID =~ s/\%n/$$ActName/g;
220 $MID =~ s/\%d/$$TDD/g;
221 $MID =~ s/\%m/$$TDM/g;
222 $MID =~ s/\%y/$$TDY/g;
225 open (FH, "<$$File");
228 push (@Body, $_), next if $InRealBody;
229 $InRealBody++ if /^$/;
230 $LastModified = $1 if /^Last-modified: (\S+)$/i;
234 push @Body, "\n" if ($Body[-1] ne "\n");
236 #Create Date- and Expires-Header:
237 my @time = localtime;
238 my $ss = ($time[0]<10) ? "0" . $time[0] : $time[0];
239 my $mm = ($time[1]<10) ? "0" . $time[1] : $time[1];
240 my $hh = ($time[2]<10) ? "0" . $time[2] : $time[2];
242 my $month = ($time[4]+1<10) ? "0" . ($time[4]+1) : $time[4]+1;
243 my $monthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$time[4]];
244 my $wday = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$time[6]];
245 my $year = (1900 + $time[5]);
246 my $tz = $time[8] ? " +0200" : " +0100";
248 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
250 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
251 my $expmonthN = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$expM-1];
253 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
254 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
256 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
258 $$Subject =~ s/\%LM/$LastModified/;
262 if($Options{'t'} and $Options{'t'} !~ /console/i) {
263 $$NG = $Options{'t'};
266 #Now create the complete Header:
267 push @Header, "From: $$From\n";
268 push @Header, "Newsgroups: $$NG\n";
269 push @Header, "Followup-To: $$Fup2\n" if $$Fup2;
270 push @Header, "Subject: $$Subject\n";
271 push @Header, "Message-ID: $MID\n";
272 push @Header, "Supersedes: $$Supersedes\n" if $$Supersedes;
273 push @Header, "Date: $date\n";
274 push @Header, "Expires: $expdate\n";
275 push @Header, "Sender: $$Sender\n" if $$Sender;
276 push @Header, "Mime-Version: 1.0\n";
277 push @Header, "Reply-To: $$ReplyTo\n" if $$ReplyTo;
278 push @Header, "Content-Type: text/plain; charset=ISO-8859-15\n";
279 push @Header, "Content-Transfer-Encoding: 8bit\n";
280 push @Header, "User-Agent: yapfaq/$Version\n";
281 if ($$ExtraHeaders) {
282 push @Header, "$_\n" for (split /\n/, $$ExtraHeaders);
285 # sign article if $UsePGP is true
286 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
289 print "$$ActName: Posting article ...\n" if($Options{'v'});
293 return if($Options{'t'});
295 # otherwise: update status data
296 print "$$ActName: Save status information.\n" if($Options{'v'});
298 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
299 print FH "##;; Lastpost: $day.$month.$year\n";
300 print FH "##;; LastMID: $MID\n";
304 ################################## post ##################################
305 # Takes a complete article (Header and Body).
307 # It opens a connection to $NNTPServer and posts the message.
313 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
314 print "-----BEGIN--------------------------------------------------\n";
316 print "------END---------------------------------------------------\n";
321 if(defined($Options{'s'})) {
322 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
323 print POST @$ArticleR;
328 my $NewsConnection = Net::NNTP->new($Config{'NNTPServer'}, Reader => 1) or die "$0: E: Can't connect to news server '$Config{'NNTPServer'}'!\n";
329 $NewsConnection->authinfo ($Config{'NNTPUser'}, $Config{'NNTPPass'}) if (defined($Config{'NNTPUser'}));
330 $NewsConnection->post();
331 $NewsConnection->datasend (@$ArticleR);
332 $NewsConnection->dataend();
334 # Posting failed? Save to ERROR.dat
335 if (!$NewsConnection->ok()) {
336 open FH, ">>ERROR.dat";
337 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
338 print FH $NewsConnection->code();
339 print FH $NewsConnection->message();
342 print FH "-" x 80, "\n";
346 $NewsConnection->quit();
349 #-------- sub getpgpcommand
350 # getpgpcommand generates the command to sign the message and returns it.
353 # - $PGPVersion: A scalar holding the PGPVersion
355 my ($PGPVersion) = @_;
358 if ($PGPVersion eq '2') {
359 if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
360 open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
361 Config{'$PGPPass'} = <PGPPW>;
365 if (Config{'$PGPPass'}) {
366 $PGPCommand = "PGPPASS=\"".$Config{'PGPPass'}."\" ".$Config{'pgp'}." -u \"".$Config{'PGPSigner'}."\" +verbose=0 language='en' -saft <".$Config{'pgptmpf'}.".txt >".$Config{'pgptmpf'}.".txt.asc";
368 die "$0: E: PGP-Passphrase is unknown!\n";
370 } elsif ($PGPVersion eq '5') {
371 if ($Config{'PathtoPGPPass'}) {
372 $PGPCommand = "PGPPASSFD=2 ".$Config{'pgp'}."s -u \"".$Config{'PGPSigner'}."\" -t --armor -o ".$Config{'pgptmpf'}.".txt.asc -z -f < ".$Config{'pgptmpf'}.".txt 2<".$Config{'PathtoPGPPass'};
374 die "$0: E: PGP-Passphrase is unknown!\n";
376 } elsif ($PGPVersion =~ m/GPG/io) {
377 if (Config{'$PathtoPGPPass'}) {
378 $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";
380 die "$0: E: Passphrase is unknown!\n";
383 die "$0: E: Unknown PGP-Version $PGPVersion!";
389 #-------- sub signarticle
390 # signarticle signs an articel and returns a reference to an array
391 # containing the whole signed Message.
394 # - $HeaderAR: A reference to a array containing the articles headers.
395 # - $BodyR: A reference to an array containing the body.
398 # - $MessageRef: A reference to an array containing the whole message.
400 my ($HeaderAR, $BodyR) = @_;
401 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
403 foreach my $line (@$HeaderAR) {
404 if ($line =~ /^(\S+):\s+(.*)$/s) {
406 $$HeaderR{lc($currentheader)} = "$1: $2";
408 $$HeaderR{lc($currentheader)} .= $line;
412 foreach (@PGPSignHeaders) {
413 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
414 push @signheaders, $_;
418 $pgpbody = join ("", @$BodyR);
420 # Delete and create the temporary pgp-Files
421 unlink "$Config{'pgptmpf'}.txt";
422 unlink "$Config{'pgptmpf'}.txt.asc";
423 $signheaders = join(",", @signheaders);
425 $pgphead = "X-Signed-Headers: $signheaders\n";
426 foreach $header (@signheaders) {
427 if ($$HeaderR{lc($header)} =~ m/^[^\s:]+: (.+?)\n?$/so) {
428 $pgphead .= $header.": ".$1."\n";
432 open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
433 print FH $pgphead, "\n", $pgpbody;
434 print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
435 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
437 # Start PGP, then read the signature;
438 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
441 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
442 $/ = "$Config{'pgpbegin'}\n";
444 unless (m/\Q$Config{'pgpbegin'}\E$/o) {
445 # unlink $Config{'pgptmpf'} . ".txt";
446 # unlink $Config{'pgptmpf'} . ".txt.asc";
447 die "$0: E: $Config{'pgpbegin'} not found in ".$Config{'pgptmpf'}.".txt.asc\n"
449 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
453 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
454 unlink $Config{'pgptmpf'} . ".txt";
455 unlink $Config{'pgptmpf'} . ".txt.asc";
456 die "$0: E: didn't find PGP Version line where expected.\n";
460 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
462 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
465 do { # skip other pgp headers like
466 $_ = <FH>; # "charset:"||"comment:" until empty line
471 last if /^\Q$Config{'pgpend'}\E$/;
472 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
475 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
479 unlink $Config{'pgptmpf'} . ".txt";
480 unlink $Config{'pgptmpf'} . ".txt.asc";
481 die "$0: E: unexpected data following $Config{'pgpend'}\n";
484 unlink "$Config{'pgptmpf'}.txt.asc";
486 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
487 delete $$HeaderR{$Config{'pgpheader'}};
490 foreach $header (@PGPorderheaders) {
491 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
492 push(@pgphead, "$$HeaderR{$header}");
493 delete $$HeaderR{$header};
497 foreach $header (keys %$HeaderR) {
498 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
499 push(@pgphead, "$$HeaderR{$header}");
500 delete $$HeaderR{$header};
504 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
507 @pgpbody = split /$/m, $pgpbody;
508 my @pgpmessage = (@pgphead, "\n", @pgpbody);
514 ################################ Documentation #################################
518 yapfaq - Post Usenet FAQs I<(yet another postfaq)>
522 B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>]
546 Furthermore you need access to a news server to actually post FAQs.
550 B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
551 frequency (every n days, weeks, months or years), adding all necessary
552 headers as defined in its config file (by default F<yapfaq.cfg>).
556 F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
557 a single line, each containing the configuration for one FAQ as a set
558 of definitions in the form of I<param = value>.
562 =item B<Name> = I<project name>
564 A name referring to your FAQ, also used for generation of a Message-ID.
566 This value must be set.
568 =item B<File> = I<file name>
570 A file containing the message body of your FAQ and all pseudo headers
571 (subheaders in the news.answers style).
573 This value must be set.
575 =item B<Posting-frequency> = I<time period>
577 The posting frequency defines how often your FAQ will be posted.
578 B<yapfaq> will only post your FAQ if this period of time has passed
579 since the last posting.
581 You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
582 or I<B<m>onths> or I<B<y>ears>.
584 This value must be set.
586 =item B<Expires> = I<time period>
588 The period of time after which your message will expire. An Expires
589 header will be calculated adding this time period to today's date.
591 You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
592 or I<B<m>onths> or I<B<y>ears>.
594 This setting is optional; the default is 3 months.
596 =item B<From> = I<author>
598 The author of your FAQ as it will appear in the From header of the
601 This value must be set.
603 =item B<Subject> = I<subject>
605 The title of your FAQ as it will appear in the Subject header of the
608 You may use the special string C<%LM> which will be replaced with
609 the contents of the Last-Modified subheader in your I<File>.
611 This value must be set.
613 =item B<NGs> = I<newsgroups>
615 A comma-separated list of newsgroup(s) to post your FAQ to as it will
616 appear in the Newsgroups header of the message.
618 This value must be set.
620 =item B<Fup2> = I<newsgroup | poster>
622 A comma-separated list of newsgroup(s) or the special string I<poster>
623 as it will appear in the Followup-To header of the message.
625 This setting is optional.
627 =item B<MID-Format> = I<pattern>
629 A pattern from which the message ID is generated as it will appear in
630 the Message-ID header of the message.
632 You may use the special strings C<%n> for the I<Name> of your project,
633 C<%d> for the date the message is posted, C<%m> for the month and
634 C<%y> for the year, respectively.
636 This value must be set.
638 =item B<Supersede> = I<yes>
640 Add Supersedes header to the message containing the Message-ID header
643 This setting is optional; you should set it to yes or leave it out.
645 =item B<ExtraHeader> = I<additional headers>
647 The contents of I<ExtraHeader> is added verbatim to the headers of
648 your message so you can add custom headers like Approved.
650 This setting is optional.
654 =head2 Example configuration file
656 # name of your project
659 # file to post (complete body and pseudo-headers)
660 # ($File.cfg contains data on last posting and last MID)
663 # how often your project should be posted
664 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
665 Posting-frequency = '1d'
667 # time period after which the posting should expire
668 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
672 From = 'test@domain.invalid'
675 # (may contain "%LM" which will be replaced by the contents of the
676 # Last-Modified pseudo header).
677 Subject = 'test noreply ignore'
679 # comma-separated list of newsgroup(s) to post to
680 # (header "Newsgroups:")
683 # header "Followup-To:"
686 # Message-ID ("%n" is $Name)
687 MID-Format = '<%n-%d.%m.%y@domain.invalid>'
689 # Supersede last posting?
692 # extra headers (appended verbatim)
693 # use this for custom headers like "Approved:"
694 ExtraHeader = 'Approved: moderator@domain.invalid
697 # other projects may follow separated with "====="
702 Posting-frequency = '2m'
703 From = 'My Name <my.name@domain.invalid>'
704 Subject = 'Test of yapfag <%LM>'
705 NGs = 'de.test,de.alt.test'
707 MID-Format = '<%n-%m.%y@domain.invalid>'
710 Information about the last post and about how to form message IDs for
711 posts is stored in a file named F<I<project name>.cfg> which will be
712 generated if it does not exist. Each of those status files will
713 contain two lines, the first being the date of the last time the FAQ
714 was posted and the second being the message ID of that incarnation.
720 =item B<-V> (version)
722 Print out version and copyright information on B<yapfaq> and exit.
726 Print this man page and exit.
728 =item B<-v> (verbose)
730 Print out status information while running to STDOUT.
732 =item B<-p> (post unconditionally)
734 Post (all) FAQs unconditionally ignoring the posting frequency setting.
736 You may want to use this with the B<-f> option (see below).
738 =item B<-d> (dry run)
740 Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
741 update any status information.
743 =item B<-t> I<newsgroup(s) | CONSOLE> (test)
745 Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
746 newsgroups given after B<-t> as a comma-separated list or print the
747 FAQs to STDOUT separated by lines of dashes if the special string
748 C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
749 do without embarassing yourself on Usenet. The status files are not
750 updated when this option is given.
752 You may want to use this with the B<-f> option (see below).
754 =item B<-f> I<project name>
756 Just deal with one FAQ only.
758 By default B<yapfaq> will work on all FAQs that are defined in
759 F<yapfaq.cfg>, check whether they are due for posting and - if they
760 are - post them. Consequently when the B<-p> option is set all FAQs
761 will be posted unconditionally. That may not be what you want to
762 achieve, so you can limit the operation of B<yapfaq> to the named FAQ
765 =item B<-s> I<program> (pipe to script)
767 Instead of posting the article(s) to Usenet pipe them to the external
768 I<program> on STDIN (which may post the article(s) then). A return
769 value of 0 will be considered success.
775 Post all FAQs that are due for posting:
779 Do a dry run, showing which FAQs would be posted:
783 Do a test run and print on STDOUT what the FAQ I<myfaq> would look
784 like when posted, regardless whether it is due for posting or not:
786 yapfaq -pt CONSOLE -f myfaq
788 Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
791 yapfaq -t de.test -f myfaq
795 There are no special environment variables used by B<yapfaq>.
807 Configuration file for B<yapfaq>.
813 The status files will be created on successful posting if they don't
814 already exist. The first line of the file will be the date of the last
815 time the FAQ was posted and the second line will be the message ID of
816 the last post of that FAQ.
826 L<http://th-h.de/download/scripts.php> will have the current
827 version of this program.
831 Thomas Hochstein <thh@inter.net>
833 Original author (until version 0.5b from 2003):
834 Marc Brockschmidt <marc@marcbrockschmidt.de>
837 =head1 COPYRIGHT AND LICENSE
839 Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
841 Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
843 This program is free software; you may redistribute it and/or modify it
844 under the same terms as Perl itself.