Moved configuration to a hash (%Config).
[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 %Config = (NNTPServer => "localhost",
18 NNTPUser => "",
19 NNTPPass => "",
20 Sender => "",
21 ConfigFile => "yapfaq.cfg",
22 UsePGP => 0,
23
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');
34
35my @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');
39
40my @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');
46
47############################# End of Configuration #############################
48
49use strict;
50use Net::NNTP;
51use Net::Domain qw(hostfqdn);
52use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
53use Fcntl ':flock'; # import LOCK_* constants
54use Getopt::Std;
55my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
56
57# read commandline options
58my %Options;
59getopts('Vhvpdt:f:s:', \%Options);
60# -V: print version / copyright information
61if ($Options{'V'}) {
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";
64 exit(0);
65}
66# -h: feed myself to perldoc
67if ($Options{'h'}) {
68 exec ('perldoc', $0);
69 exit(0);
70};
71# -f: set $Faq
72my ($Faq) = $Options{'f'} if ($Options{'f'});
73
74# read configuration (configured FAQs)
75my @Config;
76readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq);
77
78# for each FAQ:
79# - parse configuration
80# - read status data
81# - if FAQ is due: call postfaq()
82foreach (@Config) {
83 my ($LPD,$LPM,$LPY) = (01, 01, 0001); #LP: Last posting-date
84 my ($NPY,$NPM,$NPD); #NP: Next posting-date
85 my $SupersedeMID;
86
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'});
91
92 # -f: loop if not FAQ to post
93 next if (defined($Faq) && $ActName ne $Faq);
94
95 # read status data
96 if (open (FH, "<$File.cfg")) {
97 while(<FH>){
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*$/) {
101 $SupersedeMID = $1;
102 }
103 }
104 close FH;
105 } else {
106 warn "$0: W: Couldn't open $File.cfg: $!\n";
107 }
108
109 $SupersedeMID = "" unless $Supersede;
110
111 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
112
113 # if FAQ is due: get it out
114 if (Delta_Days($NPY,$NPM,$NPD,$TDY,$TDM,$TDD) >= 0 or ($Options{'p'})) {
115 if($Options{'d'}) {
116 print "$ActName: Would be posted now (but running in simulation mode [$0 -d]).\n" if $Options{'v'};
117 } else {
118 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
119 }
120 } elsif($Options{'v'}) {
121 print "$ActName: Nothing to do.\n";
122 }
123}
124
125exit;
126
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
130
131sub readconfig{
132 my ($File, $Config, $Faq) = @_;
133 my ($LastEntry, $Error, $i) = ('','',0);
134
135 print "Reading configuration.\n" if($Options{'v'});
136
137 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
138 while (<FH>) {
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;
144 }
145 if (/^\s*=====\s*$/) {
146 $i++;
147 }
148 }
149 close FH;
150
151 #Check saved values:
152 for $i (0..$i){
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"
156 }
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"
159 }
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"
162 }
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"
165 }
166 unless(defined($$Config[$i]{'subject'})) {
167 $Error .= "E: The Subject header for your project \"$$Config[$i]{'name'}\" is not defined.\n"
168 }
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"
171 }
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"
174 }
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";
177 }
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";
180 }
181 }
182 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
183 die $Error if $Error;
184}
185
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
189
190sub calcdelta {
191 my ($Year, $Month, $Day, $Period) = @_;
192 my ($NYear, $NMonth, $NDay);
193
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)));
198 }
199 return ($NYear, $NMonth, $NDay);
200}
201
202################################## postfaq ##################################
203# Takes a filename and many other vars.
204#
205# It reads the data-file $File and then posts the article.
206
207sub postfaq {
208 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
209 my (@Header,@Body,$MID,$InRealBody,$LastModified);
210
211 print "$$ActName: Preparing to post.\n" if($Options{'v'});
212
213 #Prepare MID:
214 $$TDM = ($$TDM < 10 && $$TDM !~ /^0/) ? "0" . $$TDM : $$TDM;
215 $$TDD = ($$TDD < 10 && $$TDD !~ /^0/) ? "0" . $$TDD : $$TDD;
216
217 $MID = $$MIDF;
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;
223
224 #Now get the body:
225 open (FH, "<$$File");
226 while (<FH>){
227 s/\r//;
228 push (@Body, $_), next if $InRealBody;
229 $InRealBody++ if /^$/;
230 $LastModified = $1 if /^Last-modified: (\S+)$/i;
231 push @Body, $_;
232 }
233 close FH;
234 push @Body, "\n" if ($Body[-1] ne "\n");
235
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];
241 my $day = $time[3];
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";
247
248 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
249
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];
252
253 my $date = "$day $monthN $year " . $hh . ":" . $mm . ":" . $ss . $tz;
254 my $expdate = "$expD $expmonthN $expY $hh:$mm:$ss$tz";
255
256 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
257 if ($LastModified) {
258 $$Subject =~ s/\%LM/$LastModified/;
259 }
260
261 # Test mode?
262 if($Options{'t'} and $Options{'t'} !~ /console/i) {
263 $$NG = $Options{'t'};
264 }
265
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);
283 }
284
285 # sign article if $UsePGP is true
286 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
287
288 # post article
289 print "$$ActName: Posting article ...\n" if($Options{'v'});
290 post(\@Article);
291
292 # Test mode?
293 return if($Options{'t'});
294
295 # otherwise: update status data
296 print "$$ActName: Save status information.\n" if($Options{'v'});
297
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";
301 close FH;
302}
303
304################################## post ##################################
305# Takes a complete article (Header and Body).
306#
307# It opens a connection to $NNTPServer and posts the message.
308
309sub post {
310 my ($ArticleR) = @_;
311
312 # Test mode?
313 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
314 print "-----BEGIN--------------------------------------------------\n";
315 print @$ArticleR;
316 print "------END---------------------------------------------------\n";
317 return;
318 }
319
320 # pipe to script?
321 if(defined($Options{'s'})) {
322 open (POST, "| $Options{'s'}") or die "$0: E: Cannot fork $Options{'s'}: $!\n";
323 print POST @$ArticleR;
324 close POST;
325 return;
326 }
327
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();
333
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();
340 print FH "\n";
341 print FH @$ArticleR;
342 print FH "-" x 80, "\n";
343 close FH;
344 }
345
346 $NewsConnection->quit();
347}
348
349#-------- sub getpgpcommand
350# getpgpcommand generates the command to sign the message and returns it.
351#
352# Receives:
353# - $PGPVersion: A scalar holding the PGPVersion
354sub getpgpcommand {
355 my ($PGPVersion) = @_;
356 my $PGPCommand;
357
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>;
362 close PGPPW;
363 }
364
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";
367 } else {
368 die "$0: E: PGP-Passphrase is unknown!\n";
369 }
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'};
373 } else {
374 die "$0: E: PGP-Passphrase is unknown!\n";
375 }
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";
379 } else {
380 die "$0: E: Passphrase is unknown!\n";
381 }
382 } else {
383 die "$0: E: Unknown PGP-Version $PGPVersion!";
384 }
385 return $PGPCommand;
386}
387
388
389#-------- sub signarticle
390# signarticle signs an articel and returns a reference to an array
391# containing the whole signed Message.
392#
393# Receives:
394# - $HeaderAR: A reference to a array containing the articles headers.
395# - $BodyR: A reference to an array containing the body.
396#
397# Returns:
398# - $MessageRef: A reference to an array containing the whole message.
399sub signpgp {
400 my ($HeaderAR, $BodyR) = @_;
401 my (@pgphead, @pgpbody, $pgphead, $pgpbody, $header, $signheaders, @signheaders, $currentheader, $HeaderR, $line);
402
403 foreach my $line (@$HeaderAR) {
404 if ($line =~ /^(\S+):\s+(.*)$/s) {
405 $currentheader = $1;
406 $$HeaderR{lc($currentheader)} = "$1: $2";
407 } else {
408 $$HeaderR{lc($currentheader)} .= $line;
409 }
410 }
411
412 foreach (@PGPSignHeaders) {
413 if (defined($$HeaderR{lc($_)}) && $$HeaderR{lc($_)} =~ m/^[^\s:]+: .+/o) {
414 push @signheaders, $_;
415 }
416 }
417
418 $pgpbody = join ("", @$BodyR);
419
420 # Delete and create the temporary pgp-Files
421 unlink "$Config{'pgptmpf'}.txt";
422 unlink "$Config{'pgptmpf'}.txt.asc";
423 $signheaders = join(",", @signheaders);
424
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";
429 }
430 }
431
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";
436
437 # Start PGP, then read the signature;
438 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
439 `$PGPCommand`;
440
441 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
442 $/ = "$Config{'pgpbegin'}\n";
443 $_ = <FH>;
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"
448 }
449 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
450
451 $/ = "\n";
452 $_ = <FH>;
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";
457 }
458
459 if (defined($2)) {
460 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
461 } else {
462 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
463 }
464
465 do { # skip other pgp headers like
466 $_ = <FH>; # "charset:"||"comment:" until empty line
467 } while ! /^$/;
468
469 while (<FH>) {
470 chomp;
471 last if /^\Q$Config{'pgpend'}\E$/;
472 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
473 }
474
475 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
476
477 $_ = <FH>;
478 unless (eof(FH)) {
479 unlink $Config{'pgptmpf'} . ".txt";
480 unlink $Config{'pgptmpf'} . ".txt.asc";
481 die "$0: E: unexpected data following $Config{'pgpend'}\n";
482 }
483 close(FH);
484 unlink "$Config{'pgptmpf'}.txt.asc";
485
486 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
487 delete $$HeaderR{$Config{'pgpheader'}};
488
489 @pgphead = ();
490 foreach $header (@PGPorderheaders) {
491 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
492 push(@pgphead, "$$HeaderR{$header}");
493 delete $$HeaderR{$header};
494 }
495 }
496
497 foreach $header (keys %$HeaderR) {
498 if ($$HeaderR{$header} && $$HeaderR{$header} ne "\n") {
499 push(@pgphead, "$$HeaderR{$header}");
500 delete $$HeaderR{$header};
501 }
502 }
503
504 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
505 undef $tmppgpheader;
506
507 @pgpbody = split /$/m, $pgpbody;
508 my @pgpmessage = (@pgphead, "\n", @pgpbody);
509 return \@pgpmessage;
510}
511
512__END__
513
514################################ Documentation #################################
515
516=head1 NAME
517
518yapfaq - Post Usenet FAQs I<(yet another postfaq)>
519
520=head1 SYNOPSIS
521
522B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>]
523
524=head1 REQUIREMENTS
525
526=over 2
527
528=item -
529
530Perl 5.8 or later
531
532=item -
533
534Net::NNTP
535
536=item -
537
538Date::Calc
539
540=item -
541
542Getopt::Std
543
544=back
545
546Furthermore you need access to a news server to actually post FAQs.
547
548=head1 DESCRIPTION
549
550B<yapfaq> posts (one or more) FAQs to Usenet with a certain posting
551frequency (every n days, weeks, months or years), adding all necessary
552headers as defined in its config file (by default F<yapfaq.cfg>).
553
554=head2 Configuration
555
556F<yapfaq.cfg> consists of one or more blocks, separated by C<=====> on
557a single line, each containing the configuration for one FAQ as a set
558of definitions in the form of I<param = value>.
559
560=over 4
561
562=item B<Name> = I<project name>
563
564A name referring to your FAQ, also used for generation of a Message-ID.
565
566This value must be set.
567
568=item B<File> = I<file name>
569
570A file containing the message body of your FAQ and all pseudo headers
571(subheaders in the news.answers style).
572
573This value must be set.
574
575=item B<Posting-frequency> = I<time period>
576
577The posting frequency defines how often your FAQ will be posted.
578B<yapfaq> will only post your FAQ if this period of time has passed
579since the last posting.
580
581You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
582or I<B<m>onths> or I<B<y>ears>.
583
584This value must be set.
585
586=item B<Expires> = I<time period>
587
588The period of time after which your message will expire. An Expires
589header will be calculated adding this time period to today's date.
590
591You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
592or I<B<m>onths> or I<B<y>ears>.
593
594This setting is optional; the default is 3 months.
595
596=item B<From> = I<author>
597
598The author of your FAQ as it will appear in the From header of the
599message.
600
601This value must be set.
602
603=item B<Subject> = I<subject>
604
605The title of your FAQ as it will appear in the Subject header of the
606message.
607
608You may use the special string C<%LM> which will be replaced with
609the contents of the Last-Modified subheader in your I<File>.
610
611This value must be set.
612
613=item B<NGs> = I<newsgroups>
614
615A comma-separated list of newsgroup(s) to post your FAQ to as it will
616appear in the Newsgroups header of the message.
617
618This value must be set.
619
620=item B<Fup2> = I<newsgroup | poster>
621
622A comma-separated list of newsgroup(s) or the special string I<poster>
623as it will appear in the Followup-To header of the message.
624
625This setting is optional.
626
627=item B<MID-Format> = I<pattern>
628
629A pattern from which the message ID is generated as it will appear in
630the Message-ID header of the message.
631
632You may use the special strings C<%n> for the I<Name> of your project,
633C<%d> for the date the message is posted, C<%m> for the month and
634C<%y> for the year, respectively.
635
636This value must be set.
637
638=item B<Supersede> = I<yes>
639
640Add Supersedes header to the message containing the Message-ID header
641of the last posting.
642
643This setting is optional; you should set it to yes or leave it out.
644
645=item B<ExtraHeader> = I<additional headers>
646
647The contents of I<ExtraHeader> is added verbatim to the headers of
648your message so you can add custom headers like Approved.
649
650This setting is optional.
651
652=back
653
654=head2 Example configuration file
655
656 # name of your project
657 Name = 'testpost'
658
659 # file to post (complete body and pseudo-headers)
660 # ($File.cfg contains data on last posting and last MID)
661 File = 'test.txt'
662
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'
666
667 # time period after which the posting should expire
668 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
669 Expires = '3m'
670
671 # header "From:"
672 From = 'test@domain.invalid'
673
674 # header "Subject:"
675 # (may contain "%LM" which will be replaced by the contents of the
676 # Last-Modified pseudo header).
677 Subject = 'test noreply ignore'
678
679 # comma-separated list of newsgroup(s) to post to
680 # (header "Newsgroups:")
681 NGs = 'de.test'
682
683 # header "Followup-To:"
684 Fup2 = 'poster'
685
686 # Message-ID ("%n" is $Name)
687 MID-Format = '<%n-%d.%m.%y@domain.invalid>'
688
689 # Supersede last posting?
690 Supersede = yes
691
692 # extra headers (appended verbatim)
693 # use this for custom headers like "Approved:"
694 ExtraHeader = 'Approved: moderator@domain.invalid
695 X-Header: Some text'
696
697 # other projects may follow separated with "====="
698 =====
699
700 Name = 'othertest'
701 File = 'test.txt'
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'
706 Fup2 = 'de.test'
707 MID-Format = '<%n-%m.%y@domain.invalid>'
708 Supersede = yes
709
710Information about the last post and about how to form message IDs for
711posts is stored in a file named F<I<project name>.cfg> which will be
712generated if it does not exist. Each of those status files will
713contain two lines, the first being the date of the last time the FAQ
714was posted and the second being the message ID of that incarnation.
715
716=head1 OPTIONS
717
718=over 3
719
720=item B<-V> (version)
721
722Print out version and copyright information on B<yapfaq> and exit.
723
724=item B<-h> (help)
725
726Print this man page and exit.
727
728=item B<-v> (verbose)
729
730Print out status information while running to STDOUT.
731
732=item B<-p> (post unconditionally)
733
734Post (all) FAQs unconditionally ignoring the posting frequency setting.
735
736You may want to use this with the B<-f> option (see below).
737
738=item B<-d> (dry run)
739
740Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
741update any status information.
742
743=item B<-t> I<newsgroup(s) | CONSOLE> (test)
744
745Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
746newsgroups given after B<-t> as a comma-separated list or print the
747FAQs to STDOUT separated by lines of dashes if the special string
748C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
749do without embarassing yourself on Usenet. The status files are not
750updated when this option is given.
751
752You may want to use this with the B<-f> option (see below).
753
754=item B<-f> I<project name>
755
756Just deal with one FAQ only.
757
758By default B<yapfaq> will work on all FAQs that are defined in
759F<yapfaq.cfg>, check whether they are due for posting and - if they
760are - post them. Consequently when the B<-p> option is set all FAQs
761will be posted unconditionally. That may not be what you want to
762achieve, so you can limit the operation of B<yapfaq> to the named FAQ
763only.
764
765=item B<-s> I<program> (pipe to script)
766
767Instead of posting the article(s) to Usenet pipe them to the external
768I<program> on STDIN (which may post the article(s) then). A return
769value of 0 will be considered success.
770
771=back
772
773=head1 EXAMPLES
774
775Post all FAQs that are due for posting:
776
777 yapfaq
778
779Do a dry run, showing which FAQs would be posted:
780
781 yapfaq -dv
782
783Do a test run and print on STDOUT what the FAQ I<myfaq> would look
784like when posted, regardless whether it is due for posting or not:
785
786 yapfaq -pt CONSOLE -f myfaq
787
788Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
789if it is due:
790
791 yapfaq -t de.test -f myfaq
792
793=head1 ENVIRONMENT
794
795There are no special environment variables used by B<yapfaq>.
796
797=head1 FILES
798
799=over 4
800
801=item F<yapfaq.pl>
802
803The script itself.
804
805=item F<yapfaq.cfg>
806
807Configuration file for B<yapfaq>.
808
809=item F<*.cfg>
810
811Status data on FAQs.
812
813The status files will be created on successful posting if they don't
814already exist. The first line of the file will be the date of the last
815time the FAQ was posted and the second line will be the message ID of
816the last post of that FAQ.
817
818=back
819
820=head1 BUGS
821
822Many, I'm sure.
823
824=head1 SEE ALSO
825
826L<http://th-h.de/download/scripts.php> will have the current
827version of this program.
828
829=head1 AUTHOR
830
831Thomas Hochstein <thh@inter.net>
832
833Original author (until version 0.5b from 2003):
834Marc Brockschmidt <marc@marcbrockschmidt.de>
835
836
837=head1 COPYRIGHT AND LICENSE
838
839Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
840
841Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
842
843This program is free software; you may redistribute it and/or modify it
844under the same terms as Perl itself.
845
846=cut
This page took 0.0139 seconds and 4 git commands to generate.