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