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