Update documentation for .rcfile.
[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
d60c2d5f
TH
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 ##################################
2507947f
TH
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');
dc88d139
TH
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;
40847f71 57use Net::Domain qw(hostfqdn);
dc88d139
TH
58use Date::Calc qw(Add_Delta_YM Add_Delta_Days Delta_Days Today);
59use Fcntl ':flock'; # import LOCK_* constants
4251e545 60use Getopt::Std;
dc88d139
TH
61my ($TDY, $TDM, $TDD) = Today(); #TD: Today's date
62
b9550622 63# read commandline options
4251e545 64my %Options;
86c0a100 65getopts('Vhvpdt:f:c:s:', \%Options);
b9550622 66# -V: print version / copyright information
a052296f
TH
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}
b9550622 72# -h: feed myself to perldoc
4251e545 73if ($Options{'h'}) {
ae3b1b79 74 exec ('perldoc', $0);
4251e545
TH
75 exit(0);
76};
b9550622 77# -f: set $Faq
4251e545
TH
78my ($Faq) = $Options{'f'} if ($Options{'f'});
79
d60c2d5f 80# read runtime configuration (configuration variables)
86c0a100
TH
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}
d60c2d5f 87
b9550622 88# read configuration (configured FAQs)
dc88d139 89my @Config;
2507947f 90readconfig (\$Config{'ConfigFile'}, \@Config, \$Faq);
dc88d139 91
b9550622
TH
92# for each FAQ:
93# - parse configuration
94# - read status data
95# - if FAQ is due: call postfaq()
dc88d139
TH
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
0c6ebe78 101 my ($ActName,$File,$PFreq,$Expire) =($$_{'name'},$$_{'file'},$$_{'posting-frequency'},$$_{'expires'});
dc88d139
TH
102 my ($From,$Subject,$NG,$Fup2)=($$_{'from'},$$_{'subject'},$$_{'ngs'},$$_{'fup2'});
103 my ($MIDF,$ReplyTo,$ExtHea)=($$_{'mid-format'},$$_{'reply-to'},$$_{'extraheader'});
104 my ($Supersede) =($$_{'supersede'});
4251e545 105
b9550622 106 # -f: loop if not FAQ to post
4251e545
TH
107 next if (defined($Faq) && $ActName ne $Faq);
108
b9550622 109 # read status data
dc88d139
TH
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 {
74407146 120 warn "$0: W: Couldn't open $File.cfg: $!\n";
dc88d139
TH
121 }
122
123 $SupersedeMID = "" unless $Supersede;
124
0c6ebe78
TH
125 ($NPY,$NPM,$NPD) = calcdelta ($LPY,$LPM,$LPD,$PFreq);
126
b9550622 127 # if FAQ is due: get it out
4251e545
TH
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 {
2507947f 132 postfaq(\$ActName,\$File,\$From,\$Subject,\$NG,\$Fup2,\$MIDF,\$ExtHea,\$Config{'Sender'},\$TDY,\$TDM,\$TDD,\$ReplyTo,\$SupersedeMID,\$Expire);
4251e545
TH
133 }
134 } elsif($Options{'v'}) {
135 print "$ActName: Nothing to do.\n";
dc88d139
TH
136 }
137}
138
139exit;
140
d60c2d5f
TH
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
dc88d139 161################################## readconfig ##################################
4251e545
TH
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
dc88d139
TH
164
165sub readconfig{
4251e545 166 my ($File, $Config, $Faq) = @_;
dc88d139
TH
167 my ($LastEntry, $Error, $i) = ('','',0);
168
366322b2 169 print "Reading configuration.\n" if($Options{'v'});
4251e545 170
74407146 171 open FH, "<$$File" or die "$0: E: Can't open $$File: $!";
dc88d139 172 while (<FH>) {
4251e545 173 next if (defined($$Faq) && !/^\s*=====\s*$/ && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
dc88d139
TH
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){
4251e545 187 next if (defined($$Faq) && defined($$Config[$i]{'name'}) && $$Config[$i]{'name'} ne $$Faq );
dbca4ad8
TH
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"
dc88d139 190 }
dbca4ad8
TH
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"
dc88d139
TH
202 }
203 unless(!$$Config[$i]{'fup2'} || $$Config[$i]{'fup2'} =~ /^\S+$/) {
dbca4ad8 204 $Error .= "E: The Followup-To header for your project \"$$Config[$i]{'name'}\" contains whitespaces.\n"
dc88d139 205 }
dbca4ad8 206 unless(defined($$Config[$i]{'posting-frequency'}) && $$Config[$i]{'posting-frequency'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
74407146 207 $Error .= "E: The Posting-frequency for your project \"$$Config[$i]{'name'}\" is invalid.\n"
dc88d139 208 }
5ddba442 209 unless(!$$Config[$i]{'expires'} || $$Config[$i]{'expires'} =~ /^\s*\d+\s*[dwmy]\s*$/) {
0c6ebe78
TH
210 warn "$0: W: The Expires for your project \"$$Config[$i]{'name'}\" is invalid - set to 3 month.\n";
211 }
40847f71
TH
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 }
dc88d139 215 }
dbca4ad8 216 $Error .= "-" x 25 . 'program terminated' . "-" x 25 . "\n" if $Error;
dc88d139
TH
217 die $Error if $Error;
218}
219
0c6ebe78
TH
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
dc88d139
TH
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 {
0c6ebe78 242 my ($ActName,$File,$From,$Subject,$NG,$Fup2,$MIDF,$ExtraHeaders,$Sender,$TDY,$TDM,$TDD,$ReplyTo,$Supersedes,$Expire) = @_;
dc88d139
TH
243 my (@Header,@Body,$MID,$InRealBody,$LastModified);
244
366322b2 245 print "$$ActName: Preparing to post.\n" if($Options{'v'});
4251e545 246
dc88d139
TH
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;
dbca4ad8 252 $MID = '<%n-%d.%m.%y@'.hostfqdn.'>' if !defined($MID);
dc88d139
TH
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
dc88d139
TH
258 #Now get the body:
259 open (FH, "<$$File");
260 while (<FH>){
261 s/\r//;
262 push (@Body, $_), next if $InRealBody;
263 $InRealBody++ if /^$/;
8e1cb154 264 $LastModified = $1 if /^Last-modified: (\S+)$/i;
dc88d139
TH
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";
7823ece9
TH
281
282 $$Expire = '3m' if !$$Expire; # set default if unset: 3 month
283
0c6ebe78 284 my ($expY,$expM,$expD) = calcdelta ($year,$month,$day,$$Expire);
dc88d139
TH
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";
0c6ebe78 289
dc88d139
TH
290 #Replace %LM by the content of the news.answer-pseudo-header Last-modified:
291 if ($LastModified) {
292 $$Subject =~ s/\%LM/$LastModified/;
293 }
294
4251e545
TH
295 # Test mode?
296 if($Options{'t'} and $Options{'t'} !~ /console/i) {
297 $$NG = $Options{'t'};
298 }
299
dc88d139
TH
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
b9550622 319 # sign article if $UsePGP is true
2507947f 320 my @Article = ($Config{'UsePGP'})?@{signpgp(\@Header, \@Body)}:(@Header, "\n", @Body);
dc88d139 321
b9550622 322 # post article
366322b2 323 print "$$ActName: Posting article ...\n" if($Options{'v'});
dc88d139
TH
324 post(\@Article);
325
5ddba442
TH
326 # Test mode?
327 return if($Options{'t'});
328
b9550622 329 # otherwise: update status data
366322b2 330 print "$$ActName: Save status information.\n" if($Options{'v'});
4251e545 331
74407146 332 open (FH, ">$$File.cfg") or die "$0: E: Can't open $$File.cfg: $!";
dc88d139
TH
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
4251e545
TH
346 # Test mode?
347 if(defined($Options{'t'}) and $Options{'t'} =~ /console/i) {
55bfbd3c 348 print "-----BEGIN--------------------------------------------------\n";
4251e545 349 print @$ArticleR;
55bfbd3c 350 print "------END---------------------------------------------------\n";
4251e545
TH
351 return;
352 }
353
b855559e
TH
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
2507947f
TH
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'}));
dc88d139
TH
364 $NewsConnection->post();
365 $NewsConnection->datasend (@$ArticleR);
366 $NewsConnection->dataend();
367
b9550622 368 # Posting failed? Save to ERROR.dat
dc88d139
TH
369 if (!$NewsConnection->ok()) {
370 open FH, ">>ERROR.dat";
114be302 371 print FH "\nPosting failed! Saving to ERROR.dat. Response from news server:\n";
dc88d139
TH
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') {
2507947f
TH
393 if ($Config{'PathtoPGPPass'} && !$Config{'PGPPass'}) {
394 open (PGPPW, $Config{'PathtoPGPPass'}) or die "$0: E: Can't open $Config{'PathtoPGPPass'}: $!";
395 Config{'$PGPPass'} = <PGPPW>;
dc88d139
TH
396 close PGPPW;
397 }
398
2507947f
TH
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";
dc88d139 401 } else {
74407146 402 die "$0: E: PGP-Passphrase is unknown!\n";
dc88d139
TH
403 }
404 } elsif ($PGPVersion eq '5') {
2507947f
TH
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'};
dc88d139 407 } else {
74407146 408 die "$0: E: PGP-Passphrase is unknown!\n";
dc88d139
TH
409 }
410 } elsif ($PGPVersion =~ m/GPG/io) {
2507947f
TH
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";
dc88d139 413 } else {
74407146 414 die "$0: E: Passphrase is unknown!\n";
dc88d139
TH
415 }
416 } else {
74407146 417 die "$0: E: Unknown PGP-Version $PGPVersion!";
dc88d139
TH
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
2507947f
TH
455 unlink "$Config{'pgptmpf'}.txt";
456 unlink "$Config{'pgptmpf'}.txt.asc";
dc88d139
TH
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
2507947f 466 open(FH, ">" . $Config{'pgptmpf'} . ".txt") or die "$0: E: can't open $Config{'pgptmpf'}: $!\n";
dc88d139 467 print FH $pgphead, "\n", $pgpbody;
2507947f 468 print FH "\n" if ($Config{'PGPVersion'} =~ m/GPG/io); # workaround a pgp/gpg incompatibility - should IMHO be fixed in pgpverify
74407146 469 close(FH) or warn "$0: W: Couldn't close TMP: $!\n";
dc88d139
TH
470
471 # Start PGP, then read the signature;
2507947f 472 my $PGPCommand = getpgpcommand($Config{'PGPVersion'});
dc88d139
TH
473 `$PGPCommand`;
474
2507947f
TH
475 open (FH, "<" . $Config{'pgptmpf'} . ".txt.asc") or die "$0: E: can't open ".$Config{'pgptmpf'}.".txt.asc: $!\n";
476 $/ = "$Config{'pgpbegin'}\n";
dc88d139 477 $_ = <FH>;
2507947f
TH
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"
dc88d139 482 }
2507947f 483 unlink($Config{'pgptmpf'} . ".txt") or warn "$0: W: Couldn't unlink $Config{'pgptmpf'}.txt: $!\n";
dc88d139
TH
484
485 $/ = "\n";
486 $_ = <FH>;
487 unless (m/^Version: (\S+)(?:\s(\S+))?/o) {
2507947f
TH
488 unlink $Config{'pgptmpf'} . ".txt";
489 unlink $Config{'pgptmpf'} . ".txt.asc";
74407146 490 die "$0: E: didn't find PGP Version line where expected.\n";
dc88d139
TH
491 }
492
493 if (defined($2)) {
2507947f 494 $$HeaderR{$Config{'pgpheader'}} = $1."-".$2." ".$signheaders;
dc88d139 495 } else {
2507947f 496 $$HeaderR{$Config{'pgpheader'}} = $1." ".$signheaders;
dc88d139
TH
497 }
498
499 do { # skip other pgp headers like
500 $_ = <FH>; # "charset:"||"comment:" until empty line
501 } while ! /^$/;
502
503 while (<FH>) {
504 chomp;
2507947f
TH
505 last if /^\Q$Config{'pgpend'}\E$/;
506 $$HeaderR{$Config{'pgpheader'}} .= "\n\t$_";
dc88d139
TH
507 }
508
2507947f 509 $$HeaderR{$Config{'pgpheader'}} .= "\n" unless ($$HeaderR{$Config{'pgpheader'}} =~ /\n$/s);
dc88d139
TH
510
511 $_ = <FH>;
512 unless (eof(FH)) {
2507947f
TH
513 unlink $Config{'pgptmpf'} . ".txt";
514 unlink $Config{'pgptmpf'} . ".txt.asc";
515 die "$0: E: unexpected data following $Config{'pgpend'}\n";
dc88d139
TH
516 }
517 close(FH);
2507947f 518 unlink "$Config{'pgptmpf'}.txt.asc";
dc88d139 519
2507947f
TH
520 my $tmppgpheader = $Config{'pgpheader'} . ": " . $$HeaderR{$Config{'pgpheader'}};
521 delete $$HeaderR{$Config{'pgpheader'}};
dc88d139
TH
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
2507947f 538 push @pgphead, ("X-PGP-Key: " . $Config{'PGPSigner'} . "\n"), $tmppgpheader;
dc88d139
TH
539 undef $tmppgpheader;
540
541 @pgpbody = split /$/m, $pgpbody;
542 my @pgpmessage = (@pgphead, "\n", @pgpbody);
543 return \@pgpmessage;
544}
272b0243
TH
545
546__END__
547
548################################ Documentation #################################
549
550=head1 NAME
551
552yapfaq - Post Usenet FAQs I<(yet another postfaq)>
553
554=head1 SYNOPSIS
555
227afd47 556B<yapfaq> [B<-hvpd>] [B<-t> I<newsgroups> | CONSOLE] [B<-f> I<project name>] [B<-s> I<program>] [B<-c> I<.rc file>]
272b0243
TH
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
227afd47
TH
592of definitions in the form of I<param = value>. Everything after a "#"
593sign is ignored so you may comment your configuration file.
272b0243
TH
594
595=over 4
596
597=item B<Name> = I<project name>
598
599A name referring to your FAQ, also used for generation of a Message-ID.
600
601This value must be set.
602
603=item B<File> = I<file name>
604
605A file containing the message body of your FAQ and all pseudo headers
606(subheaders in the news.answers style).
607
608This value must be set.
609
610=item B<Posting-frequency> = I<time period>
611
612The posting frequency defines how often your FAQ will be posted.
613B<yapfaq> will only post your FAQ if this period of time has passed
614since the last posting.
615
616You can declare that time period either in I<B<d>ays> or I<B<w>weeks>
617or I<B<m>onths> or I<B<y>ears>.
618
619This value must be set.
620
621=item B<Expires> = I<time period>
622
623The period of time after which your message will expire. An Expires
624header will be calculated adding this time period to today's date.
625
626You can declare this time period either in I<B<d>ays> or I<B<w>weeks>
627or I<B<m>onths> or I<B<y>ears>.
628
629This setting is optional; the default is 3 months.
630
631=item B<From> = I<author>
632
633The author of your FAQ as it will appear in the From header of the
634message.
635
636This value must be set.
637
638=item B<Subject> = I<subject>
639
640The title of your FAQ as it will appear in the Subject header of the
641message.
642
643You may use the special string C<%LM> which will be replaced with
644the contents of the Last-Modified subheader in your I<File>.
645
646This value must be set.
647
648=item B<NGs> = I<newsgroups>
649
650A comma-separated list of newsgroup(s) to post your FAQ to as it will
651appear in the Newsgroups header of the message.
652
653This value must be set.
654
655=item B<Fup2> = I<newsgroup | poster>
656
657A comma-separated list of newsgroup(s) or the special string I<poster>
658as it will appear in the Followup-To header of the message.
659
660This setting is optional.
661
662=item B<MID-Format> = I<pattern>
663
664A pattern from which the message ID is generated as it will appear in
665the Message-ID header of the message.
666
667You may use the special strings C<%n> for the I<Name> of your project,
668C<%d> for the date the message is posted, C<%m> for the month and
669C<%y> for the year, respectively.
670
671This value must be set.
672
673=item B<Supersede> = I<yes>
674
675Add Supersedes header to the message containing the Message-ID header
676of the last posting.
677
678This setting is optional; you should set it to yes or leave it out.
679
680=item B<ExtraHeader> = I<additional headers>
681
682The contents of I<ExtraHeader> is added verbatim to the headers of
683your message so you can add custom headers like Approved.
684
685This setting is optional.
686
687=back
688
227afd47 689=head3 Example configuration file
272b0243
TH
690
691 # name of your project
692 Name = 'testpost'
693
694 # file to post (complete body and pseudo-headers)
695 # ($File.cfg contains data on last posting and last MID)
696 File = 'test.txt'
697
698 # how often your project should be posted
699 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
700 Posting-frequency = '1d'
701
702 # time period after which the posting should expire
703 # use (d)ay OR (w)eek OR (m)onth OR (y)ear
704 Expires = '3m'
705
706 # header "From:"
707 From = 'test@domain.invalid'
708
709 # header "Subject:"
710 # (may contain "%LM" which will be replaced by the contents of the
711 # Last-Modified pseudo header).
712 Subject = 'test noreply ignore'
713
714 # comma-separated list of newsgroup(s) to post to
715 # (header "Newsgroups:")
716 NGs = 'de.test'
717
718 # header "Followup-To:"
719 Fup2 = 'poster'
720
721 # Message-ID ("%n" is $Name)
722 MID-Format = '<%n-%d.%m.%y@domain.invalid>'
723
724 # Supersede last posting?
725 Supersede = yes
726
727 # extra headers (appended verbatim)
728 # use this for custom headers like "Approved:"
729 ExtraHeader = 'Approved: moderator@domain.invalid
730 X-Header: Some text'
731
732 # other projects may follow separated with "====="
733 =====
734
735 Name = 'othertest'
736 File = 'test.txt'
737 Posting-frequency = '2m'
738 From = 'My Name <my.name@domain.invalid>'
739 Subject = 'Test of yapfag <%LM>'
740 NGs = 'de.test,de.alt.test'
741 Fup2 = 'de.test'
742 MID-Format = '<%n-%m.%y@domain.invalid>'
743 Supersede = yes
744
227afd47
TH
745=head3 Status Information
746
272b0243
TH
747Information about the last post and about how to form message IDs for
748posts is stored in a file named F<I<project name>.cfg> which will be
749generated if it does not exist. Each of those status files will
750contain two lines, the first being the date of the last time the FAQ
751was posted and the second being the message ID of that incarnation.
752
227afd47
TH
753=head2 Runtime Configuration
754
755Apart from configuring which FAQ(s) to post you may (re)set some
756runtime configuration variables via the .rcfile (by default
757F<.yapfaqrc>). F<.yapfaqrc> must contain one definition in the form of
758I<param = value> on each line; everything after a "#" sign is ignored.
759
760If you omit some settings they will be set to default values hardcoded
761in F<yapfaq.pl>.
762
763B<Please note that all parameter names are case-sensitive!>
764
765=over 4
766
767=item B<NNTPServer> = I<NNTP server> (mandatory)
768
769Host name of the NNTP server to post to. Must be set (or omitted; the
770default is "localhost"); if set to en empty string, B<yapfaq> falls
771back to Perl's build-in defaults (contents of environment variables
772NNTPSERVER and NEWSHOST; if not set, default from Net::Config; if not
773set, "news" is used).
774
775=item B<NNTPUser> = I<user name> (optional)
776
777User name used for authentication with the NNTP server (I<AUTHINFO
778USER>).
779
780This setting is optional; if it is not set, I<NNTPPass> is ignored and
781no authentication is tried.
782
783=item B<NNTPPass> = I<password> (optional)
784
785Password used for authentication with the NNTP server (I<AUTHINFO
786PASS>).
787
788This setting is optional; it must be set if I<NNTPUser> is present.
789
790=item B<Sender> = I<Sender header> (optional)
791
792The Sender header that will be added to every posted message.
793
794This setting is optional.
795
796=item B<ConfigFile> = I<configuration file> (mandatory)
797
798The configuration file defining the FAQ(s) to post. Must be set (or
799omitted; the default is "yapfaq.cfg").
800
801=item B<UsePGP> = I<whether to add a digital signature> (optional)
802
803Boolean value (0 or 1) controlling whether the FAQs will get digitally
804signed via an X-PGP-Sig header.
805
806This setting is optional; the default is 0.
807
808If you have set I<UsePGP> to 1, you must also supply the necessary
809information on your PGP oder GPG installation; please refer to the
810sample F<.yapfaqrc> file (see below) for more information on this
811topic.
812
813=back
814
815=head3 Example runtime configuration file
816
817 NNTPServer = 'localhost'
818 NNTPUser = ''
819 NNTPPass = ''
820 Sender = ''
821 ConfigFile = 'yapfaq.cfg'
822 UsePGP = 0
823
824 ################################## PGP-Config #################################
825 pgp = '/usr/bin/pgp' # path to pgp
826 PGPVersion = '2' # Use 2 for 2.X 5 for PGP > 2.X and GPG for GPG
827 PGPSigner = '' # sign as who?
828 PGPPass = '' # pgp2 only
829 PathtoPGPPass = '' # pgp2 pgp5 and gpg
830 pgpbegin = '-----BEGIN PGP SIGNATURE-----' # Begin of PGP-Signature
831 pgpend = '-----END PGP SIGNATURE-----' # End of PGP-Signature
832 pgptmpf = 'pgptmp' # temporary file for PGP.
833 pgpheader = 'X-PGP-Sig'
834
835=head3 Using more than one runtime configuration
836
837You may use more than one runtime configuration file with the B<-c>
838option (see below).
839
272b0243
TH
840=head1 OPTIONS
841
842=over 3
843
a052296f
TH
844=item B<-V> (version)
845
846Print out version and copyright information on B<yapfaq> and exit.
847
272b0243
TH
848=item B<-h> (help)
849
ae3b1b79 850Print this man page and exit.
272b0243
TH
851
852=item B<-v> (verbose)
853
854Print out status information while running to STDOUT.
855
856=item B<-p> (post unconditionally)
857
858Post (all) FAQs unconditionally ignoring the posting frequency setting.
859
860You may want to use this with the B<-f> option (see below).
861
862=item B<-d> (dry run)
863
864Start B<yapfaq> in simulation mode, i.e. don't post anything and don't
865update any status information.
866
867=item B<-t> I<newsgroup(s) | CONSOLE> (test)
868
869Don't post to the newsgroups defined in F<yqpfaq.cfg>, but to the
870newsgroups given after B<-t> as a comma-separated list or print the
871FAQs to STDOUT separated by lines of dashes if the special string
872C<CONSOLE> is given. This can be used to preview what B<yapfaq> would
873do without embarassing yourself on Usenet. The status files are not
874updated when this option is given.
875
876You may want to use this with the B<-f> option (see below).
877
878=item B<-f> I<project name>
879
880Just deal with one FAQ only.
881
882By default B<yapfaq> will work on all FAQs that are defined in
883F<yapfaq.cfg>, check whether they are due for posting and - if they
884are - post them. Consequently when the B<-p> option is set all FAQs
885will be posted unconditionally. That may not be what you want to
886achieve, so you can limit the operation of B<yapfaq> to the named FAQ
887only.
888
b855559e
TH
889=item B<-s> I<program> (pipe to script)
890
891Instead of posting the article(s) to Usenet pipe them to the external
892I<program> on STDIN (which may post the article(s) then). A return
893value of 0 will be considered success.
894
227afd47
TH
895=item B<-c> I<.rc file>
896
897Load another runtime configuration file (.rc file) than F<.yaofaq.rc>.
898
899You may for example define another usenet server to post your FAQ(s)
900to or load another configuration file defining (an)other FAQ(s).
901
272b0243
TH
902=back
903
904=head1 EXAMPLES
905
906Post all FAQs that are due for posting:
907
908 yapfaq
909
910Do a dry run, showing which FAQs would be posted:
911
912 yapfaq -dv
913
914Do a test run and print on STDOUT what the FAQ I<myfaq> would look
915like when posted, regardless whether it is due for posting or not:
916
917 yapfaq -pt CONSOLE -f myfaq
918
919Do a "real" test run and post the FAQ I<myfaq> to I<de.test>, but only
920if it is due:
921
922 yapfaq -t de.test -f myfaq
923
924=head1 ENVIRONMENT
925
926There are no special environment variables used by B<yapfaq>.
927
928=head1 FILES
929
930=over 4
931
932=item F<yapfaq.pl>
933
934The script itself.
935
227afd47
TH
936=item F<.yapfaqrc>
937
938Runtime configuration file for B<yapfaq>.
939
272b0243
TH
940=item F<yapfaq.cfg>
941
942Configuration file for B<yapfaq>.
943
944=item F<*.cfg>
945
946Status data on FAQs.
947
948The status files will be created on successful posting if they don't
949already exist. The first line of the file will be the date of the last
950time the FAQ was posted and the second line will be the message ID of
951the last post of that FAQ.
952
953=back
954
955=head1 BUGS
956
957Many, I'm sure.
958
959=head1 SEE ALSO
960
961L<http://th-h.de/download/scripts.php> will have the current
962version of this program.
963
964=head1 AUTHOR
965
966Thomas Hochstein <thh@inter.net>
967
227afd47 968Original author (up to version 0.5b, dating from 2003):
272b0243
TH
969Marc Brockschmidt <marc@marcbrockschmidt.de>
970
272b0243
TH
971=head1 COPYRIGHT AND LICENSE
972
973Copyright (c) 2003 Marc Brockschmidt <marc@marcbrockschmidt.de>
974
975Copyright (c) 2010 Thomas Hochstein <thh@inter.net>
976
977This program is free software; you may redistribute it and/or modify it
978under the same terms as Perl itself.
979
980=cut
This page took 0.066689 seconds and 4 git commands to generate.