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