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