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