Change: Authenticate only if $NNTPUser is set.
[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
6b421553 15my $Version = "0.6.2";
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;
40847f71 55use Net::Domain qw(hostfqdn);
dc88d139
TH
56use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
57use Fcntl ':flock'; # import LOCK_* constants
4251e545 58use Getopt::Std;
dc88d139
TH
59my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
60
4251e545
TH
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
dc88d139 69my @Config;
4251e545 70readconfig (\$ConfigFile, \@Config, \$Faq);
dc88d139
TH
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
0c6ebe78 77 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
dc88d139
TH
78 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
79 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
80 my ($Supersede) =($$_{'supersede'});
4251e545
TH
81
82 next if (defined($Faq) && $ActName ne $Faq);
83
dc88d139
TH
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 {
74407146 94 warn "$0: W: Couldn't open $File.cfg: $!\n";
dc88d139
TH
95 }
96
97 $SupersedeMID = "" unless $Supersede;
98
0c6ebe78
TH
99 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
100
4251e545
TH
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 {
a0605478 105 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Sender,\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
4251e545
TH
106 }
107 } elsif($Options{'v'}) {
108 print "$ActName: Nothing to do.\n";
dc88d139
TH
109 }
110}
111
112exit;
113
114################################## readconfig ##################################
4251e545
TH
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
dc88d139
TH
117
118sub readconfig{
4251e545 119 my ($File, $Config, $Faq) = @_;
dc88d139
TH
120 my ($LastEntry, $Error, $i) = ('','',0);
121
366322b2 122 print "Reading configuration.\n" if($Options{'v'});
4251e545 123
74407146 124 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
dc88d139 125 while (<FH>) {
4251e545 126 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
dc88d139
TH
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){
4251e545 140 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
dbca4ad8
TH
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"
dc88d139 143 }
dbca4ad8
TH
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"
dc88d139
TH
155 }
156 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
dbca4ad8 157 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
dc88d139 158 }
dbca4ad8 159 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
74407146 160 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
dc88d139 161 }
5ddba442 162 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
0c6ebe78
TH
163 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
164 }
40847f71
TH
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 }
dc88d139 168 }
dbca4ad8 169 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
dc88d139
TH
170 die $Error if $Error;
171}
172
0c6ebe78
TH
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
dc88d139
TH
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 {
0c6ebe78 195 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
dc88d139
TH
196 my (@Header,@Body,$MID,$InRealBody,$LastModified);
197
366322b2 198 print "$$ActName: Preparing to post.\n" if($Options{'v'});
4251e545 199
dc88d139
TH
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;
dbca4ad8 205 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID);
dc88d139
TH
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 /^$/;
8e1cb154 218 $LastModified = $1 if /^Last-modified: (\S+)$/i;
dc88d139
TH
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";
7823ece9
TH
235
236 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
237
0c6ebe78 238 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
dc88d139
TH
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";
0c6ebe78 243
dc88d139
TH
244 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
245 if ($LastModified) {
246 $$Subject =~ s/\%LM/$LastModified/;
247 }
248
4251e545
TH
249 # Test mode?
250 if($Options{'t'} and $Options{'t'} !~ /console/i) {
251 $$NG = $Options{'t'};
252 }
253
dc88d139
TH
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
366322b2 275 print "$$ActName: Posting article ...\n" if($Options{'v'});
dc88d139
TH
276 post(\@Article);
277
5ddba442
TH
278 # Test mode?
279 return if($Options{'t'});
280
366322b2 281 print "$$ActName: Save status information.\n" if($Options{'v'});
4251e545 282
74407146 283 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
dc88d139
TH
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
4251e545
TH
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
dc88d139 305 my $NewsConnection = Net::NNTP->new($NNTPServer, Reader => 1)
74407146 306 or die "$0: E: Can't connect to news server '$NNTPServer'!\n";
dc88d139 307
d69ba0bb 308 $NewsConnection->authinfo ($NNTPUser, $NNTPPass) if (defined($NNTPUser));
dc88d139
TH
309 $NewsConnection->post();
310 $NewsConnection->datasend (@$ArticleR);
311 $NewsConnection->dataend();
312
313 if (!$NewsConnection->ok()) {
314 open FH, ">>ERROR.dat";
114be302 315 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
dc88d139
TH
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) {
74407146 338 open (PGPPW, $PathtoPGPPass) or die "$0: E: Can't open $PathtoPGPPass: $!";
dc88d139
TH
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 {
74407146 346 die "$0: E: PGP-Passphrase is unknown!\n";
dc88d139
TH
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 {
74407146 352 die "$0: E: PGP-Passphrase is unknown!\n";
dc88d139
TH
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 {
74407146 358 die "$0: E: Passphrase is unknown!\n";
dc88d139
TH
359 }
360 } else {
74407146 361 die "$0: E: Unknown PGP-Version $PGPVersion!";
dc88d139
TH
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
74407146 410 open(FH, ">" . $pgptmpf . ".txt") or die "$0: E: can't open $pgptmpf: $!\n";
dc88d139
TH
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
74407146 413 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
dc88d139
TH
414
415 # Start PGP, then read the signature;
416 my $PGPCommand = getpgpcommand($PGPVersion);
417 `$PGPCommand`;
418
74407146 419 open (FH, "<" . $pgptmpf . ".txt.asc") or die "$0: E: can't open ".$pgptmpf.".txt.asc: $!\n";
dc88d139
TH
420 $/ = "$pgpbegin\n";
421 $_ = <FH>;
422 unless (m/\Q$pgpbegin\E$/o) {
423# unlink $pgptmpf . ".txt";
424# unlink $pgptmpf . ".txt.asc";
74407146 425 die "$0: E: $pgpbegin not found in ".$pgptmpf.".txt.asc\n"
dc88d139 426 }
74407146 427 unlink($pgptmpf . ".txt") or warn "$0: W: Couldn't unlink $pgptmpf.txt: $!\n";
dc88d139
TH
428
429 $/ = "\n";
430 $_ = <FH>;
431 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
432 unlink $pgptmpf . ".txt";
433 unlink $pgptmpf . ".txt.asc";
74407146 434 die "$0: E: didn't find PGP Version line where expected.\n";
dc88d139
TH
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";
74407146 459 die "$0: E: unexpected data following $pgpend\n";
dc88d139
TH
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}
272b0243
TH
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.053301 seconds and 4 git commands to generate.